iOS底层高级面试题整理一(II)
iOS底层高级面试题整理【OC本质、KVC、KVO、Categroy、Block】(II)
6. Category
6.1 Category的实现原理
- Category编译之后的底层结构是
struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
1 | |
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
6.2 Category的使用场合
- 在不修改原有类代码的情况下,为类添对象方法或者类方法,提供类的扩展
- 或者为类关联新的属性、添加协议、添加属性、关联成员变量
- 分解庞大的类文件,可以将类的实现分散到多个不同的文件或者不同的框架中,方便代码的管理
6.3 Category和Extension的区别是什么?
- Class Extension在**
编译期**决议,在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。它的数据就已经包含在类信息中,它就是类的一部分,伴随着类的产生而产生,也随着类的消失而消失 - Category是在**
运行时**,才会将数据合并到类信息中 - 扩展可以添加实例变量,而类别是无法添加实例变量的(因为在运行期,对象的**
内存布局已经确定,且Category的结构体中没有成员变量列表**,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)
6.4 Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
- 有load方法
- load方法在runtime加载类、分类的时候调用
- load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
6.5 Category能否添加成员变量?如果可以,如何给Category添加成员变量?
- 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
- Category是发生在运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构)
- 可以通过 runtime 动态的关联属性
- 分类中添加属性时,虽然在分类中可以写@property 添加属性,但是不会自动生成私有属性(_age),也不会生成set,get方法的实现,只会生成set,get的声明,需要我们自己去实现setter/getter。
7. load和initialize
7.1 load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
load是类加载到内存的时候调用, 优先父类->子类->分类initialize是类第一次收到消息时候调用,优先分类->子类->父类- 同级别和编译顺序有关系
load方法是在 main 函数之前调用的- 当类第一次收到消息的时候会调用类的
initialize方法 是通过 runtime 的消息机制objc_msgSend(obj,@selector())进行调用的 优先调用分类的 initialize, 如果没有分类会调用 子类的,如果子类未实现则调用 父类的
7.2 Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
- 有load方法
- load方法在runtime加载类、分类的时候调用
- load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
7.3 关联对象
关联对象并不存储在被关联对象本身内存中,而是有一个全局统一的 AssociationsManager中 一个实例对象就对应一个ObjectAssociationMap, 而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation, ObjcAssociation中存储着关联对象的value和policy策略 删除的时候接收一个object对象,然后遍历删除该对象所有的关联对象 设置关联对象_object_set_associative_reference的是时候,如果传入的value为空就删除这个关联对象
objc_AssociationPolicy policy参数: 属性以什么形式保存的策略。
1
2
3
4
5
6
7typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};objc_AssociationPolicy (关联策略)
对应的修饰符
OBJC_ASSOCIATION_ASSIGN
assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC
strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC
copy, nonatomic
OBJC_ASSOCIATION_RETAIN
strong, atomic
OBJC_ASSOCIATION_COPY
copy, atomic
8 BLock
8.1 block本质
block底层就是一个struct __main_block_impl_0类型的结构体,这个结构体中包含一个isa指针,Block 本质是一个封装了函数调用以及函数调用环境的 OC 对象,它主要由一个 isa 指针和一个 impl 函数指针和一个 descriptor 组成。 有点类似 C 里面的函数指针。
8.2 block内存布局
1 | |
8.3 底层结构
Block 的类型都是继承于 NSBlock,最终是继承于 NSObject。 block底层结构就是__main_block_impl_0结构体,内部包含了impl结构体和Desc结构体以及外部需要访问的变量,block将需要执行的代码放到一个函数里
impl内部的FuncPtr指向这个函数的地址,通过地址调用这个函数,就可以执行block里面的代码了Desc用来描述block内部的
reserved作保留Block_size描述block占用内存8.4 block的变量捕获

局部变量block访问方式是值传递,auto自动变量可能会销毁,内存可能会消失,不采用指针访问;局部静态变量block访问方式是指针传递,static变量一直保存在内存中,指针访问即可;全局变量、静态全局变量block不需要对变量捕获,直接取值
修饰block的关键词什么时候用 copy, 什么时候用strong?
- 在非 ARC 的情况下,对于 block 类型的属性应该使用copy ,因为 block 需要维持其作用域中捕获的变量。
- 在 ARC中编译器会自动对 block 进行 copy 操作,因此使用 strong 或者 copy 都可以,没有什么区别,但是苹果仍然建议使用 copy 来指明编译器的行为。
8.5 block类型
block类型
环境
存储域
copy操作后
__NSGlobalBlock__
没有访问auto变量
数据区
什么也不做,类型不改变
__NSStackBlock__
访问了auto变量
栈区
从栈复制到堆,类型改变为__NSMallocBlock__
__NSMallocBlock__
__NSStackBlock__调用了copy
堆区
引用计数+1,类型不改变
8.6 __block 修饰符
__block修饰符作用: block可以用于解决block内部无法修改auto变量值的问题 block不能修饰全局变量、静态变量static
__block修饰符原理: 编译器会将block变量包装成一个结构体Block_byref_age_0,结构体内部*forwarding是指向自身的指针,内部还存储着外部auto变量的值 block的forwarding指针如下图:

- 栈上,block结构体中的forwarding指针指向自己,一旦复制到堆上,栈上的block结构体中的forwarding指针会指向堆上的__block结构体
- 堆上block结构体中的forwarding还是指向自己。
8.7 block 的内存管理
- 当 Block 在栈上时,并不会对 __block 变量产生强引用;
- 当 Block 被 copy 到堆时,会调用 Block 内部的 copy 方法;copy 方法内部会调用 _Block_object_assign 函数;_Block_object_assign 函数会对 __block 变量形成强引用(retain)。
- 当 Block 从堆中移除时,会调用 Block 内部的 dispose 方法;dispose 方法内部会调用 _Block_object_dispose 函数;_Block_object_dispose 函数会自动释放引用的 __block 变量(release)

8.8 block 循环引用
某个类将 block 作为自己的属性变量,然后该类在 block 的方法体里面又使用了该类本身,如下:
1 | |
循环引用问题就是两个对象相互持有,导致两个对象无法被释放。
同理,Block 发生循环引用就是,Block 持有(强引用)对象,对象持有(强引用) Block,导致对象 A 在堆中将要被释放时,发现还持有 B 对象,同时 B 对象也持有 A 对象,僵持在那里,谁都无法被释放。如图:
8.9 怎么样解决block循环引用问题
a. 通过
__weak、__unsafe_unretained使 Block 对对象弱引用1)使用
__weak解决循环引用__weak修饰的对象释放后会自动置为 nil;__weak修饰的对象注册到 autoreleasepool 中;__weak只能在 ARC 模式下使用,也只能修饰对象,不能修饰基本数据类型。1
2
3
4
5__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"Hello Block, %p", weakSelf);
};
self.block();__weak对性能有一定影响,一个对象有大量__weak引用时候,当对象被废弃,要给所有__weak引用过它的对象赋 nil,消耗 CPU 资源。话虽如此,不过该用的时候就用。- 使用 __unsafe_unretained 解决循环引用
1
2
3
4
5__unsafe_unretained id weakSelf = self;
self.block = ^{
NSLog(@"Hello Block, %p", weakSelf);
};
self.block();__weak只支持 iOS 5.0+ 和 OS X Mountain Lion+ 作为部署版本。__unsafe_unretained兼容性更好一些。 虽然__unsafe_unretained和__weak都能防止对象持有,但是对于__weak,指针的对象在它指向的对象释放的时候回转换为 nil ,这是一种特别安全的行为;而__unsafe_unretained会继续指向对象存在的那个内存,即使是在它已经销毁之后。这会导致因为访问那个已释放对象引起的崩溃,当然相对而言,__unsafe_unretained对性能影响没那么大。b. 用
__block解决(必须要调用 Block)1
2
3
4
5
6__block id weakSelf = self;
self.block = ^{
NSLog(@"Hello Block, %p", weakSelf);
weakSelf = nil;
};
self.block();通过使用
__block,手动把弱引用的对象设置为 nil. Block 和 对象之间的关系变成:
使用弱引用会带来另一个问题,weakSelf 有可能会为 nil,如果多次调用 weakSelf 的方法,有可能在 block 执行过程中 weakSelf 变为 nil。因此需要在 block 中将 weakSelf “强化”
1 | |
__strong 这一句在执行的时候,如果 weakSelf 还没有变成 nil,那么就会 retain self,让 self 在 block 执行期间不会变为 nil。这样上面的 doSomething 和 doMoreThing 要么全执行成功,要么全失败,不会出现一个成功一个失败,即执行到中间 self 变成 nil 的情况。
通过用
__weak来修饰self变量来打破循环引用的原理
- 因为
__weak修饰的变量block不会持有它,执行拷贝操作引用计数也不会增加, 但是在block的实现内记得用__strong再修饰一遍self变量, 防止外面的变量提前释放或者被置空导致访问错误.- 原理也很简单:因为block的结构体会捕获
self,加上__weak修饰符就可以 不持有self变量, 也就不会造成循环引用.而__strong是加在block的实现里的, 当前变量出了block的作用域会自动失效, 也不会造成循环引用, 又可以保证代码的安全访问.
部分来源参考: 稀土掘金作者:iSwifter 链接:https://juejin.cn/post/6844903920322494478

