iOS底层高级面试题整理一(II)

iOS底层高级面试题整理【OC本质、KVC、KVO、Categroy、Block】(II)

6. Category

6.1 Category的实现原理

  • Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
1
2
3
4
5
6
7
8
typedef struct category_t {
const char *name; // 类的名字(名称)
classref_t cls; // 类(CLS)
struct method_list_t *instanceMethods; // 类中所有给类添加的实例方法的列表
struct method_list_t *classMethods; // 类中所有添加的类方法的列表
struct protocol_list_t *protocols; // category实现的所有协议的列表
struct property_list_t *instanceProperties; // category中添加的所有属性
} category_t;
  • 在程序运行的时候,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
7
typedef 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Revised new layout. */
struct Block_descriptor {
unsigned long int reserved; //预留内存大小
unsigned long int size; //块大小
void (*copy)(void *dst, void *src); //指向拷贝函数的函数指针
void (*dispose)(void *); //指向释放函数的函数指针
};

struct Block_layout {
void *isa; //指向Class对象
int flags; //状态标志位
int reserved; //预留内存大小
void (*invoke)(void *, ...); //指向块实现的函数指针
struct Block_descriptor *descriptor;
/* Imported variables. */
};

8.3 底层结构

LiPhvf.png Block 的类型都是继承于 NSBlock,最终是继承于 NSObject。 block底层结构就是__main_block_impl_0结构体,内部包含了impl结构体Desc结构体以及外部需要访问的变量,block将需要执行的代码放到一个函数里

  • impl内部的FuncPtr指向这个函数的地址,通过地址调用这个函数,就可以执行block里面的代码了

  • Desc用来描述block

  • 内部的reserved作保留

  • Block_size描述block占用内存

    8.4 block的变量捕获

    LiPCGH.png

  • 局部变量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指针如下图:

LiPKcL.png

  • 栈上,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)

LiPKcL.png

8.8 block 循环引用

某个类将 block 作为自己的属性变量,然后该类在 block 的方法体里面又使用了该类本身,如下:

1
2
3
self.someBlock = ^(Type var){
[self dosomething];
};

循环引用问题就是两个对象相互持有,导致两个对象无法被释放。 LiUJrG.png

同理,Block 发生循环引用就是,Block 持有(强引用)对象,对象持有(强引用) Block,导致对象 A 在堆中将要被释放时,发现还持有 B 对象,同时 B 对象也持有 A 对象,僵持在那里,谁都无法被释放。如图: LiUHdu.png

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 资源。话虽如此,不过该用的时候就用。

      1. 使用 __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 和 对象之间的关系变成:

    LiUsZ6.png

使用弱引用会带来另一个问题,weakSelf 有可能会为 nil,如果多次调用 weakSelf 的方法,有可能在 block 执行过程中 weakSelf 变为 nil。因此需要在 block 中将 weakSelf “强化”

1
2
3
4
5
6
7
8
__weak __typeof__(self) weakSelf = self;
NSBlockOperation *op = [[[NSBlockOperation alloc] init] autorelease];
[ op addExecutionBlock:^ {
__strong __typeof__(self) strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doMoreThing];
} ];
[someOperationQueue addOperation:op];

__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


iOS底层高级面试题整理一(II)
https://blog.qzl-coding.top/2022/04/19/iOS底层高级面试题整理一(II)/
作者
Long Chiu
发布于
2022年4月19日
许可协议