从底层了解面试题-Runtime篇

339 阅读46分钟

第一部分:结构模型

一.内存模型

OC中定义的对象是struct objc_object

struct objc_object {
private:
  isa_t isa;

public:
  Class ISA();
  Class rawISA();
  Class getIsa();
  
  uintptr_t isaBits() const;

  void initIsa(Class cls /*nonpointer=false*/);
  void initClassIsa(Class cls /*nonpointer=maybe*/);
  void initProtocolIsa(Class cls /*nonpointer=maybe*/);
  void initInstanceIsa(Class cls, bool hasCxxDtor);

  Class changeIsa(Class newCls);
....
}

1.私有变量isa,它是一个指针,指向的是什么?

  • 对象的isa指针指向类对象,类对象的 isa 指针指向元类对象,元类对象的 isa 指针指向根元类,根元类的 isa 指针指向本身
  • 根元类一般是 NSObject, 还有一个 NSProxy

2.id 的定义

typedef struct objc_object *id
所以id能指向任何对象

3.类的定义

类其实也是一个对象,被定义为 struct objc_class,继承自 objc_object

struct objc_class : objc_object {
    // Class ISA;
    // 父类类对象
    Class superclass;
    // 方法、指针缓存
    cache_t cache;       
    // 存储类的方法、属性、遵循的协议等信息   
    class_data_bits_t bits;    
    // 便捷方法用于返回其中的 class_rw_t * 指针
    class_rw_t *data() const {
        return bits.data();
    }

    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }

    void setInfo(uint32_t set) {
        ASSERT(isFuture()  ||  isRealized());
        data()->setFlags(set);
    }
....
}

3.1.cache_t cache

cache_t cache 是为了方法调用性能优化,将调用过的方法缓存,当对某个对象发送消息时先从 cache 中找,找不到再到isa指向的类中寻找方法实现,提高效率。 class_data_bits_t 内容很少,重点在包装的bits.data().

struct class_data_bits_t {
	// 友元类
    friend objc_class;
    // 掩码的形式保存 class_rw_t 指针和是否是 swift 类等一些标志位
    uintptr_t bits;
    ...
};

3.2.class_rw_t

class_rw_tbits.data() 返回的类型;可以动态添加修改方法、属性到 methods,properties

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t version;
    uint16_t witness;
    // 存储当前类在编译期就已经确定的属性、方法以及遵循的协议
    const class_ro_t *ro;
    // 方法列表
    method_array_t methods;
    // 属性列表
    property_array_t properties;
    // 遵循的协议列表
    protocol_array_t protocols;

    ...
}

3.3.class_ro_t

class_ro_t 存储的大多是类在编译时就已经确定的信息。

struct class_ro_t {
	// 通过掩码保存的一些标志位
    uint32_t flags;
    // 当父类大小发生变化时,调整子类的实例对象的大小
    uint32_t instanceStart;
    // 根据内存对齐计算成员变量从前到后所占用的内存大小,
    uint32_t instanceSize;
    // 仅在 64 为系统架构下的包含的保留位
#ifdef __LP64__
    uint32_t reserved;
#endif
	// 记录了哪些是 strong 的 ivar
    const uint8_t * ivarLayout;
    // 类名
    const char * name;
    // 实例方法列表
    method_list_t * baseMethodList;
    // 协议列表
    protocol_list_t * baseProtocols;
    // 成员变量列表
    const ivar_list_t * ivars;
    // 记录了哪些是 weak 的 ivar
    const uint8_t * weakIvarLayout;
    // 属性列表
    property_list_t *baseProperties;
...
}

二.类别

1.Category 底层实现

Category 底层其实就是一个 category_t 类型的结构体

// 定义在objc-runtime-new.h文件中
struct category_t {
    const char *name; // 比如给Student添加分类,name就是Student的类名
    classref_t cls;
    struct method_list_t *instanceMethods; // 分类的实例方法列表
    struct method_list_t *classMethods; // 分类的类方法列表
    struct protocol_list_t *protocols; // 分类的协议列表
    struct property_list_t *instanceProperties; // 分类的实例属性列表
    struct property_list_t *_classProperties; // 分类的类属性列表
};

  • 从这个结构体可以看出,Category 中存储的不仅有方法列表,还有协议列表和属性列表。
  • 我们每创建一个分类,在编译时都会生成这样一个结构体并将分类的方法列表等信息存入这个结构体。
  • 在编译阶段分类的相关信息和本类的相关信息是分开的。等到运行阶段,会通过 runtime 加载某个类的所有 Category 数据,把所有 Category 的方法、属性、协议数据分别合并到一个数组中,然后再将分类合并后的数据插入到本类的数据的前面。

当编译完成后:
假设我们创建了一个类(Student)和两个分类aaa,bbb.
2个分类aaa和bbb的信息分别存储在它们对应的结构体中的实例方法列表

aaa->instanceMethods = @[@"study",@"studentAaaTest"]
bbb->instanceMethods = @[@"study",@"studentBbbTest"]

而此时 Student 类对象的方法列表是存在 class_ro_t 结构体的 baseMethodList 中,所以在编译阶段各个方法列表都是分开存储的。

等到运行阶段:
Student 类对象会初始化 class_rw_t 结构体,这个结构体中也有个方法列表 methods ,它是一个二维数组,它初始化后首先将 class_ro_t 中的 baseMethodList 拷贝过来,此时 methods = @[baseMethodList].

然后通过 runtime 加载aaa和bbb这两个分类的数据并将它们的方法列表进行合并;

它们在合并后的数组(我这里给它取个名字叫 categoryMethodList )中的顺序是和他们参与编译的顺序有关的,如果先编译aaa,再编译bbb,那么在合并后的数组中,bbb的方法列表在前面,aaa的方法列表在后面所以此时
categoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods]。

然后再将 categoryMethodList 的数据添加到 methods 中。添加之前 methods 的容量大小是1,它会先根据 categoryMethodList 中方法列表的个数(也就是有几个分类,这里是2个分类)进行扩容,methods 扩容后的大小是3,它先将 methods 中原来的数据(baseMethodList)移到最后,然后再将 categoryMethodList 中的数据插入进来,所以最后的结果就是
methods = @[bbb->instanceMethods,aaa->instanceMethods,baseMethodList]。

这样就完成了分类方法列表和本类方法列表的合并。

  • 合并后分类的方法在前面(最后参与编译的那个分类的方法列表在最前面),本类的方法列表在最后面。
  • 当分类中有和本类同名的方法时,调用这个方法执行的就是分类中的方法。
  • 从这个现象来看,就好像本类的方法被分类中同名的方法给覆盖了,实际上并没有覆盖,只是调用方法时最先查找到了分类的方法所以就执行分类的方法。

2.Category如何给类扩展属性

我们从 Category 的底层结构体 category_t 可以看出,这个结构体中有方法列表、协议列表和属性列表,但是没有成员变量列表,所以我们可以在 Category 中定义属性,但是不能定义成员变量,定义成员变量的话编译器会直接报错。

可以通过通过关联对象来扩展属性

2.1.添加关联对象

void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy);

比如 Student 的分类中新增了一个 name 属性,我要给一个实例对象的 stuname 属性赋值为 Jack:

  • 第一个参数(object):关联的对象,也就是上面的stu
  • 第二个参数(key):这里传入一个void * 类型的指针作为key,这个key是自己随便设置的,后面获取关联对象也是根据这个key来获取的。
  • 第三个参数(value):要设置的属性值,也就是上面的Jack
  • 第四个参数(policy):要设置的属性的修饰类型,比如name的修饰类型是strong, nonatomic,那这里对应的 policy 就是 OBJC_ASSOCIATION_RETAIN_NONATOMIC。 具体对应关系如下(注意是没有和 weak 对应的 policy 的):
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

2.2.获取关联对象

id objc_getAssociatedObject(id object, const void * key);

2.3.移除所有的关联对象

void objc_removeAssociatedObjects(id object);

3.关联对象存储原理

关联对象是另外单独存储的,底层实现关联对象技术的核心对象有4个

  1. ObjcAssociation:这个对象里面有2个成员 uintptr_t _policyid _value,这两个很显然就是我们设置关联对象传入的参数 policyvalue
  2. ObjectAssociationMap:这是一个 HashMap (以键值对方式存储,可以理解为是一个字典),以设置关联对象时传入的 key 值作为 HashMap 的键,以 ObjcAssociation 对象作为 HashMap 的值。比如一个分类添加了3个属性,那一个实例对象给这3个属性都赋值了,那么这个 HashMap 中就有3个元素,如果给这个实例对象的其中一个属性赋值为 nil,那这个 HashMap 就会把这个属性对应的键值对给移除,然后 HashMap 中就还剩2个元素
  3. AssociationsHashMap:这也是一个 HashMap ,以设置关联属性时传入的参数 object 作为键(实际是对 object 对象通过某个算法计算出一个值作为键)。以 ObjectAssociationMap 作为值。所以当某个类(前提是这个类的分类中有设置关联对象)每实例化一个对象,这个 HashMap 就会新增一个元素,当某个实例化对象被释放时,其对应的键值对也会被这个 HashMap 给移除。注意整个程序运行期间, AssociationsHashMap 只会有一个,也就是说所有的类的关联对象信息都是存储在这个 HashMap 中。
  4. AssociationsManager:从名字就可以看出它是一个管理者,注意整个程序运行期间它也只有一个,他就只包含一个 AssociationsHashMap

三.面试题

1.介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)

OC中的对象指向的是一个 objc_object 指针类型(typedef struct objc_object *id);
从它的结构体中可以看出,它包括一个 isa 指针,指向的是这个对象的类对象,一个对象实例通过这个 isa 找到它自己的 Class,而这个 Class 中存储的就是这也实例的方法列表、属性列表、成员变量列表等相关信息;
类在 OC 中用 Class 表示,实际上它指向的是一个 objc_class 的指针类型(typedef struct objc_class *Class),内部包括:

  • isa指针(指向 meta_class )
  • 父类类对象(superclass)
  • 方法和指针的缓存(cache)
  • 存储类的方法、属性、遵循的协议等信息的bits
  • 返回 class_rw_t * 指针的data

class_rw_t 可以动态添加修改方法、属性到 methods,properties 中; 内部包括:

  • 存储当前类在编译期就已经确定的属性、方法以及遵循的协议(const class_ro_t *ro
  • 方法列表: method_array_t methods;
  • 属性列表: property_array_t properties;
  • 遵循的协议列表: protocol_array_t protocols;

class_ro_t 内部包括:

  • 类名: const char * name;
  • 实例方法列表: method_list_t * baseMethodList;
  • 协议列表: protocol_list_t * baseProtocols;
  • 成员变量列表: const ivar_list_t * ivars;
  • 属性列表: property_list_t *baseProperties;
  • strong ivar: const uint8_t * ivarLayout;
  • weak ivar: const uint8_t * weakIvarLayout;
  • 当父类大小发生变化时,调整子类的实例对象的大小:instanceStart
  • 根据内存对齐计算成员变量从前到后所占用的内存大小:instanceSize

2.为什么要设计 metaclass

  1. 进入 _objc_msgSend 后首先判断消息的接受者是否为 nil 或者是否使用了tagPointer 技术
  2. 根据消息接受者的 isa 指针找到 metaclass(因为类方法存在元类中。如果调用的是实例方法,isa指针指向的是类对象。)
  3. 进入 CacheLookup 流程,这一步会去寻找方法缓存,如果缓存命中则直接调用 TailCallCachedImp 验证方法 IMP 的有效性并调用改方法的实现,如果缓存不存在则进入 objc_msgSend_uncached 流程
  4. 最后调用 _class_lookupMethodAndLoadCache3 ,该方法会去调用 lookUpImpOrForward ,步骤如下:
    (1)首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走。
    (2)从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
    (3)从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
    (4)如果到基类依然没有找到该方法则触发动态方法解析流程。
    (5)还是找不到就触发消息转发流程。

2.1.这跟元类的存在有啥关系?我们都知道类方法是存储在元类中的,那么可不可以把元类干掉,在类中把实例方法和类方法存在两个不同的数组中?

行是肯定可行的
但是在 lookUpImpOrForward 执行的时候就得标注上传入的 cls 到底是实例对象还是类对象,这也就意味着在查找方法的缓存时同样也需要判断 cls 到底是个啥。

倘若该类存在同名的类方法和实例方法是该调用哪个方法呢?

  • 这也就意味着还得给传入的方法带上是类方法还是实例方法的标识,SEL并没有带上当前方法的类型(实例方法还是类方法),参数又多加一个;
  • 而我们现在的 objc_msgSend() 只接收了(id self, SEL _cmd, ...)这三种参数,第一个 self 就是消息的接收者,第二个就是方法,后续的...就是各式各样的参数
  • 通过元类就可以巧妙的解决上述的问题,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,完美的符合6大设计原则中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。

元类的存在巧妙的简化了实例方法和类方法的调用流程,大大提升了消息发送的效率。

3.class_copyPropertyListclass_copyIvarList 的区别

  • class_copyPropertyList:仅仅是对象类的属性@property申明的属性
  • class_copyIvarList: 所有属性和变量(包括在@interface大括号中声明的变量)

class_copyIvarList

  • 它返回的是一个 Ivar 的数组,这个数组里面包含了你要查看类的所有实例变量,但是不包括从父类继承过来的。
  • 如果你传入的类没有实例变量或者该 classNil ,那么该方法返回的就是 NULLcount 值也就变成了0。
  • 有一点需要注意:你必须使用 free() 方法将该数组释放。
  • 然后就是通过for循环遍历,通过 ivar _ getName 拿到 ivarName

4.有时通过 class_copyIvarList 获取的变量并不完全,这是为什么?

属性被 @dynamic 修饰了,这个关键字的作用就是告诉编译器属性的setter/getter需要用户自己实现,不自动生成,而且也不会产生 _var 变量

5.class_ro_t 和 class_rw_t 的区别?

  1. 每个类都对应有 一个 class_ro_t 结构体和一个 class_rw_t 结构体。
  2. 在编译期间,class_ro_t 结构体就已经确定,objc_classbitsdata 部分存放着该结构体的地址。
  3. runtime 运行之后,具体说来是在运行 runtimerealizeClass 方法时,会生成 class_rw_t 结构体,该结构体包含了 class_ro_t,并且更新 data 部分,换成 class_rw_t 结构体的地址。

两个结构体都存放着当前类的属性、实例变量、方法、协议等等;区别在于:

  • class_ro_t 存放的是编译期间就确定的
  • class_rw_t 是在 runtime 时才确定,它会先将 class_ro_t 的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。
  • 所以可以说 class_rw_tclass_ro_t 的超集,当然实际访问类的方法、属性等也都是访问的 class_rw_t 中的内容

6.category 如何被加载的?两个 categoryload 方法的加载顺序?两个 category 的同名方法的加载顺序?

  • category 的加载是在运行时发生的,加载过程是,把 category 的实例方法、属性、协议添加到类对象上。把 category 的类方法、属性、协议添加到 metaclass 上。
  • categoryload 方法执行顺序是根据类的编译顺序决定的,即:xcode中的Build Phases中的Compile Sources中的文件从上到下的顺序加载的。
  • category 并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且 category 添加的methodA会排在原有类的methodA的前面,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category 里的对应方法。

7.+load 方法

  • +load 方法会在 runtime 加载类,分类时调用
  • 每个类,分类的 +load,在程序运行过程中只调用一次

调用顺序
1.先调用类的+load

  • 按照编译先后顺序调用(先编译,先调用)
    
  • 调用子类的+load之前会先调用父类的+load
    

2.再调用分类的+load

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

8. +initialize 方法

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

调用顺序
先调用父类的+initialize,再调用子类的+initialize (先初始化父类,再初始化子类,每个类只会初始化1次)

+initialize+load的很大区别是 +initialize是通过objc_msgSend进行调用的,所以有以下特点

  • 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
  • 如果分类实现了+initialize,就覆盖类本身的+initialize调用

9.+load+initialize异同点和使用场景

区别

1.调用时机

  • +load:在runtime加载类、分类时调用,在main函数之前调用(只会调用一次)
    
  • +initialize:在类第一次接收到消息时调用,惰性调用,每个类只会initialize一次(父类的initialize方法可能会被调用多次)
    

2.调用方式

  • +load:根据方法地址调用
    
  • +initialize:objc_msgSend
    

3.调用顺序

  • +load:1.父类 ->-> 子类 -> 分类 2.按照编译先后顺序调用(先编译,先调用)
    
  • +initialize:分类 -> 父类 ->-> 子类 2.如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次(只初始化化一次))
    

4.方法是否会覆盖调用

  • +load:不会
    
  • +initialize:会
    

5.调用次数

  • +load:一次
    
  • +initialize:可能多次(父类实现里被调用多次)
    

相同点

  1. load和initialize会被自动调用,不能手动调用它们。
  2. 子类实现了load和initialize的话,会隐式调用父类的load和initialize方法。
  3. load和initialize方法内部使用了锁,因此它们是线程安全的。

使用场景

  • +load:一般是用来交换方法Method Swizzle,由于它是线程安全的,而且一定会调用且只会调用一次,通常在使用UrlRouter的时候注册类的时候也在+load方法中注册。
  • +initialize:主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作。

注意点
+initialize方法会被自动继承,所以,+initialize的出错率要比+load更大一些。 如何避免

1)判断类
+ (void)initialize{
    if (self == [MyClass class]) {
          ....
    }
}

(2dispatch_once,一次性代码
+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
            ...
    });
}

+load在main函数之前执行,如果+load函数里面任务过重,会影响应用的启动速度

10.Category 的使用场合是什么?

  1. 可以减少单个类的体积,降低耦合性,同一个类可以多人进行开发
  2. 可以为系统类添加分类进行拓展
  3. 模拟多继承
  4. 把静态库的私有方法公开

11.CategoryClass Extension 的区别是什么?能给 NSObject 添加 Extension 吗,结果如何?

  1. Category 是运行时决定生效的,Extension 是编译时就决定生效的
  2. Category 可以为系统类添加分类,Extension 不能
  3. Category 是有声明和实现,Extension 直接写在宿主.m文件,只有声明
  4. Category 只能扩充方法,不能扩充成员变量和属性

不能给 NSObject 添加 Extension ,因为 NSObject 不是开源的,无法找到.m文件

12.怎么保证在+load方法里交换方法总是在别人交换完之后执行的(交换方法的实现总是以我们交换的为准)

  1. 交换方法名称要带前缀,不与他人的方法名一样,这样所有的交换方法都会执行
  2. 如果方法名一样,则只会执行后添加的方法;如果要保证执行自己的,则需要将该文件移到文件列表的最下面

13.Objective-c 方法调用流程

OC是动态语言,每个方法在运行时会被动态转为消息发送

objc_msgSend(receiver,selector))
id objc_msgSend ( id receiver, SEL op, ... ); 
  • 参数 receiver : 消息接收者,如果OC代码中的调用为[ObjectA test],这个 receiver 就是 ObjectA
  • 参数 op :方法名
  • 参数 :不定参数
  1. OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类
  2. 然后在该类的cache中查询,如果找到了,就直接返回;如果没找到,就去methodList中查找。如果找到了,则将方法的IMP(方法实现的指针)返回,并将IMP存入Cache; 如果还没找到,就通过super_class找到父类,在父类的methodList中查找;
  3. 如果在最顶层的父类(一般也就NSObject)中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常(unrecognized selector send to XXX)

抛出异常前会有三次补救机会

  1. 动态方法解析过程中的:对象方法动态解析(+(BOOL)resolveInstanceMethod:(SEL)sel)和 类方法动态解析(+(BOOL)resolveClassMethod:(SEL)sel
  2. 如果动态解析失败,则会进入消息转发流程,消息转发又分为:快速转发和慢速转发两种方式。
  3. 快速转发的实现是 forwardingTargetForSelector,让其他能响应要查找消息的对象来干活。
  4. 慢速转发的实现是 methodSignatureForSelectorforwardInvocation 的结合,提供了更细粒度的控制,先返回方法签名给 Runtime,然后让 anInvocation 来把消息发送给提供的对象,最后由 Runtime 提取结果然后传递给原始的消息发送者。
  5. 如果在3次挽救机会都没有处理时,就会报unrecognized selector sent to XXX异常。此时程序会崩溃。

关于resolveInstanceMethod 方法又称为对象方法动态解析,它的流程大致如下:\

  1. 检查是否实现了 +(BOOL)resolveInstanceMethod:(SEL)sel 类方法,如果没有实现则直接返回(通过 cls->ISA() 拿到元类,因为类方法是存储在元类上的对象方法)

  2. 如果当前实现了 +(BOOL)resolveInstanceMethod:(SEL)sel 类方法,则通过 objc_msgSend 手动调用该类方法。

    1. 创建一个IMP方法指针 
    IMP imp = class_getMethodImplementation([self class], @selector(@"intercept"))
    2. 将IMP加入到类方法列表中
    // "v@:"解析:v表示返回值类型void,@表示方法名,:表示参数
    class_addMethod([self class], sel, imp, "v@:");
    
  3. 完成调用后,再次查询 cls 中的 imp

  4. 如果 imp 找到了,则输出动态解析对象方法成功的日志。

  5. 如果 imp 没有找到,会沿着 isa 指针,去调用 _class_resolveClassMethod 走类方法动态解析流程:

    1. 判断是否是元类,如果不是,直接退出。
    2. 检查是否实现了 +(BOOL)resolveClassMethod:(SEL)sel 类方法,如果没有实现则直接返回(通过 cls- 是因为当前 cls 就是元类,因为类方法是存储在元类上的对象方法)
    3. 如果当前实现了 +(BOOL)resolveClassMethod:(SEL)sel 类方法,则通过 objc_msgSend 手动调用该类方法,注意这里和动态解析对象方法不同,这里需要通过元类和对象来找到类,也就是 _class_getNonMetaClass
      1.创建一个IMP方法指针
      IMP imp = class_getMethodImplementation([UIViewController class], @selector(@"intercept"));
      2.将IMP加入到元类方法列表中
      class_addMethod(objc_getMetaClass(object_getClassName(self)), sel, imp, "v@:");
      
  6. 完成调用后,再次查询 cls 中的 imp

  7. 如果 imp 找到了,则输出动态解析对象方法成功的日志。

  8. 如果 imp 没有找到,则进入快速消息转发

关于forwardingTargetForSelector方法 又称为快速消息转发:

  1. forwardingTargetForSelector 是一种快速的消息转发流程,它直接让其他对象来响应未知的消息。
  2. forwardingTargetForSelector 不能返回 self,否则会陷入死循环,因为返回 self 又回去当前实例对象身上走一遍消息查找流程,显然又会来到 forwardingTargetForSelector
  3. forwardingTargetForSelector 适用于消息转发给其他能响应未知消息的对象,也就是最终返回的内容必须和要查找的消息的参数和返回值一致,如果想要不一致,就需要走其他的流程。

关于 forwardInvocation 对应慢速消息转发 methodSignatureForSelector 方法签名:

  1. 重写 methodSignatureForSelector:方法,该方法返回一个 NSMethodSIgnature 对象,该对象包含了给定选择器所标识方法的描述。主要包含返回值的信息和参数信息.
  2. 实现 forwardInvocation: 方法时,若发现调用的message不是由本类处理,则续调用超类的同名方法。这样所有父类均有机会处理此消息,直到NSObject。如果最后调用了NSObject的方法,那么该方法就会调用doesNotRecognizerSelector:,抛出异常,标明选择器最终未能得到处理。

注意:

  1. 调用 methodSignatureForSelector: 方法,尝试获得一个方法签名。如果获取不到,则直接调用 doesNotRecognizeSelector 抛出异常。如果能获取,则返回非nil;传给一个 NSInvocation 并传给 forwardInvocation:
  2. 调用 forwardInvocation: 方法,将第三步获取到的方法签名包装成Invocation传入,如何处理就在这里面了,并返回非nil。
  3. 调用 doesNotRecognizeSelector:,默认的实现是抛出异常。如果第三步没能获得一个方法签名,执行该步骤

14.Runtime 中,SEL、MethodIMP 有什么区别,使用场景?

一个类(Class)持有一个分发表,在运行期分发消息,表中的每一个实体代表一个方法(Method),它的名字叫做选择子(SEL),对应着一种方法实现(IMP)

SEL
定义: typedef struct objc_selector *SEL,代表方法的名称。
仅以名字来识别。翻译成中文叫做选择子或者选择器,选择子代表方法在 Runtime 期间的标识符。为 SEL 类型,虽然 SELobjc_selector 结构体指针,但实际上它只是一个 C 字符串
在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-CRuntime 运行系统。不论两个类是否存在依存关系,只要他们拥有相同的方法名,那么他们的 SEL 都是相同的。比如,有n个viewcontroller页面,每个页面都有一个viewdidload,每个页面的载入,肯定都是不尽相同的。但是我们可以通过打印,观察发现,这些viewdidload的SEL都是同一个。因此类方法定义时,尽量不要用相同的名字,就算是变量类型不同也不行,否则会引起重复。

IMP
定义:typedef id (*IMP)(id, SEL, ...),代表函数指针,即函数执行的入口。
该函数使用标准的 C 调用。第一个参数指向 self(它代表当前类实例的地址,如果是类则指向的是它的元类),作为消息的接受者;第二个参数代表方法的选择子;... 代表可选参数,前面的 id 代表返回值。

Method
定义:typedef struct objc_method *Method,Method对开发者来说是一种不透明的类型,被隐藏在我们平时书写的类或对象的方法背后。
它是一个objc_method结构体指针,我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

objc_method 的定义为:

struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
 };
  • 方法名 method_name 类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型 method_types 是个 char 指针,其实存储着方法的参数类型和返回值类型,即是 Type Encoding 编码。
  • method_imp 指向方法的实现,本质上是一个函数的指针,就是前面讲到的 Implementation

第二部分:内存管理

一.内存布局

从低到高分别为

  1. 代码区:编译之后的二进制代码
  2. 数据区:存放字符串常量,全局变量,静态变量
  3. 堆区(heap):通过alloc,malloc,calloc等关键字动态分配的空间
  4. 栈区(stack):存放局部变量,函数调用开销

二.OC对象的内存管理

1.OC对象的一些特殊类型的内存管理(Tagged Pointer

从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等;NSNumber指针存储的是堆中NSNumber对象的地址值

使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

objc_msgSend能识别Tagged Pointer,比如NSNumberintValue方法,直接从指针提取数据,节省了以前的调用开销

如果要是动态分配内存,由于OC对象都有isa指针,所以最少分配16个字节,换算成十六进制后末位都是0,由此可以推断使用了Tagged Pointer的内存地址末位不为0

是否使用了TaggedPointer

可以通过和一个掩码_OBJC_TAG_MASK进行按位与运算来判断

objc_object::isTaggedPointer() {
    return _objc_isTaggedPointer(this);
}

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr){
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

2.其他对象内存管理

在iOS中,使用引用计数来管理OC对象的内存

2.1.引用计数的原则

  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
  • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
  • 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1

2.2.isa指针

从arm64架构开始,苹果对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。

define ISA_BITFIELD                                                     
      uintptr_t nonpointer        : 1;   //指针是否优化过                                  
      uintptr_t has_assoc         : 1;   //是否有设置过关联对象,如果没有,释放时会更快                                  
      uintptr_t has_cxx_dtor      : 1; 	 //是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快                                     
      uintptr_t shiftcls          : 33; //存储着Class、Meta-Class对象的内存地址信息 \
      uintptr_t magic             : 6;  //用于在调试时分辨对象是否未完成初始化                                     
      uintptr_t weakly_referenced : 1;  //是否有被弱引用指向过,如果没有,释放时会更快                                     
      uintptr_t deallocating      : 1;  //对象是否正在释放                                     
      uintptr_t has_sidetable_rc  : 1;  //引用计数器是否过大无法存储在isa中                                     
      uintptr_t extra_rc          : 19  //里面存储的值是引用计数器减1

2.3.isa中不同的位域代表不同的含义

1.nonpointer
  • 0:代表普通的指针,存储着Class、Meta-Class对象的内存地址
  • 1:代表优化过,使用位域存储更多的信息
2.has_assoc

是否有设置过关联对象,如果没有,释放时会更快

3.has_cxx_dtor

是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快

4.shiftcls

存储着Class、Meta-Class对象的内存地址信息

5.magic

用于在调试时分辨对象是否未完成初始化

6.weakly_referenced

是否有被弱引用指向过,如果没有,释放时会更快

deallocating

对象是否正在释放

7.extra_rc

里面存储的值是引用计数器减1

8.has_sidetable_rc

引用计数器是否过大无法存储在isa中 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中

三.SideTable底层分析

1.retain操作

新版本的 objc 中引入了 Tagged Pointer,且 isa 采用 union 的方式进行构造,其中 isa 的结构体中有一个 extra_rchas_sidetable_rc,这两者共同记录引用计数器

inline id 
objc_object::retain(){
    ASSERT(!isTaggedPointer());
    return rootRetain(false, RRVariant::FastOrMsgSend);
}

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant){
    if (slowpath(isTaggedPointer())) return (id)this;
    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    isa_t oldisa;
    isa_t newisa;
    oldisa = LoadExclusive(&isa.bits);
    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_retain()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())){
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return (id)this;
        }
    }

    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        // 如果不是nonpointer,直接操作散列表+1
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        
        uintptr_t carry;
        // 执行引用计数加1操作
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        // 判断extra_rc是否满了,carry是标识符
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            // 如果extra_rc满了,则拿出一半存储到side table散列表中
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }

        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }

    return (id)this;
}

关键方法 sidetable_addExtraRC_nolock():

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc){
    assert(isa.nonpointer);
    // 取出this对象所在的SideTable
    SideTable& table = SideTables()[this];
    // 取出SideTable中存储的refcnts,类型为Map
    size_t& refcntStorage = table.refcnts[this];
    // 记录原始的引用计数器
    size_t oldRefcnt = refcntStorage;

    // 容错处理
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
        // SideTable溢出处理
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    } else {
        // SideTable未溢出
        refcntStorage = newRefcnt;
        return false;
    }
}

这个函数的逻辑如下:

  1. 根据 this,也就是对象的地址从 SideTables 中取出一个 SideTable
  2. 获取 SideTablerefcnts,这个成员变量是一个 Map
  3. 存储旧的引用计数器;
  4. 进行 add 计算,并记录是否有溢出;
  5. 根据是否溢出计算并记录结果,最后返回;

那么,这里有几个点需要解开:

  1. 什么是 SideTables
  2. 什么是 SideTable
  3. 什么是 refcnts
  4. add 的计算逻辑为什么需要位移?
  5. SideTable 中的溢出时如何处理的?

2.SideTables

static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
  • SideTables() 使用 static 修饰,是一个静态函数,返回 StripedMap<SideTable> 类型
  • & 表示返回引用类型;
  • reinterpret_cast 是一个强制类型转换符号;
  • 函数最终的结果就是返回 SideTableBuf

3.deTableBuf

alignas(StripedMap<SideTable>) static uint8_t 
    SideTableBuf[sizeof(StripedMap<SideTable>)];
  • alignas 表示对齐;
  • StripedMap<SideTable>size 为 4096(存疑,待验证);
  • uint8_t 实际上是 unsigned char 类型,即占 1 个字节;

由此可以得出:

SideTableBuf 本质上是一个长度为 sizeof(StripedMap<SideTable>)char 类型的数组;

同时也可以这么理解: SideTableBuf 本质上就是一个大小为和 StripedMap<SideTable> 对象一致的内存块;

这也是为什么 SideTableBuf 可以用来表示 StripedMap<SideTable> 对象。本质上而言,SideTableBuf 就是指一个 StripedMap<SideTable>对象;

SideTablesC++initializers 函数之前被调用,所以不能使用 C++ 初始化函数来初始化 SideTables,而 SideTables 本质就是 SideTableBuf, 不能使用全局指针来指向这个结构体,因为涉及到重定向问题

StripedMap<SideTable>删减后代码

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }
    ...省略了对象方法...
}

上述代码的逻辑为:

  • 根据是否为 iphone 定义了一个 StripeCountiphone 下为 8;
  • 源码中 CacheLineSize 为 64,使用 T 定义了一个结构体,而 T 就是 SideTable 类型;
  • 生成了一个长度为 8 类型为 SideTable 的数组;
  • indexForPointer() 逻辑为根据传入的指针,经过一定的算法,计算出一个存储该指针的位置,因为使用了取模运算,所以值的范围是 0 ~ (StripeCount-1),所以不会出现数组越界;
  • 后面的 operator 表示重写了运算符 [] 的逻辑,调用了 indexForPointer() 方法,这样使用起来更像一个数组;
  • 至此,SideTables 的含义已经很清楚了,可以理解成一个类型为 StripedMap<SideTable> 静态全局对象,内部以数组的形式存储了 StripeCountSideTable

4.SideTable

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }
    ...省略对象方法...
}

SideTable 有三个成员变量:

  • spinlock_t:自旋锁,负责加锁相关逻辑;
  • refcnts:存储引用计数器的Map(散列表);
  • weak_table:存储弱引用的表

refcnts 的定义

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;

DenseMap 就是一个 hash Map,过于复杂,先不看。

来看看基类 DenseMapBase 中的部分代码,如下,DenseMapBase 中重写了操作符 []:

ValueT &operator[](const KeyT &Key) {
    return FindAndConstruct(Key).second;
}

大意是通过传入的 Key 寻找对应的 Value

KeyDisguisedPtr<objc_object> 类型,Valuesize_t 类型

使用 obj.address :refCount 的形式来记录引用计数器 回到最初的 sidetable_addExtraRC_nolock 方法中:

size_t& refcntStorage = table.refcnts[this];

上述代码就是通过 thisobject对象的地址)取出 refcnts 这个哈希表中存储的引用计数器;

refcnts 可以理解成一个 Map,使用 address:refcount 的形式存储了很多个对象的引用计数器;

5.引用计数器原理总结

  • iphoneSideTables() 本质是返回一个 SideTableBuf 对象,该对象存储 8 个 SideTable
  • 因为涉及到多线程和效率的问题,必定不可能只使用一个 SideTable 来存储对象相关的引用计数器和弱引用;
  • Apple 通过对 object 的地址进行运算之后,对 SideTable 的个数进行取模运算,以此来决定将对象分配到哪个 SideTable 进行信息存储,因为有取模运算,不会出现数组溢出的情况;

objc 中当对象需要使用到 sideTable 时,会被分配到 8/64 个全局 sideTables 中的某一个表中存储相关的引用计数器或者弱引用信息;

6.weak_table

从上文中可以看出,8/64 个 SideTable 对象中不仅保存了引用计数器相关的 Map,还保存了一个 weak_table

// weak_table_t 是一个全局引用表,object 的地址作为 key,weak_entry_t 作为 Value。只不过这个全局引用表有 8 或者 64 个;
struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

weak_table 中以 weak_entry_t 的形式存储对象的弱引用,weak_table_t 中使用数组的形式来存储 weak_entry_t 对象,以此来表示该表中每个对象的弱引用情况

6.1.store_weak 流程分析

要弄清楚 weak_table_tweak_entry_t 的使用,就要从新增弱引用作为突破口,来看看 objc_storeWeak() 方法:

id
objc_storeWeak(id *location, id newObj){
    return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object *)newObj);
}

storeWeak 的定义
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj){
...省略...
}

haveOldhaveNew 是作为参数来使用的

精简 storeWeak() 函数的代码

SideTable *oldTable;
SideTable *newTable;

// 根据参数判断是否存在旧表决定使用哪个表进行存储
if (haveOld) {
    oldObj = *location;
    oldTable = &SideTables()[oldObj];
} else {
    oldTable = nil;
}
if (haveNew) {
    newTable = &SideTables()[newObj];
} else {
    newTable = nil;
}

...省略很多异常场景处理代码...

// 只看 new 的逻辑
if (haveNew) {
    // 在weak_table中新增弱引用
    // 如果失败则会返回 nil,成功则返回对象本身
    newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);

    if (newObj  &&  !newObj->isTaggedPointer()) {
        // 成功则设置weakly_referenced为1;
        newObj->setWeaklyReferenced_nolock();
    }

    // 赋值
    *location = (id)newObj;
}

location 是作为入参传递进来的,是被 __weak 修饰的指针本身,而 newObj 就是这个弱指针所指向的对象;伪代码:__weak NSObject * location = newObj;

梳理下 storeWeak() 的逻辑:

  1. 根据 haveOld/haveNew 调用 SideTables() 方法,获取到 8/64 个全局 SideTable 中的某一个;
  2. 调用 weak_register_no_lock()方法将 newObj 添加到 SideTableweak_table 中,如果失败则会返回 nil,成功则返回对象本身;
  3. 调用 setWeaklyReferenced_nolock() 方法,设置 isaweakly_referenced 为 1;
  4. location 正式指向 newObj 进行赋值,但是注意此时并没有调用 retain 方法,所以引用计数器不会 + 1;

weak_register_no_lock() 方法

精简代码
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating){
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    ...省略异常场景处理代码...

    // now remember it and where it is being stored
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        // 存在 weak_entry_t 对象则直接新增
        append_referrer(entry, referrer);
    }  else {
        // 不存在则证明是第一次被弱引用,新建一个weak_entry_t对象
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }
    return referent_id;
}

代码逻辑

  1. 存在 weak_entry_t 则证明该对象存在其他的弱引用,直接在原来的 weak_entry_t 最后新增一个 new_referrer
  2. 不存在 weak_entry_t 则证明该对象是第一次被弱引用,新增一个 weak_entry_t 后插入;

6.2.SideTables 的初始化时机和流程

SideTables 也就是 SideTableBuf, 是在 SideTableInit() 方法中初始化

static void SideTableInit() {
    new (SideTableBuf) StripedMap<SideTable>();
}

6.3.SideTableInit 的调用顺序

map_images() -> map_images_nolock() -> arr_init() -> SideTableInit()

初始化关键:map_images()

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_objc_init() 调用 dyldApi 注册通知并绑定了三个函数:

  • map_images:印射到内存中的回调;
  • load_images:加载时的回调;
  • unmap_image:从内存中移除时的回调;

_dyld_objc_notify_register 最终调用 registerObjCNotifiers 函数,dyld 中的源码如下:

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped){
    // record functions to call
    sNotifyObjCMapped   = mapped;
    sNotifyObjCInit     = init;
    sNotifyObjCUnmapped = unmapped;

    // call 'mapped' function with all images mapped so far
    try {
        notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
    }
    catch (const char* msg) {
        // ignore request to abort during registration
    }
}

mapped 的回调最终被赋值给了 sNotifyObjCMapped,而该函数的调用只存在于 notifyBatchPartial()中,且 statedyld_image_state_bound;回调赋值之后就立马尝试了一次 notifyBatchPartial() 的调用。

逻辑梳理

  1. map_images 函数在第一次注册 dyld 监听时被调用,会将所有具有 objc sectionimage 进行回传;
  2. 如果有新的 objc 相关的 image 被印射到内存,也会触发 map_images 的调用;
  3. SideTables 在第一次处理包含 objc sectionimage 时被初始化(只会被初始化一次,具体参见源码);
  4. arr_init 只调用一次,所以这个 SideTables 是整个生命周期只会生成一次,记录着所有对象的引用计数器和弱引用关系。这也是为什么注释中写道不能析构的原因;

7.SideTable总结

SideTable.jpg

四.问题

为什么 weak 能够自动置为 nil?(被 __weak 修饰的对象在被析构之后,弱指针为何会被置为 nil?而 assign 修饰的指针则仍然存储原来的内存地址)

应该从对象的析构开始研究:

inline void
objc_object::rootDealloc(){
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

如果开启了指针优化、没有弱引用、没有关联对象、没有 c++ 析构函数、引用计数器未存储到 sidetable 中,则直接 free(this),否则进入object_dispose()

很显然,我们要寻找的逻辑肯定不符合上述的条件,继续用看看这个函数的代码:

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}

objc_destructInstance 函数中做了很多处理,比如 c++ 析构函数的处理、关联对象的处理等,暂时不关心这些逻辑,只关心弱引用逻辑,顺着代码最终进入到这个函数:

inline void 
objc_object::clearDeallocating(){
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

很显然 isa.weakly_referenced == 1,我们要的逻辑在 clearDeallocating_slow 中,最终进入到 weak_clear_no_lock 函数,在这里我们找到了答案:

for (size_t i = 0; i < count; i ++) {
	objc_object **referrer = referrers[i];
	if (referrer) {
		if (*referrer == referent) {
			*referrer = nil;
		} else if (*referrer) {
			...
		}
	}
}

弱引用标志为 1 的对象在析构时,会遍历 weak_table 中的 referrers 数组并将指针置为 nil。该数组正是存储了哪些指针对该对象进行了弱引用。

五.面试题

1.weak的实现原理?SideTable的结构是什么样的?

weak:其实是一个hash表结构,其中的key是所指对象的地址,value是weak的指针数组,weak表示的是弱引用,不会对对象引用计数+1,当引用的对象被释放的时候,其值被自动设置为nil,一般用于解决循环引用的。

weak的实现原理

  1. 初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak 指针指向对象的地址。
  2. 添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
  3. 释放时,调用 clearDeallocating 函数。clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entryweak 表中删除,最后清理对象的记录。

SideTable的结构

struct SideTable {
// 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;

详细介绍

2.关联对象的应用?系统如何实现关联对象的?

应用:

  1. 由于不改变原类的实现,所以可以给原生类或者是打包的库进行扩展,一般配合 Category 实现完整的功能。
  2. ObjC 类定义的变量,由于 runtime 的特性,都会暴露到外部,使用关联对象可以隐藏关键变量,保证安全。
  3. 可以用于 KVO ,使用关联对象作为观察者,可以避免观察自身导致循环。

关联对象实现原理:

关联对象的值实际上是通过 AssociationsManager 对象负责管理的,这个对象里有个 AssociationsHashMap 静态表,用来存储对象的关联值的,关于 AssociationsHashMap 存储的数据结构如下:

AssociationsHashMap:

添加属性对象的指针地址(key):ObjectAssociationMap(value:所有关联值对象)

ObjectAssociationMap

关联值的key:关联值的value

详细介绍

3.关联对象的如何进行内存管理的?关联对象如何实现weak属性?

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     // 指定相关的对象被复制,原子性   
}
  1. 内存管理方面是通过在赋值的时候设置一个 policy ,根据这个 policy 的类型对设置的对象进行 retain/copy 等操作。
  2. policyOBJC_ASSOCIATION_ASSIGN 的时候,设置的关联值将是以 weak 的方式进行内存管理的。
    详细介绍

4.Autoreleasepool的原理?所使用的的数据结构是什么?

void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);

///而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。
void *
objc_autoreleasePoolPush(void){
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt){
    if (UseGC) return;
    AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePool的是通过AutoreleasePoolPage类实现的

    magic_t const magic; //用来校验 AutoreleasePoolPage 的结构是否完整;
    id *next; //指向栈顶,也就是最新入栈的autorelease对象的下一个位置;
    pthread_t const thread; //指向当前线程
    AutoreleasePoolPage * const parent; //指向父节点
    AutoreleasePoolPage *child; //指向子节点
    uint32_t const depth; //表示链表的深度,也就是链表节点的个数
    uint32_t hiwat;
  • AutoreleasePool 并没有单独的结构,而是由若干个AutoreleasePoolPage栈为节点的双向链表的形式组合而成(分别对应结构中的 parent 指针和 child 指针)
  • AutoreleasePool 是按线程一一对应的(结构中的 thread 指针指向当前线程)
  • AutoreleasePoolPage 每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存 autorelease 对象的地址
  • 上面的 id *next 指针作为游标指向栈顶最新add进来的 autorelease 对象的下一个位置
  • 一个 AutoreleasePoolPage 的空间被占满时,会新建一个 AutoreleasePoolPage 对象,连接链表,后来的 autorelease 对象在新的 page 加入

AutoreleasePool 的释放有如下两种情况:

  • 一种是 Autorelease 对象是在当前的 runloop 迭代结束时释放的,而它能够释放的原因是系统在每个 runloop 迭代中都加入了自动释放池 PushPop
  • 手动调用 AutoreleasePool 的释放方法(drain方法)来销毁 AutoreleasePool 或者 @autoreleasepool{} 执行完释放

AutoreleasePoolRunLoop 有什么联系? 因为在iOS应用启动后会注册两个 Observer 管理和维护 AutoreleasePool

  • 第一个 Observer 会监听RunLoop的进入,它会回调objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个 Observer 的 order 是 -2147483647优先级最高,确保发生在所有回调操作之前
  • 第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态

runloop 即将休眠的时候会把之前的自动释放池释放,然后重新创建一个新的释放池主线程的其他操作通常均在这个 AutoreleasePool 之内( main 函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建 AutoreleasePool 否则一般不需要自己创建)。 详细介绍

5.ARC的实现原理?ARC下对retain & release做了哪些优化?

详细介绍

6.ARC下哪些情况会造成内存泄漏

  1. block中的循环引用
  2. NSTimer的循环引用
  3. addObserver的循环引用
  4. delegate的强引用
  5. 大次数循环内存爆涨
  6. 非OC对象的内存处理(需手动释放)

7.为什么都同样是target-action方式button不会出现循环引用的问题,而NSTimer会?

UIControl 的内部做了weak操作,即真正持有的时候是 weak 的并没有导致 retain 加1,而NSTimer由于runloop的原因并没有做 weak 操作。

NSTimer

  • 它会被添加到 runloop,否则不会运行,当然添加的 runloop 不存在也不会运行;
  • 还要指定添加到的 runloop 的哪个模式,而且还可以指定添加到 runloop 的多个模式,模式不对也是不会运行的
  • runloop 会对 timer 有强引用,timer 会对目标对象target进行强引用(是否隐约的感觉到坑了。。。)
  • timer 的执行时间并不准确,系统繁忙的话,还会被跳过去
  • invalidate 调用后,timer 停止运行后,就一定能从 runloop 中消除吗,资源????

7.1.解决循环引用的方法

1.invalidate 方法

  • timerrunloop 中移除
  • timer 本身也会释放它持有资源,比如 target

2.引入中间者, 借助runtime给对象添加消息处理的能力

    _target = [[NSObject alloc] init];
    class_addMethod([_target class], @selector(fire), class_getMethodImplementation([self class], @selector(fire)), "v@:");
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_target selector:@selector(fire) userInfo:nil repeats:YES];

3.通过消息转发的方法的方式

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface XProxy : NSProxy
@property (nonatomic, weak) id target;
@end
    
// XProxy的实现

@implementation XProxy
// 发送给target
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
// 给target注册一个方法签名
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

@end

XProxyNSTimer 的使用

   self.proxy = [XProxy alloc];
   self.proxy.target = self;
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxy 
   selector:@selector(fire) userInfo:nil repeats:YES];

NSProxy是一个虚类,你可以通过继承它,并重写这两个方法以实现消息转发到另一个实例。说白了,NSProxy 专为代理而生(负责将消息转发到真正的 target 的代理类)。从类名来看是代理类,专门负责代理对象转发消息的。相比 NSObject 类来说 NSProxy 更轻量级,通过 NSProxy 可以帮助 Objective-C 间接的实现多重继承的功能。

7.2.设置weak能解决循环引用吗?

不能,runloop 是强持有 timer 的,声明为 weak 只是 vc 不持有

7.3.日常如何检查内存泄露?

泄露的内存主要有以下两种

  • Laek Memory 这种是忘记 Release 操作所泄露的内存。
  • Abandon Memory 这种是循环引用,无法释放掉的内存。 检查方式

1.Memory Leaks

Leaks 工具只负责检测 Leaked Memory,而不管 Abandoned Memory。
MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC时代更常见的内存泄露是循环引用导致的 Abandoned Memory,Leaks 工具查不出这类内存泄露,应用有限

2.Alloctions

对于 Abandoned memory,可以用 Instrument 的 Allocations 检测出来。
检测方法:每次点击 Mark Generation 时,Allocations 会生成当前 App 的内存快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息。

我们可以不断重复 push 和 pop 同一个 UIViewController,理论上来说,push 之前跟 pop 之后,app 会回到相同的状态。因此,在 push 过程中新分配的内存,在 pop 之后应该被 dealloc 掉,除了前几次 push 可能有预热数据和 cache 数据的情况。如果在数次 push 跟 pop 之后,内存还不断增长,则有内存泄露。
用这种方法来发现内存泄露还是很不方便的:
1、首先,你得打开 Allocations
2、其次,你得一个个场景去重复的操作
3、无法及时得知泄露,得专门做一遍上述操作,十分繁琐

3.Analyse

静态分析工具: 可以通过Product ->Analyze菜单项启动
Analyze主要分析以下四种问题:
1、逻辑错误:访问空指针或未初始化的变量等;
2、内存管理错误:如内存泄漏等;
3、声明错误:从未使用过的变量;
4、API调用错误:未包含使用的库和框架。
这里使用Analyze静态分析查找出来的泄漏点,称之为"可疑泄漏点"。之所以称之为"可疑泄漏点",是因为这些点未必一定泄露,确认这些点是否泄露, 还要通过Instruments动态分析工具的 Leaks和Allocations跟踪模板。 Analyze静态分析只是一个理论上的预测过程.

4.MLeaksFinder

MLeaksFinder 是腾讯WeRead团队开源的一款检测 iOS 内存泄漏的框架,其使用非常简单,只需将文件加入项目中,如果有内存泄漏,3秒后自动弹出 alert 来捕捉循环引用。具有无侵入性、
可构建泄漏堆栈、白名单机制等优点。
目前只检测ViewControllerView对象(可扩展,MLCheck())

总体思路:当一个 ViewController 被 pop 或 dismiss 之后,我们认为该 ViewController,包括它上面的子 ViewController,及它的 View,View 的 subView 等,都很快会被释放,如果某个 View 或者 ViewController 没释放,我们就认为该对象泄漏了。

具体的做法:为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在3秒后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接弹框提醒该对象可能存在内存泄漏。
UIViewController的分类中,使用 Method Swizzling,hook掉了
viewDidDisappear:viewWillAppear:dismissViewControllerAnimated:completion:等方法,让他们都执行willDealloc方法,这样,在不入侵开发代码的情况下,为UIViewController添加了检查内存泄露的功能(AOP

查找循环引用链:
Facebook 开源了一个循环引用检测工具 FBRetainCycleDetector。当传入内存中的任意一个 OC 对象,FBRetainCycleDetector 会递归遍历该对象的所有强引用的对象,以检测以该对象为根结点的强引用树有没有循环引用。
我们知道,很多循环引用是 block 的使用不当造成的。而 FBRetainCycleDetector 最大的技术亮点,正在于如何找出一个 block 的所有强引用对象
当 MLeaksFinder 与 FBRetainCycleDetector 结合使用时,正好能达到很好的效果。我们先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。

第三部分:其他

1.Method Swizzle注意事项?

(1)避免交换父类方法

如果当前类未实现被交换的方法而父类实现了的情况下,此时父类的实现会被交换,若此父类的多个继承者都在交换时会导致方法被交换多次而混乱,同时当调用父类的方法时会因为找不到而发生崩溃。
所以在交换前都应该先尝试为当前类添加被交换的函数的新的实现IMP,如果添加成功则说明类没有实现被交换的方法,则只需要替代分类交换方法的实现为原方法的实现,如果添加失败,则原类中实现了被交换的方法,则可以直接进行交换。

这个过程主要涉及以下三个函数:

  • class_addMethod
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

给类cls的SEL添加一个实现IMP, 返回YES则表明类cls并未实现此方法,返回NO则表明类已实现了此方法。注意:添加成功与否,完全由该类本身来决定,与父类有无该方法无关。

  • class_replaceMethod
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) 

替换类cls的SEL的函数实现为imp

  • method_exchangeImplementations
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 

交换两个方法的实现m1,m1

完整代码:

BOOL didAddMethod = class_addMethod(class,originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {//添加成功:则表明没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
    class_replaceMethod(class,swizzledSelector,
                        method_getImplementation(originalMethod),
                        method_getTypeEncoding(originalMethod));
} else {//添加失败:表明已实现,则可以直接交换实现IMP
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

(2)交换方法应在+load方法中实现

方法交换应当在调用前完成交换,+load方法发生在运行时初始化过程中类被加载的时候调用,且每个类被加载的时候只会调用一次load方法,调用的顺序是父类、类、分类,且他们之间是相互独立的,不会存在覆盖的关系,所以放在+load方法中可以确保在使用时已经完成交换。

(3)交换方法应该放到dispatch_once中执行

在第2点已经写到,+load方法在类被加载的时候调用,且只会调用一次,那为什么还需要dispatch_once呢?这是为了防止手动调用+load方法而导致反复的被交换,因为这是存在可能的。

(4)交换的分类方法应该添加自定义前缀,避免冲突

因为分类的方法会覆盖类中同名的方法,这样会导致无法预估的后果

(5)交换的分类方法应调用原实现

很多情况我们不清楚被交换的的方法具体做了什么内部逻辑,而且很多被交换的方法都是系统封装的方法,所以为了保证其逻辑性都应该在分类的交换方法中去调用原被交换方法,注意:调用时方法交换已经完成,在分类方法中应该调用分类方法本身才正确。

完整封装的代码

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface NSObject (Swizzling) 

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                         swizzledSelector:(SEL)swizzledSelector;
@end


#import "NSObject+Swizzling.h"
@implementation NSObject (Swizzling)

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{
    Class class = [self class];

    //原有被交换方法
    Method originalMethod = class_getInstanceMethod(class, originalSelector);

    //要交换的分类新方法
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    //避免交换到父类的方法,先尝试添加被交换方法的新实现IMP
    BOOL didAddMethod = class_addMethod(class,originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {//添加成功:则表明没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
        class_replaceMethod(class,swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {//添加失败:表明已实现,则可以直接交换实现IMP
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end

2.属性修饰符atomic的内部实现是怎么样的?能保证线程安全吗?

atomic内部实现

1.  id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
2.      ...
3.      id *slot = (id*) ((char*)self + offset);
4.      if (!atomic) return *slot;  
5.      // Atomic retain release world
6.      spinlock_t& slotlock = PropertyLocks[slot];
7.      slotlock.lock();
8.      id value = objc_retain(*slot);
9.      slotlock.unlock();
10.     return objc_autoreleaseReturnValue(value);
11. }

1.  static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
2.  {
3.      ...
4.      if (!atomic) {
5.          oldValue = *slot;
6.          *slot = newValue;
7.      } else {
8.          spinlock_t& slotlock = PropertyLocks[slot];
9.          slotlock.lock();
10.         oldValue = *slot;
11.         *slot = newValue;        
12.         slotlock.unlock();
13.     }
14.     objc_release(oldValue);
15. }

property 的 atomic 是采用 spinlock_t自旋锁实现的.

能保证线程安全吗?

atomic通过这种方法.在运行时仅仅是保证了set,get方法的原子性.所以使用atomic并不能保证线程安全。

举个例子:

@property (atomic, assign) int a;

// 线程a
for (int i = 0; i < 10000; i ++) {
    self.a = self.a + 1;
    NSLog(@"resultA = %d",self.a);
}

// 线程b
for (int i = 0; i < 10000; i ++) {
    self.a = self.a + 1;
    NSLog(@"resultB = %d",self.a);
}

self.a 是原子操作,但是self.a = self.a + 1这个表达式并不是原子操作,所以线程是不安全的。
resultA 在执行表达式 self.a 之后, self.a = self.a + 1 并没有执行完毕。
resultB 执行 self.a = self.a + 1,再回到 resultA 时,self.a 的数值就被更新了。
所以仅仅使用 atomic 并不能保证线程安全。

3. iOS 中内省的几个方法有哪些?内部实现原理是什么?

首先要明白一个名词 introspection 反省,内省的意思,在iOS开发中我们会称它为反射.

内省方法 例如常用的NSObject中的isKindOfClass: 通过实例对象判断class这就是一种内省方法或者叫反射方法,但我认为NSClassFromString()这个应该也算一种反射方法.

iOS 中内省的几个方法

1.  - (BOOL)isKindOfClass:(Class)aClass; //判断是否是这个类或者这个类的子类的实例
2.  - (BOOL)isMemberOfClass:(Class)aClass; //判断是否是这个类的实例
3.  - (BOOL)conformsToProtocol:(Protocol *)aProtocol;  //判断是否遵守某个协议
4.  + (BOOL)conformsToProtocol:(Protocol *)protocol; //判断某个类是否遵守某个协议
5.  - (BOOL)respondsToSelector:(SEL)aSelector;  //判读实例是否有这样方法
6.  + (BOOL)instancesRespondToSelector:(SEL)aSelector; //判断类是否有这个方法
7.  ...

内部实现原理

1.isKindOfClass

1.  + (BOOL)isKindOfClass:(Class)cls {
2.      for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
3.          if (tcls == cls) return YES;
4.      }
5.      return NO;
6.  }
7.      
8.  - (BOOL)isKindOfClass:(Class)cls {
9.      for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
10.         if (tcls == cls) return YES;
11.     }
12.     return NO;
13. }

当前的 objc 若是 实例对象,那么(isa.bits & ISA_MASK)返回的结果必定是当前实例对象的所对应的
当前的 objc 若是 类对象,那么(isa.bits & ISA_MASK)返回的结果必定是当前类对象所对应的 元类

isKindOfClass 类方法底层函数实际上就是一个for循环,通过继承链往上查找 object_getClass((id)self):
如果objc是一个类对象,那么获取的类就是当前类的元类
如果objc是一个实例对象,那么获取的就是当前的类。

2.isMemberOfClass

1.  + (BOOL)isMemberOfClass:(Class)cls {
2.      return object_getClass((id)self) == cls;
3.  }
4.  
5.  - (BOOL)isMemberOfClass:(Class)cls {
6.      return [self class] == cls;
7.  }

这俩方法非常简单直接,拿到isa指针对比,它跟 isKindOfClass 的区别在于,它不走循环,只找当前的。

3.conformsToProtocol

1.  + (BOOL)conformsToProtocol:(Protocol *)protocol {
2.      if (!protocol) return NO;
3.      for (Class tcls = self; tcls; tcls = tcls->superclass) {
4.          if (class_conformsToProtocol(tcls, protocol)) return YES;
5.      }
6.      return NO;
7.  }
8.  
9.  - (BOOL)conformsToProtocol:(Protocol *)protocol {
10.     if (!protocol) return NO;
11.     for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
12.         if (class_conformsToProtocol(tcls, protocol)) return YES;
13.     }
14.     return NO;
15. }

两个方法最终还是去isa->data()->protocols 拿到相关协议然后判断是否存在相关协议

// 这里可以清晰的看到for循环 取出相关protocol指针
// 然后通过指针和传入的参数生成的`proto`对比

1.  BOOL class_conformsToProtocol(Class cls, Protocol *proto_gen)
2.  {
3.      protocol_t *proto = newprotocol(proto_gen);  
4.      if (!cls) return NO;
5.      if (!proto_gen) return NO;
6.      mutex_locker_t lock(runtimeLock);
7.      checkIsKnownClass(cls);
8.      ASSERT(cls->isRealized())
9.      for (const auto& proto_ref : cls->data()->protocols) {
10.         protocol_t *p = remapProtocol(proto_ref);
11.         if (p == proto || protocol_conformsToProtocol_nolock(p, proto)) {
12.             return YES;
13.         }
14.     }
15.     return NO;
16. }

4.respondsToSelector

1.  + (BOOL)respondsToSelector:(SEL)sel {
2.      return class_respondsToSelector_inst(self, sel, self->ISA());
3.  }
4.  
5.  - (BOOL)respondsToSelector:(SEL)sel {
6.      return class_respondsToSelector_inst(self, sel, [self class]);
7.  }

简单的说就是一直寻找到当前实例能响应哪些方法,当前类没有就去父类,父类没有则直到元类。

1.  respondsToSelector:
2.      |__ class_respondsToSelector_inst()
3.          |__ lookUpImpOrNil()
4.              |__ lookUpImpOrForward()
5.                  返回IMP结果

其实就是整个消息转发的过程

4.classobjc_getClassobject_getclass 方法有什么区别?

1.当参数 obj 为Object实例对象
object_getClass(obj)[obj class]输出结果一直,均获得isa指针,即指向类对象的指针

**2.当参数 objClass类对象 **

  • object_getClass(obj)返回类对象中的isa指针,即指向元类对象的指针
  • [obj class]返回的则是其本身

3.当参数 objMetaclass类对象

  • object_getClass(obj) 返回 元类对象中的isa指针,因为元类对象的isa指针指向根类,所以返回的是根类对象的地址指针
  • [obj class]返回的则是其本身

4.objRootclass类对象

  • object_getClass(obj)返回 根类对象中的isa指针,因为根类对象的isa指针指向Rootclass‘s metaclass(根元类),即返回的是根元类的地址指针
  • [obj class]返回的则是其本身

总结:

  • object_getClass(obj) 返回的是 obj 中的 isa指针
  • [obj class]则分两种情况:
    1. obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的结果为obj对象中的isa指针
    2. obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身