1、一个NSObject对象占用多少内存?

回答:

系统分配了16个字节给NSObject对象(通过malloc_size函数获得)(因为内存对齐,必须是16的倍数)

但NSObject对象内部只有一个isa指针,只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

2、对象的isa指针指向哪里?

回答:
  • instance对象的isa指针指向class对象

  • class对象的isa指针指向meta-class对象

  • meta-class对象的isa指针指向基类的meta-class对象

3、OC的类信息存放在哪里?

回答:
  • 对象方法、属性、成员变量、协议信息,存放在class对象中
  • 类方法,存放在meta-class对象中
  • 成员变量的具体值,存放在instance对象中

4、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

回答:
  • 利用RuntimeAPI生成一个子类NSKVOXXXNotifying,并且让instance对象的isa指针指向这个全新的子类
  • 当修改instance对象的属性时,会调用Foundation框架中的_NSSetXXXValueAndNotify函数(重写了setter方法):
    • willChangeValueForKey:
    • 父类原来的setter方法
    • didChangeValueForKey:
      • 内部会触发监听器(Observer)的监听方法(observeValueForKeyPath:ofObject:change:context)

5、如何手动触发KVO?

回答:

手动去调用willChangeValueForKey:和didChangeValueForKey:的方法

6、直接修改成员变量会触发KVO吗?

回答:

不会,因为修改成员变量并不会调用setter方法

7、通过KVC修改属性会触发KVO吗?

回答:

会触发KVO,通过KVC修改相当于手动调用了willChangeValueForKey:和didChangeValueForKey:

8、KVC的赋值和取值的过程是怎样的?原理是什么?

回答:

赋值

image-20240307174322871

取值

image-20240307174430932

9、Category的使用场合是什么?

回答:
  • 类包含了很多个方法实现,而这些方法需要不同团队的成员来实现
  • 当你在使用基础类库中的类时,你不想继承这些类,而只是想添加一些方法的时候

10、Category的实现原理

回答:
  • Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
  • 程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

11、Category和Class Extension的区别是什么?

回答:
  • Class Extension是在程序编译的时候,它的数据就已经包含在类信息中了
  • Category在底层是以结构体的形式存在,在程序运行的时候,才会将数据合并到类信息中

12、Category中有load方法吗?load方法是什么时候调用的?load方法能继承吗?

回答:
  • load方法
  • load方法在runtime加载类、分类的时候调用,而且只调用一次
  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

13、load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承时它们之间的调用过程?

回答:

load方法

  • load方法会在Runtime加载类、分类的时候调用

  • 每个类、分类的load,在程序运行的过程中只调用一次

  • 调用顺序:

    • 先调用类的load

      • 按照编译的先后顺序调用(先编译,先调用)

      • 调用子类的load之前会先调用父类的load

    • 再调用分类的load

      • 按照编译先后顺序调用(先编译,先调用)
  • load方法系统调用和主动调用的区别

    • 系统调用load是直接找到类或分类中的方法的内存地址直接调用
    • 主动调用load是通过消息机制来发送消息的,会在对应的消息列表里按顺序遍历一层层查找,找到就调用

initialize方法

  • initialize方法会在类第一次接收到消息时调用

  • 调用顺序:

    • 先调用父类的initialize,再调用子类的initialize,每个类只会初始化1次(如果子类没有实现initialize方法,会调用父类的initialize方法,所以父类的initialize方法可能会调用多次)
  • initialize的调用是通过消息机制来发送消息的

14、Category能否添加成员变量?如果可以,如何给Category添加成员变量?

回答:

不能直接给Category添加成员变量,但是可以简介实现Category有成员变量的效果——关联对象

15、Block的原理是怎样的?Block的本质是什么?

回答:

其本质就是封装了函数调用以及调用环境的OC对象

16、__block的作用是什么?有什么使用注意点?

回答:

可以将修饰的对象包装成一个对象,解决在block内部无法修改外部变量的问题。

__block内部会进行内存管理,在MRC环境下不会对对象进行强引用。

17、看下面代码,分别输出的值是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int a = 10;
static int b = 10;

int main(int argc, const char * argv[]) {
@autoreleasepool {

auto int age = 10;
static int height = 10;

void (^block)(void) = ^{
NSLog(@"age is %d, height is %d", age, height);
NSLog(@"a is %d, b is %d", a, b);
};

age = 20;
height = 20;
a = 20;
b = 20;

block();
}
return 0;
}
回答:

输出结果为:age=10,height=20,a=20,b=20

age是自动变量,是值传递

height表示的是指针传递,block获取的是该变量的地址

a/b都是全局变量,block不会捕获,需要时直接拿取当前最新的值就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int a = 10;
static int b = 10;

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int age = 10;
static int height = 10;

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));

age = 20;
height = 20;
a = 20;
b = 20;

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}

18、看下面代码,block内部会不会捕获self?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@interface Person : NSObject

@property (copy, nonatomic) NSString *name;

- (instancetype)initWithName:(NSString *)name;
@end

@implementation Person

- (void)test
{
void (^block)(void) = ^{
NSLog(@"-------%d", [self name]);
};
block();
}

- (instancetype)initWithName:(NSString *)name
{
if (self = [super init]) {
self.name = name;
}
return self;
}

@end

回答:

会捕获,因为self本质上也是一个局部变量,block内部会生成一个变量来保存Person对象的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// 函数都会生成隐式参数self和_cmd
static void _I_Person_test(Person * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

19、Block的属性词为什么是Copy?使用Block有哪些注意事项?

回答:

block如果没有进行copy操作,就不会放在堆上。放到堆上的主要目的是方便我们来控制它的生命周期,可以更有效的进行内存管理。

注意事项:注意不要产生循环引用