iOS 2021 面试前的准备(总结各知识点方便面试前快速复习使用)(六)

941 阅读17分钟

48. 成员变量修饰符的作用。

 那么我们常用的 __strong__weak__unsafe_unretained 等等修饰成员变量的修饰符系统又是如何处理的呢?

 当我们定义一个类的成员变量的时候,可以为其指定其修饰符 __strong__weak__unsafe_unretained(未指定时默认为 __strong),这使得成员变量可以像 strongweakunsafe_unretained 修饰符修饰的属性一样在 ARC 下进行正确的引用计数管理。定义如下 Person 类:

// Person.m 什么都不用实现
@interface LGPerson : NSObject {
    NSObject *ivar_none; // 未明确指定修饰符的成员变量默认为 __strong 修饰
    __strong NSObject *ivar_strong;
    __weak NSObject *ivar_weak;
    __unsafe_unretained NSObject *ivar_unsafe_unretained;
}
@end

// 编写如下代码,分别进行测试:
    Person *person = [[Person alloc] init];
    NSObject *temp = [[NSObject alloc] init];
    
    NSLog(@"START");
    person->ivar_none = temp;
    
//    person->ivar_strong = temp;
//    NSLog(@"TTT %@", person->ivar_strong);

//    person->ivar_weak = temp;
//    NSLog(@"read weak: %@", person->ivar_weak);

//    person->ivar_unsafe_unretained = temp;

    NSLog(@"END"); // ⬅️ 在这里打断点

 在 END 行打断点,然后 xcode 菜单栏依次 Debug -> Debug Workflow -> Always Show Disassembly 勾选 Always Show Disassembly,运行程序当断点执行时,我们的代码会被编译为汇编代码,依次看到 temp 为成员变量赋值时的指令跳转:

  • ivar_none 赋值时 bl 0x10092e470; symbol stub for: objc_storeStrong

  • ivar_strong 赋值时 bl 0x1009be470; symbol stub for: objc_storeStrong

  • ivar_weak 赋值时 bl 0x1009be47c; symbol stub for: objc_storeWeak,读取时调用了 objc_loadWeakRetainedobjc_release

  • ivar_unsafe_unretained 赋值时没有发生任何指令跳转,只是单纯的根据地址存储值。

 结果和我们前面面的不同修饰符修饰属性时测试的结果完全相同。分析上面属性的汇编代码时我们已知编译器在生成属性的 getter setter 函数时会针对不同的属性修饰符做不同的处理来正确管理对象的引用计数,那么我们为不同的成员变量指定的修饰符信息又是保存在哪里?又是怎么起作用的呢?

struct class_ro_tconst uint8_t * ivarLayoutconst uint8_t * weakIvarLayout 两个成员变量分别记录了那些成员变量是 strong 或是 weak,都未记录的就是基本类型和 __unsafe_unretained 的对象类型。这两个值可以通过 runtime 提供的如下几个 API 来访问和修改:

OBJC_EXPORT const uint8_t * _Nullable class_getIvarLayout(Class _Nullable cls);
OBJC_EXPORT const uint8_t * _Nullable class_getWeakIvarLayout(Class _Nullable cls);
OBJC_EXPORT void class_setIvarLayout(Class _Nullable cls, const uint8_t * _Nullable layout);
OBJC_EXPORT void class_setWeakIvarLayout(Class _Nullable cls, const uint8_t * _Nullable layout);

ivarLayoutweakIvarLayout 类型是 uint8_t *,一个 uint8_t16 进制下是两位。

ivarLayout 是一系列的字符,每两个一组,比如 \xmn,每一组 Ivar Layout 中第一位表示有 m 个非强引用成员变量,第二位表示接下来有 n 个强引用成员变量。

 对于 ivarLayout 来说,每个 uint8_t 的高 4 位代表连续是非 storng 类型 Ivar 的数量(m),m ∈ [0x0, 0xf],低 4 位代表连续是 strong 类型 Ivar 的数量(n),n ∈ [0x0, 0xf]

 对于 weakIvarLayout 来说,每个 uint8_t 的高 4 位代表连续是非 weak 类型 Ivar 的数量(m),m ∈ [0x0, 0xf],低 4 位代表连续是 weak 类型 Ivar 的数量(n),n ∈ [0x0, 0xf]

 无论是 ivarLayout 还是 weakIvarLayout,都在末尾填充 \x00 表示结尾。

 对于 ivarLayout 来说,它只关心 strong 成员变量的数量,而记录前面有多少个非 strong 变量的数量无非是为了正确移动索引值而已。在最后一个 strong 变量后面的所有非 strong 变量,都会被自动忽略。weakIvarLayout 同理,apple 这么做的初衷是为了尽可能少的内存去描述类的每一个成员变量的内存修饰符。像上面的例子中 20 个成员变量,ivarLayout 用了 2 + 1 = 3 个字节 weakIvarLayout 用了 2 + 1 = 3 个字节,就描述了 20 个变量的内存修饰符。


49. objc_class 的函数列表,快速浏览即可。

class_rw_t *data() const; data 函数是直接调用的 class_data_bits_t bitsdata 函数,内部实现的话也很简单,通过掩码 #define FAST_DATA_MASK 0x00007ffffffffff8UL(二进制第 3-46 位是 1,其他位都是 0) 从 class_data_bits_t bitsuintptr_t bitsuintptr_t bitsstruct class_data_bits_t 仅有的一个成员变量)中取得 class_rw_t 指针。

void setData(class_rw_t *newData); 同样 setData 函数调用的也是 class_data_bits_t bitssetData 函数,在 struct class_data_bits_tsetData 函数中, uintptr_t bits 首先和 ~FAST_DATA_MASK 做与操作把掩码位置为 0,其它位保持不变,然后再和 newData 做或操作,把 newData 中值为 1 的位也设置到 uintptr_t bits 中。

void setInfo(uint32_t set); setInfo 函数调用的是 class_rw_tsetFlags 函数,通过原子的或操作把 set 中值为 1 的位也设置到 class_rw_tuint32_t flags 中。(flagsset 都是 uint32_t 类型,都是 32 位。)

void clearInfo(uint32_t clear); clearInfo 函数调用的是 class_rw_tclearFlags 函数,通过原子的与操作把 ~clear 中值为 0 的位也设置到 class_rw_tuint32_t flags 中。(flagsclear 都是 uint32_t 类型,都是 32 位。)

void changeInfo(uint32_t set, uint32_t clear); changeInfo 函数调用的同样也是 class_rw_tchangeFlags 函数,先取得 class_rw_tuint32_t flags 赋值到一个临时变量 uint32_t oldf 中,然后 oldfuint32_t set 做一个或操作,把 set 值为 1 的位也都设置到 oldf 中,然后再和 ~clear 做与操作,把 ~clear 中是 0 的位也都设置其中,并把 !OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags) 作为 do while 循环的循环条件,保证 uint32_t oldf 的值都能写入 uint32_t flags 中。

 下面是一些掩码的使用。

 FAST_HAS_DEFAULT_RR / RW_HAS_DEFAULT_RR

FAST_HAS_DEFAULT_RR 用以在 __LP64__ 平台下判断 objc_classclass_data_bits_t bits 第二位的值是否为 1,以此表示该类或者父类是否有如下函数的默认实现,对应在 非 __LP64 平台下,则是使用 RW_HAS_DEFAULT_RR,且判断的位置发生了变化,RW_HAS_DEFAULT_RR 用以判断从 objc_classclass_data_bits_t bits 中取得 class_rw_t 指针指向的 class_rw_t 实例的 uint32_t flags 的第 14 位的值是否为 1,以此表示该类或者父类是否有如下函数的默认实现:

  • retain/release/autorelease/retainCount
  • _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
  1. 当从 class_rw_t->flags 取值时,使用 RW_* 做前缀,
  2. 当从 cache_t->_flags 取值时,使用 FAST_CACHE_* 做前缀
  3. 当从 class_data_bits_t->bits 取值时,使用 FAST_* 做前缀

FAST_*FAST_CACHE_* 前缀开头的值,分别保存在 objc_classclass_data_bits_t bitscache_t cache 两个成员变量中,直接减少了指针间接寻值,RW_* 前缀开头的值则首先要从 class_data_bits_t bits 中找到 class_rw_t 的指针,然后根据指针再去寻 class_rw_t 实例的值,然后再读取它的 flags 成员变量中的值。

hasCustomRR/setHasDefaultRR/setHasCustomRR__LP64__ 平台和其它平台下的判断、设置、清除 objc_class 的默认 RR 函数的标记位。

 FAST_CACHE_HAS_DEFAULT_AWZ / RW_HAS_DEFAULT_AWZ

FAST_CACHE_HAS_DEFAULT_AWZ 用以在 __LP64__ 平台下判断 objc_classcache_t cacheuint16_t _flags 二进制表示时第 14 位的值是否为 1,以此表示该类或者父类是否有 alloc/allocWithZone 函数的默认实现。(注意,这里和上面的 RR 不同,RR 是一组实例方法保存在类中,而 alloc/allocWithZone 是一组类方法保存在元类中。)而在 非 __LP64__ 平台下,则是使用 RW_HAS_DEFAULT_AWZ,且判断的位置发生了变化,RW_HAS_DEFAULT_AWZ 用以判断从 objc_classclass_data_bits_t bits 中取得的 class_rw_t 指针指向的 class_rw_t 实例的 uint32_t flags 的第 16 位的值是否为 1,以此表示该类或者父类是否有 alloc/allocWithZone 函数的默认实现。

hasCustomAWZ/setHasDefaultAWZ/setHasDefaultAWZ__LP64__ 平台和其它平台下判断、设置、清除 objc_class 的默认 AWZ 函数的标记位。

 FAST_CACHE_HAS_DEFAULT_CORE / RW_HAS_DEFAULT_CORE

FAST_CACHE_HAS_DEFAULT_CORE 用以在 __LP64__ 平台下判断 objc_classcache_t cacheuint16_t _flags 二进制表示时第 15 位的值是否为 1,以此表示该类或者父类是否有 new/self/class/respondsToSelector/isKindOfClass 函数的默认实现。而在 非 __LP64__ 平台下,则是使用 RW_HAS_DEFAULT_CORE,且判断的位置发生了变化,RW_HAS_DEFAULT_CORE 用以判断从 objc_classclass_data_bits_t bits 中取得 class_rw_t 指针指向的 class_rw_t 实例的 uint32_t flags 的第 13 位的值是否为 1,以此表示该类或者父类是否有 new/self/class/respondsToSelector/isKindOfClass 函数的默认实现。

hasCustomCore/setHasDefaultCore/setHasCustomCore__LP64__ 平台和其它平台下判断、设置、清除 objc_class 的默认 Core 函数的标记位。

 FAST_CACHE_HAS_CXX_CTOR / RW_HAS_CXX_CTOR / FAST_CACHE_HAS_CXX_DTOR / RW_HAS_CXX_DTOR

FAST_CACHE_HAS_CXX_CTOR 用以在 __LP64__ 平台下判断 objc_classcache_t cacheuint16_t _flags 二进制表示时第 1 位的值是否为 1,以此表示该类或者父类是否有 .cxx_construct 函数实现。而在 非 __LP64__ 平台下,则是使用 RW_HAS_CXX_CTOR,且判断的位置发生了变化,RW_HAS_CXX_CTOR 用以判断从 objc_classclass_data_bits_t bits 中取得 class_rw_t 指针指向的 class_rw_t 实例的 uint32_t flags 的第 18 位的值是否为 1,以此表示该类或者父类是否有 .cxx_construct 函数实现。对应的 FAST_CACHE_HAS_CXX_DTORRW_HAS_CXX_DTOR 表示该类或者父类是否有 .cxx_destruct 函数实现。 这里需要注意的是在 __LP64__ && __arm64__ 平台下 FAST_CACHE_HAS_CXX_DTOR1<<0,而在 __LP64__ && !__arm64__ 平台下 FAST_CACHE_HAS_CXX_DTOR1<<2

hasCxxCtor/setHasCxxCtor/hasCxxDtor/setHasCxxDtor__LP64__ 平台和其它平台下判断、设置(注意这里没有清除)objc_class.cxx_construct/.cxx_destruct 函数实现的标记位。

 FAST_CACHE_REQUIRES_RAW_ISA / RW_REQUIRES_RAW_ISA

FAST_CACHE_REQUIRES_RAW_ISA 用以在 __LP64__ 平台下判断 objc_classcache_t cacheuint16_t _flags 二进制表示时第 13 位的值是否为 1,以此表示类实例对象(此处是指类对象,不是使用类构建的实例对象,一定要记得)是否需要原始的 isa。而在 非 __LP64__SUPPORT_NONPOINTER_ISA 的平台下,则是使用 RW_REQUIRES_RAW_ISA,且判断的位置发生了变化,RW_REQUIRES_RAW_ISA 用以判断从 objc_classclass_data_bits_t bits 中取得 class_rw_t 指针指向的 class_rw_t 实例的 uint32_t flags 的第 15 位的值是否为 1,以此表示类实例对象(此处是指类对象,不是使用类构建的实例对象,一定要记得)是否需要原始的 isa

instancesRequireRawIsa/setInstancesRequireRawIsa__LP64__ 平台和其它平台下判断、设置类实例(此处是指类对象,不是使用类构建的实例对象,一定要记得)需要原始 isa 的标记位。

  • ! __LP64__ 平台下,所有掩码都存储在 class_rw_tuint32_t flags
  • __LP64__ 平台下,FAST_HAS_DEFAULT_RR 存储在 class_data_bits_t bitsuintptr_t bits。(1UL<<2)(retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
  • __LP64__ 平台下,FAST_CACHE_HAS_DEFAULT_AWZ 存储在 cache_t cacheuint16_t _flags 下。(1<<14)(alloc/allocWithZone:
  • __LP64__ 平台下,FAST_CACHE_HAS_DEFAULT_CORE 存储在 cache_t cacheuint16_t _flags 下。(1<<15)(new/self/class/respondsToSelector/isKindOfClass
  • __LP64__ 平台下,FAST_CACHE_HAS_CXX_CTOR 存储在 cache_t cacheuint16_t _flags 下。(1<<1)(.cxx_construct
  • __LP64__ 平台下,FAST_CACHE_HAS_CXX_DTOR 存储在 cache_t cacheuint16_t _flags 下。(1<<2 / 1<<0)(.cxx_destruct
  • __LP64__ 平台下,FAST_CACHE_REQUIRES_RAW_ISA 存储在 cache_t cacheuint16_t _flags 下。(1<<13)(requires raw isa

 我们如何才能获取一个类的所有子类呢 ?这里正式使用到了 struct class_rw_t 的两个成员变量 Class firstSubclassClass nextSiblingClass

bool canAllocNonpointer(); 表示 objc_classisa 是非指针,即类对象不需要原始 isa 时,能根据该函数返回值设置 isa_t isauintptr_t nonpointer : 1 字段,标记该类的 isa 是非指针。

 下面的一些掩码相关的操作我们开始用到 struct class_ro_tuint32_t flags了!它们的宏定义名都是以 RO_ 开头的。

bool hasAutomaticIvars();class_data_bits_t bits 中取出 class_rw_t 指针,然后从 struct class_rw_t 中取出 explicit_atomic<uintptr_t> ro_or_rw_ext 对应的 class_rw_ext_t 指针,然后从 struct class_rw_ext_t 中取出 const class_ro_t *ro,然后取出 uint32_t flags 和 (RO_IS_ARC | RO_HAS_WEAK_WITHOUT_ARC (二进制表示第 7 位和第 9 位是 1,其它位都是 0))做与操作。

 如果类的 ivars 由 ARC 管理,或者该类是 MRC 但具有 ARC 样式的 weak ivars,则返回 YES。(weak 修饰符是可以在 MRC 中使用的,weak 是和 ARC 一起推出的,根据之前 weak 的实现原理也可知它的实现流程和 ARC 或者 MRC 是完全没有关系的。)

bool isARC(); 同上,最后取出 class_ro_tuint32_t flagsRO_IS_ARC 做与操作。如果类的 ivar 由 ARC 管理,则返回 YES。

 RW_FORBIDS_ASSOCIATED_OBJECTS 禁止类的实例对象进行关联对象的掩码,看到它前缀是 RW 开始的,表示它用在 struct class_rw_tuint32_t flags 中。

bool forbidsAssociatedObjects(); 禁止该类的实例对象进行 Associated Object。从 class_data_bits_t bits 中取出 class_rw_t 指针,然后从 struct class_rw_t 中取出 uint32_t flagsRW_FORBIDS_ASSOCIATED_OBJECTS(第 20 位值为 1)与操作的结果。

instancesHaveAssociatedObjects / setInstancesHaveAssociatedObjectsstruct class_rw_tuint32_t flags 做掩码操作。

 RW_INITIALIZING 判断 objc_class 是否正在进行初始化的掩码。判断位置在 struct class_rw_tuint32_t flags 中。

bool isInitializing(); RW_INITIALIZINGRW 前缀开头,可直接联想到其判断位置在 struct class_rw_tuint32_t flags 中,与前面的一些判断相比这里 objc_class 的位置发生了变化,前面我们所有的判断都是在当前的 objc_class 中进行的,而此处的判断要转移到当前 objc_class 的元类中,元类的类型也是 struct objc_class,所以它们同样也有 class_data_bits_t bitscache_t cache 等成员变量,这里 isInitializing 函数使用的正是元类的 class_data_bits_t bits 成员变量。getMeta 函数是取得当前 objc_class 的元类,然后 data 函数从元类的 class_data_bits_t bits 中取得 class_rw_t 指针,然后取得 struct class_rw_tuint32_t flagsRW_INITIALIZING 做与操作,取得 flags 二进制表示的第 28 位的值作为结果返回。

void setInitialized(); 标记该类初始化完成。

IMP getLoadMethod(); 获取一个类的 +load 函数且仅从 mlist = ISA()->data()->ro()->baseMethods(); 中获取,如果找不到的话返回 nil,会忽略分类中的 +load 函数。

 首先我们要对 +load 函数和别的函数做出一些理解上的区别,首先我们在任何时候都不应该自己主动去调用 +load 函数,它是由系统自动调用的,且它被系统调用时是直接通过它的函数地址调用的,它是不走 objc_msgSend 消息发送流程的。当我们在自己的类定义中添加了 +load 函数,编译过程中编译器会把它存储在元类的 struct class_ro_tmethod_list_t * baseMethodList 成员变量中。那么 category 中的 +load 函数在编译过程中会被放在哪里呢?

 FAST_CACHE_META / RW_META / RO_META 在 __LP64__ 平台下标识 objc_class 是否是元类的值在 cache_t cache 中,其它情况则是在 struct class_rw_tuint32_t flags 中(需要根据指针进行寻址)。

bool isMetaClass(); 如果 FAST_CACHE_META 存在,则从 cache_t cacheuint16_t _flags 二进制表示的第 2/0 位判断当前 objc_class 是否是元类。其它情况则从 class_data_bits_t bits 中取得 class_rw_t 指针指向的 class_rw_t 实例的 uint32_t flags 二进制表示的第 0 位进行判断。

Class getMeta(); 取得当前类的元类。如果当前类是元类,则直接返回 this,如果不是元类,则调用 objc_object::ISA() 函数取得元类。(从 isa 中根据掩码取出类的地址:(Class)(isa.bits & ISA_MASK))

bool isRootClass(); 判断一个类是否是根类,只是判断一个类的 superclass 是否为 nil 即可。

  • 根类的父类是 nil,根类的元类是根元类。
  • 根元类的父类是根类,根元类的元类是它自己。

bool isRootMetaclass(); 判断是否是根元类,判断依据是根元类的元类是它自己,即如果一个 objc_class 的元类是自己的话,那么它是根元类。(ISA() == (Class)this;


50. method_t / ivar_t / property_t

 method_t 是方法的数据结构,包括方法名、方法类型(参数和返回值)、方法实现(IMP)。

struct method_t {
    SEL name; // 方法名、选择子
    const char *types; // 方法类型
    
    // using MethodListIMP = IMP;
    MethodListIMP imp; // 方法实现

    // 根据选择子的地址进行排序
    struct SortBySELAddress :
        public std::binary_function<const method_t&, const method_t&, bool>
    {
        bool operator() (const method_t& lhs, const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
struct ivar_t {

    // 首先这里要和结构体中成员变量的偏移距离做出理解上的区别。

    // offset 它是一个指针,那它指向谁呢,它指向一个全局变量,
    // 编译器为每个类的每个成员变量都分配了一个全局变量,用于存储该成员变量的偏移值。

    // 如果我们仅有成员变量的名字,能想到的成员变量的读取可能是这样的:
    // 当我们读取成员变量时从实例对象的 isa 找到类,然后 data() 找到 class_rw_t 
    // 然后再找到 class_ro_t 然后再找到 ivar_list_t,
    // 然后再比较 ivar_t 的 name 和我们要访问的成员变量的名字是否相同,然后再读出 int 类型的 offset,
    // 再进行 self + offset 指针偏移找到这个成员变量。

    // 现在当我们访问一个成员变量时,只需要 self + *offset 就可以了。
    // 下面会验证 offset 指针。
    
    int32_t *offset;
    
    // 成员变量名称(如果类中有属性的话,编译器会自动生成 _属性名 的成员变量)
    const char *name;
    
    // 成员变量类型
    const char *type;
    
    // alignment is sometimes -1; use alignment() instead
    // 对齐有时为 -1,使用 alignment() 代替。
    
    // 原始对齐值是 2^alignment_raw 的值
    // alignment_raw 应该叫做对齐值的指数
    uint32_t alignment_raw;
    
    // 成员变量的类型的大小
    uint32_t size;

    // #ifdef __LP64__
    // #   define WORD_SHIFT 3UL
    // #else
    // #   define WORD_SHIFT 2UL
    // #endif
    
    uint32_t alignment() const {
    
        // 应该没有类型的 alignment_raw 会是 uint32_t 类型的最大值吧!
        // WORD_SHIFT 在 __LP64__ 下是 3,表示 8 字节对齐,
        // 在非 __LP64__ 下是 2^2 = 4 字节对齐。
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        
        // 2^alignment_raw 
        return 1 << alignment_raw;
    }
};

 我们首先定义一个 Person 类:

// 定义一个 LGPerson 类
// Person.h 如下,.m 为空即可
@interface Person : NSObject

@property (nonatomic, strong) NSMutableArray *arr;

@end

 然后在终端执行 clang -rewrite-objc Person.m 生成 Person.cpp

 摘录 Person.cpp:

ivar_list_t 如下,arr 为仅有的成员变量,它对应的 ivar_t 初始化部分 int32_t *offset 值使用了 (unsigned long int *)&OBJC_IVAR_$_LGPerson$_arr

static struct /*_ivar_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count;
    struct _ivar_t ivar_list[1];
} _OBJC_$_INSTANCE_VARIABLES_LGPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_ivar_t),
    1,
    {{(unsigned long int *)&OBJC_IVAR_$_LGPerson$_arr, "_arr", "@\"NSMutableArray\"", 3, 8}}
};

 在 LGPerson.cpp 文件中全局搜索 OBJC_IVAR_$_LGPerson$_arr 有如下结果:

// 全局变量声明和定义赋值
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_arr;
extern "C" unsigned long int 
           OBJC_IVAR_$_LGPerson$_arr 
           __attribute__ ((used, section ("__DATA,__objc_ivar"))) = 
           __OFFSETOFIVAR__(struct LGPerson, _arr);

// arr 的 setter getter 函数,看到都是直接使用了 OBJC_IVAR_$_LGPerson$_arr
static NSMutableArray * _Nonnull _I_LGPerson_arr(LGPerson * self, SEL _cmd) { 
    return (*(NSMutableArray * _Nonnull *)((char *)self + OBJC_IVAR_$_LGPerson$_arr)); 
}

static void _I_LGPerson_setArr_(LGPerson * self, SEL _cmd, NSMutableArray * _Nonnull arr) { 
    (*(NSMutableArray * _Nonnull *)((char *)self + OBJC_IVAR_$_LGPerson$_arr)) = arr; 
}

 看到 property_t 极其简单,编译器会自动生成一个对应的 _属性名的成员变量保存在 ivars 中。

struct property_t {
    // 属性名字
    const char *name;
    
    // 属性的 attributes,例如:
    
    // @property (nonatomic, strong) NSObject *object;
    // object 的 attributes:"T@\"NSObject\",&,N,V_object"
    //
    // @property (nonatomic, copy) NSArray *array2;
    // array2 的 attributes:"T@\"NSArray\",C,N,V_array2"
    
    const char *attributes;
};

51. dealloc 函数执行。

dealloc 函数的调用是在 rootRelease 函数的最后通过 ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc)) 来执行的,正常情况下我们不能主动调用 dealloc 函数。dealloc 函数内部调用 rootDealloc,其内部有一个这样的判断:if (fastpath(isa.nonpointer && !isa.weakly_referenced && !isa.has_assoc && !isa.has_cxx_dtor && !isa.has_sidetable_rc)) {...} 如下条件全部为真的话,可以直接调用 free 进行快速内存释放:

  1. 对象的 isa 是优化的 isa
  2. 对象不存在弱引用。
  3. 对象没有关联对象。
  4. 对象没有自定义的 C++ 的析构函数。
  5. 对象的引用计数没有保存在 SideTable 中。

52. AutoreleasePool 自动释放池。

 一个线程的自动释放池就是一个存放指针的栈(自动释放池整体结构是由 AutoreleasePoolPage 组成的双向链表,而每个 AutoreleasePoolPage 里面则有一个存放对象指针的栈)。栈里面的每个指针要么是等待 autorelease 的对象,要么是 POOL_BOUNDARY 自动释放池边界(实际为 #define POOL_BOUNDARY nil,同时也是 next 的指向)。一个 pool token 是指向 POOL_BOUNDARY 的指针。When the pool is popped, every object hotter than the sentinel is released. 当自动释放池执行 popped,every object hotter than the sentinel is released.。(这句没有看懂)这些栈分散位于由 AutoreleasePoolPage 构成的双向链表中。AutoreleasePoolPage 会根据需要进行添加和删除。hotPage 保存在当前线程中,当有新的 autorelease 对象添加进自动释放池时会被添加到 hotPage,hotPage 即为当前 AutoreleasePoolPage 组成的双向链表中,有空位的一个节点。

 根据 AutoreleasePoolPageData 的构造函数可知,第一个节点的 parentchild 都是 nil,当第一个 AutoreleasePoolPage 满了,会再创建一个 AutoreleasePoolPage,此时会拿第一个节点作为 newParent 参数来构建这第二个节点,即第一个节点的 child 指向第二个节点,第二个节点的 parent 指向第一个节点。

begin 函数超关键的,首先要清楚一点 beginAutoreleasePoolPage 中存放的 自动释放对象 的起点。回顾上面的的 new 函数的实现我们已知系统总共给 AutoreleasePoolPage 分配了 4096 个字节的空间,这么大的空间除了前面一部分空间用来保存 AutoreleasePoolPage 的成员变量外,剩余的空间都是用来存放自动释放对象地址的。

AutoreleasePoolPage 的成员变量都是继承自 AutoreleasePoolPageDate,它们总共需要 56 个字节的空间,然后剩余 4040 字节空间,一个对象指针占 8 个字节,那么一个 AutoreleasePoolPage 能存放 505 个需要自动释放的对象。(可在 main.m 中引入 #include "NSObject-internal.h" 打印 sizeof(AutoreleasePoolPageData) 的值确实是 56。)

id * begin() {
    // (uint8_t *)this 是 AutoreleasePoolPage 的起始地址,
    // 且这里用的是 (uint8_t *) 的强制类型转换,uint8_t 占 1 个字节,
    // 然后保证 (uint8_t *)this 加 56 时是按 56 个字节前进的
    
    // sizeof(*this) 是 AutoreleasePoolPage 所有成员变量的宽度是 56 个字节,
    // 返回从 page 的起始地址开始前进 56 个字节后的内存地址。
    return (id *) ((uint8_t *)this+sizeof(*this));
}

next 指针通常指向的是当前自动释放池内最后面一个自动释放对象的后面,如果此时 next 指向 begin 的位置,表示目前自动释放池内没有存放自动释放对象。

 "冷" page,首先找到 hotPage 然后沿着它的 parent 走,直到最后 parentnil,最后一个 AutoreleasePoolPage 就是 coldPage,返回它。这里看出来其实 coldPage 就是双向 page 链表的第一个 page

autoreleaseFast 把对象快速放进自动释放池。

static inline id *autoreleaseFast(id obj)
{
    // hotPage
    AutoreleasePoolPage *page = hotPage();
    
    if (page && !page->full()) {
        // 如果 page 存在并且 page 未满,则直接调用 add 函数把 obj 添加到 page
        return page->add(obj);
    } else if (page) {
        // 如果 page 满了,则调用 autoreleaseFullPage 构建新 AutoreleasePoolPage,并把 obj 添加进去
        return autoreleaseFullPage(obj, page);
    } else {
        // 连 hotPage 都不存在,可能就一 EMPTY_POOL_PLACEHOLDER 在线程的存储空间内保存 
        // 如果 page 不存在,即当前线程还不存在自动释放池,构建新 AutoreleasePoolPage,并把 obj 添加进去
        return autoreleaseNoPage(obj);
    }
}

53. retain 函数。

// Equivalent to calling [this retain], with shortcuts if there is no override
// 等效于调用 [this retain],如果没有重载此函数,则能快捷执行(快速执行)
inline id 
objc_object::retain()
{
    // Tagged Pointer 不参与引用计数管理,它的内存在栈区,由系统自行管理
    ASSERT(!isTaggedPointer());

    // 如果没有重载 retain/release 函数,则调用根类的 rootRetain() 函数
    //(hasCustomRR() 函数定义在 objc_class 中,等下面分析 objc_class 时再对其进行详细分析)
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    // 如果重载了 retain 函数,则以 objc_msgSend 调用重载的 retain 函数
    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}

 Base retain 函数实现,忽略重载。这不检查 isa.fast_rr; 如果存在 RR 重载,则它已经被调用,并选择调用 [super retain]。 当 tryRetain=true 会执行 -_tryRetain 路径,会执行一个(return sidetable_tryRetain() ? (id)this : nil;) 当 handleOverflow=false 时是 frameless 的快速路径。 当 handleOverflow=true 时是一个 framed 的慢速路径会把溢出的引用计数转移到 SideTable 的 refcnts 中。 当 SIDE_TABLE_DEALLOCATING 被标记时,会返回 nil,其它情况都返回 (id)this 以这种方式构造代码以防止重复。

ALWAYS_INLINE id 
objc_object::rootRetain()
{
    // tryRetain 和 handleOverflow 都传入的 false,不执行 -_tryRetain path. 
    // handleOverflow=false 处理 extra_rc++ 溢出的情况
    return rootRetain(false, false);
}

tryRetain 参数如其名,尝试持有,它涉及到的只有一个 return sidetable_tryRetain() ? (id)this : nil; 操作,只有当对象处于正在销毁状态时,才会返回 false。当对象的 isa 是原始指针时,对象的引用计数全部保存在 SideTablerefcnts 里面的。sidetable_tryRetain 函数只会在对象的 isa 是原始指针时才会被调用。

handleOverflow 参数是处理 newisa.extra_rc++ overflowed 情况的,当溢出情况发生后,如果 handleOverflow 传入的是 false 时,则会调用 return rootRetain_overflow(tryRetain),它只有一件事情,就是把 handleOverflow 传递为 true 再次调用 rootRetain 函数。所以无论如何,当引用计数溢出时都是必会进行处理的。

 当对象的 isa 是优化的 isaextra_rc 溢出时,会把一部分引用计数转移到 RefcountMap 中,只是转移一部分,并不是 extra_rc 溢出以后,对象的引用计数全部交给 RefcountMap 管理了,每次溢出后会把 extra_rc 置为 RC_HALF,然后下次增加引用计数增加的还是 extra_rc,直到再次溢出再转移到 RefcountMap 中。大概是因为操作 extra_rc 的消耗要远低于操作 RefcountMap

 循环结束的条件是: !StoreExclusive(&isa.bits, oldisa.bits, newisa.bits),这句代码意思是,&isa.bitsoldisa.bits 进行原子比较字节逐位相等的话,则把 newisa.bits 复制(同 std::memcpy 函数)到 &isa.bits 中,并且返回 true。如果 &isa.bitsoldisa.bits 不相等的话,则把 &isa.bits 中的内容加载到 oldisa.bits 中。所以这里的循环的真实目的就是为了把: newisa.bits 复制到 &isa.bits 中,保证对象 bits 中的数据能正确的进行修改。

  1. 当对象的 isa 是非优化的 isa 时,对象的引用计数全部保存在 SideTable 中,当要增加引用计数时就调用 sidetable_tryRetain/sidetable_retain 增加 SideTable 中的引用计数。
  2. 当对象的 isa 是优化的 isa 且对象的引用计数保存在 extra_rc 字段中且加 1 后未溢出时,此时也是比较清晰的,执行完加 1 后,函数也直接 return (id)this 结束了。
  3. 只有第三种情况比较特殊,当对象的 isa 是优化的 isa 且对象的引用计数保存在 extra_rc 中,此时 extra_rc++ 后发生溢出,此时会把 extra_rc 赋值为 RC_HALF,把 has_sidetable_rc 赋值为 true,然后调用 sidetable_addExtraRC_nolock(RC_HALF)。其实疑问就发生在这里,如果对象的 extra_rc 中的引用计数已经溢出过了,并转移到了 SideTable 中一部分,此时 extra_rc 是被置为了 RC_HALF,那下次增加对象的引用计数时,并不是直接去 SideTable 中增加引用计数,其实是增加 extra_rc 中的值,直到增加到再次溢出时才会跑到 SideTable 中增加引用计数。这里还挺迷惑的,觉的最好的解释应该是尽量在 extra_rc 字段中增加引用计数,少去操作 SideTable,毕竟操作 SideTable 还要加锁解锁,还要哈希查找等,整体消耗肯定是大于直接操作 extra_rc 字段的。

 我们首先要清楚一件很重要的事情,当对象的 isa 是原始类指针时,在 SideTableRefcountMap refcnts 中取出 objc_object 对应的 size_t 的值并不是单纯的对象的引用计数这一个数字,它是明确有一些标志位存在的,且有些标志位所代表的含义与 isa 是非指针的 objc_objectisa_t isa 中的一些位是相同的。所以这里我们不能形成定式思维,觉的这些标志位只存在于 isa_t isa 中。

 如下列举当对象的 isa 是原始指针时,一些标志位所代表的含义:

  • SIDE_TABLE_WEAKLY_REFERENCEDsize_t 的第 0 位,表示该对象是否有弱引用。(此时是针对 isa 是原始指针的对象,对应于 isa 是非指针时,x86_64isa_t isauintptr_t weakly_referenced : 1; 字段)
  • SIDE_TABLE_DEALLOCATINGsize_t 的第 1 位,表示对象是否正在进行释放。(同上,对应于 x86_64isa_t isauintptr_t deallocating : 1; 字段)
  • SIDE_TABLE_RC_ONEsize_t 的第 2 位,这时才正式开始表示该对象的引用计数。(当对象的 isa 是非指针时,引用计数也是从第 2 位开始的,但是它的前两位不做任何标记位使用,只是单纯的被舍弃了。)
  • SIDE_TABLE_RC_PINNED__LP64__ 64 位系统架构下是 size_t 的第 63 位,32 位系统架构下是第 31 位,也就是 size_t 的最后一位,表示在 SideTable 中的引用计数溢出。(大概是不会存在一个对象的引用计数大到连 SideTable 都存不下的吧)
  • SIDE_TABLE_RC_SHIFT 帮助我们从 size_t 中拿出真实引用计数用的,即从第 2 位开始,后面的数值都表示对象的引用计数了。
  • SIDE_TABLE_FLAG_MASKSIDE_TABLE_RC_ONE 的值减 1,它的二进制表示是 0b11 后两位是 1,其它位都是 0,做掩码使用。

54. rootRetainCount 计算引用计数的值。

 如果对象的 isa 是非指针的话,引用计数同时在 extra_rc 字段和 SideTable 中保存,要求它们的和。如果对象的 isa 是原始 isa 的话,对象的引用计数数据只保存在 SideTable 中。

inline uintptr_t 
objc_object::rootRetainCount()
{
    // 如果是 Tagged Pointer 的话,获取它的引用计数则直接返回 (uintptr_t)this
    if (isTaggedPointer()) return (uintptr_t)this;
    
    // 加锁
    sidetable_lock();
    
    // 以原子方式加载 &isa.bits 数据
    isa_t bits = LoadExclusive(&isa.bits);
    // 如果是 __arm64__ && !__arm64e__ 平台下,要清除独占标记
    ClearExclusive(&isa.bits);
    
    if (bits.nonpointer) {
        // 如果对象的 isa 是非指针的话,引用计数同时在 extra_rc 字段和 SideTable 中保存,要求它们的和
        // 这里加 1, 是因为 extra_rc 存储的是对象本身之外的引用计数的数量
        uintptr_t rc = 1 + bits.extra_rc;
        
        // 如果 has_sidetable_rc 位为 1,则表示在 SideTable 中也保存有对象的引用计数数据
        if (bits.has_sidetable_rc) {
            // 找到对象的在 SideTable 中的引用计数并增加到 rc 中
            rc += sidetable_getExtraRC_nolock();
        }
        // 解锁
        sidetable_unlock();
        // 返回 rc
        return rc;
    }

    sidetable_unlock();
    
    // 如果对象的 isa 是原始 isa 的话,对象的引用计数数据只保存在 SideTable 中
    return sidetable_retainCount();
}

🎉🎉🎉 未完待续...