美文网首页
iOS底层与runtime

iOS底层与runtime

作者: 夜雨聲煩_ | 来源:发表于2020-05-18 17:38 被阅读0次

    重新理解iOS底层和runtime

    对象(objc_object)

    我们总提到的OC对象是什么,实际上就是一个名为objc_object的结构体。
    如下:

    //id类型实际为objc_object结构体
    typedef struct objc_object *id;
    
    objc.h
    // objc_object 的声明
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    objc-private.h
    // objc_object 的实现
    struct objc_object {
    private:
        isa_t isa;
    以下是一些函数,省略
    ...
    }
    

    可以看到, objc_object的定义很简单,仅包含一个isa_t 类型。

    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
        
        // 省略其余
        。。。
    }
    

    isa_t 是一个联合,可以表示Class cls或uintptr_t bits类型。实际上在OC 2.0里面,多数时间用的是uintptr_t bits。bits是一个64位的数据,每一位或几位都表示了关于当前对象的信息.

    内存管理

    此部分参考文章

    概述

    当我们创建一个对象时:

    Person *person = [[Person alloc] init];
    

    上面这行代码,在栈上创建person指针,在堆上创建person对象。目前iOS不能再栈上创建对象。栈会自动管理person指针的内存管理。而person对象在堆上,需要我们自己管理。

    内存分区
    • 代码区: 存放APP二进制代码
    • 常量区:存放程序中定义的各种常量, 包括字符串常量,各种被const修饰的常量
    • 全局/静态区: 全局变量,静态变量就放在这里
    • 堆区:在程序运行时调用alloc,copy,mutablecopy,new会在堆上分配内存。堆内存需要程序员手动释放,这在ARC中是通过引用计数的形式表现的。堆分配地址不连续,但整体是地址从低到高地址分配
    • 栈区:存放局部变量,当变量超出作用域时,内存会被系统自动释放。栈上的地址连续分配,在内存地址由高向低增长
    tagged pointer

    taggedPointer是一种特殊的指针。它并非像普通指针那样储存地址,而是直接储存数据以及特殊标志。是针对NSString等类型的优化。

    32位cpu使用4字节存储NSNumber的值,使用4字节存储相应指针。一个字节为8位,即能存储2的31次方大小的数(第一位作为符号位)。而64位的cpu使用8字节存储NSNumber的值,使用8字节存储相应的指针,即能存储2的63次方大小的数(第一位作为符号位)。而实际上并不需要如此浪费。因此苹果为了优化而推出的tagged pointer,8字节中一部分存储数据,一部分存储相应标志。对于String,大于9位的字符串,不再是tagged pointer。

    a NSTaggedPointerString
    ab NSTaggedPointerString
    ...
    abcdefghi NSTaggedPointerString
    abcdefghij __NSCFString
    
    • Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
    • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
    • 在内存读取上有着3倍的效率,创建时比以前快106倍。

    string三种不同的初始化方式的三种不同类型,也对应三种不同的存储空间:

        NSString *constString = @"person";
        NSMutableString *mutableString = [NSMutableString stringWithFormat:@"person"];
        NSString *taggedPointerString = [NSString stringWithFormat:@"person"];
        NSLog(@"+++person:%p,%@,%p,%@",constString,constString,&constString,constString.class);
        NSLog(@"+++person:%p,%@,%p,%@",mutableString,mutableString,&mutableString,mutableString.class);
        NSLog(@"+++person:%p,%@,%p,%@",taggedPointerString,taggedPointerString,&taggedPointerString,taggedPointerString.class);
    

    运行结果:

    2020-05-18 17:35:12.038424+0800 CPTest[3630:296051] +++person:0x100001018,person,0x7ffeefbff590,__NSCFConstantString
    2020-05-18 17:35:12.039217+0800 CPTest[3630:296051] +++person:0x101201470,person,0x7ffeefbff598,__NSCFString
    2020-05-18 17:35:12.039448+0800 CPTest[3630:296051] +++person:0x533eaffec150723,person,0x7ffeefbff5a0,NSTaggedPointerString
    
    内存地址

    综上,iOS需要一个标志位来判断当前指针是真正的指针还是tagged pointer。这里有一个宏定义_OBJC_TAG_MASK (1UL<<63) ,它表明如果64位数据中,最高位是1的话,则表明当前是一个tagged pointer类型。
    下面是runtime源码的objc-internal.h中,有关于标志位的具体定义:

    {
        //用4个二进制位表示,下面是十进制下对应的枚举。
        OBJC_TAG_NSAtom            = 0, 
        OBJC_TAG_1                 = 1, 
        OBJC_TAG_NSString          = 2, 
        OBJC_TAG_NSNumber          = 3, 
        OBJC_TAG_NSIndexPath       = 4, 
        OBJC_TAG_NSManagedObjectID = 5, 
        OBJC_TAG_NSDate            = 6, 
        OBJC_TAG_RESERVED_7        = 7, 
    }
    

    也就是,1XXX表示tagged pointer类型,而1010表示是NSTaggedPointerString,1011表示NSTaggedPointerNumber。其余同理。

    tagged point string类型地址如下。

    2020-05-18 19:33:58.374317+0800 Zero[4785:350752] ++++0xe6dea3cc6aec6a95
    2020-05-18 19:33:58.374445+0800 Zero[4785:350752] ++++0xe6dea3cc6aea4aa6
    2020-05-18 19:33:58.374538+0800 Zero[4785:350752] ++++0xe6d8f58a2d8a4aa2
    

    0xe转化为1110并非1010。
    看一下源码:

    static void
    initializeTaggedPointerObfuscator(void)
    {
        if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
            // Set the obfuscator to zero for apps linked against older SDKs,
            // in case they're relying on the tagged pointer representation.
            DisableTaggedPointerObfuscation) {
            objc_debug_taggedpointer_obfuscator = 0;
        } else {
            // Pull random data into the variable, then shift away all non-payload bits.
            arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                           sizeof(objc_debug_taggedpointer_obfuscator));
            objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
        }
    }
    
    

    可以看到10.14以后执行了objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;。而此函数的作用为,生成随机数按位混淆,防止直接通过控制台输出观察是否是tagged pointer。

    注意:苹果官方采用随机数+按位运算的方式进行混淆。
    另补充:&=与赋值运算, ~按位求反`

    在环境变量里添加OBJC_DISABLE_TAG_OBFUSCATIONYES,关闭混淆。(EditScheme->Envieroment Variables->+
    打印变成:

    2020-05-18 20:16:42.622093+0800 Zero[4896:375513] ++++0xa000000000000621
    2020-05-18 20:16:42.622234+0800 Zero[4896:375513] ++++0xa000000000062612
    2020-05-18 20:16:42.622335+0800 Zero[4896:375513] ++++0xa006564647662616
    

    0xa000000000000621中,a为1010,即符合前文说的NSTaggedPointerString类型。而的62是 ASCLL 中的b。至于最后一位:

    • 0 表示unsigned int类型,
    • 1 表示short类型,
    • 2 表示整型,
    • 3 表示长整型,
    • 4 表示单精度类型,
    • 5 表示双精度类型。

    另外:栈区内存地址:一般为:0x7开头
    堆区内存地址:一般为:0x6开头
    数据段、BSS内存地址:一般为:0x1开头

    isa

    此部分非常好的原文
    (基本都是原文复制,只是原文图片经常加载不出来,感谢原作者)

    概念

    一个Objc中所有对象都有的一个成员变量,是指向该对象的类的指针。

    苹果文档:A Pointer to the class definition of which this object is an instance.

    struct objc_class : objc_object {
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    }
    
    struct objc_object {
    private:
        isa_t isa;
    }
    

    我们都知道,Objc的所有的对象都包含一个类型为 isa 的指针。不过现在的 ObjC 对象的结构已经不是这样了。代替 isa 指针的是结构体 isa_t, 这个结构体中"包含"了当前对象指向的类的信息。包括用来标识它是什么类型,以及一些其他内容。

    OC对象的根本就是objc_object,而类继承自objc_object,即类也是对象。而objc_object的核心即是isa_t isa。

    当 ObjC 为一个对象分配内存,初始化实例变量后,在这些对象的实例变量的结构体中的第一个就是 isa。


    D1908773E58C2F4B15FD1FEFF86DE36E.jpg
    64位优化

    如果isa指针仅表示类型的话,对内存显然也是一个极大的浪费。于是,就像tagged pointer一样,对于isa指针,苹果同样进行了优化。isa指针表示的内容变得更为丰富,除了表明对象属于哪个类之外,还附加了引用计数extra_rc,是否有被weak引用标志位weakly_referenced,是否有附加对象标志位has_assoc等信息。

    不只是实例会包含一个 isa 结构体,所有的类也有这么一个 isa。即类也是对象。

    struct objc_class : objc_object {
        isa_t isa;
        Class superclass;
        cache_t cache;
        class_data_bits_t bits;
    };
    

    由于 objc_class 结构体是继承自 objc_object 的,所以在这里显式地写出了 isa_t isa 这个成员变量。

    元类

    因为在 Objective-C 中,对象的方法并没有存储于对象的结构体中(如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响)。

    当实例方法被调用时,它要通过自己持有的 isa 来查找对应的类,然后在这里的 class_data_bits_t 结构体中查找对应方法的实现。同时,每一个 objc_class 也有一个指向自己的父类的指针 super_class 用来查找继承的方法。


    E951404586AB6724B325816FE83A440B.jpg

    但是,这样就有一个问题,类方法的实现又是如何查找并且调用的呢?这时,就需要引入元类来保证无论是类还是对象都能通过相同的机制查找方法的实现。
    下面这张图介绍了对象,类与元类之间的关系,笔者认为已经觉得足够清晰了,所以不在赘述。


    7FF1891DE3B7610FEA53E7DC9B4E64D6.jpg

    结构体 isa_t

    其实 isa_t 是一个定义得非常"奇怪"的结构体,在 ObjC 源代码中可以看到这样的定义:

    #define ISA_MASK        0x00007ffffffffff8ULL
    #define ISA_MAGIC_MASK  0x001f800000000001ULL
    #define ISA_MAGIC_VALUE 0x001d800000000001ULL
    #define RC_ONE   (1ULL<<56)
    #define RC_HALF  (1ULL<<7)
    
    union isa_t {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    
        struct {
            uintptr_t indexed           : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 44;
            uintptr_t magic             : 6;
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;
            uintptr_t extra_rc          : 8;
        };
    };
    

    isa_t 是一个 union 类型的结构体,其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间。而 isa 总共会占据 64 位的内存空间(决定于其中的结构体)


    192840591E6157389093DA3581D2C346.png

    Class cls即未启动isa优化的指针,而struct为启动isa优化的指针。同样可参考下表:

    成员 含义
    nonpointer 1bit 标志位。1(奇数)表示开启了isa优化,0(偶数)表示没有启用isa优化。所以,我们可以通过判断isa是否为奇数来判断对象是否启用了isa优化。
    has_assoc 1bit 标志位。表明对象是否有关联对象。没有关联对象的对象释放的更快。
    has_cxx_dtor 1bit 标志位。表明对象是否有C++或ARC析构函数。没有析构函数的对象释放的更快。
    shiftcls 33bit 类指针的非零位。
    magic 6bit 固定为0x1a,用于在调试时区分对象是否已经初始化。
    weakly_referenced 1bit 标志位。用于表示该对象是否被别的对象弱引用。没有被弱引用的对象释放的更快。
    deallocating 1bit 标志位。用于表示该对象是否正在被释放。
    has_sidetable_rc 1bit 标志位。用于标识是否当前的引用计数过大,无法在isa中存储,而需要借用sidetable来存储。(这种情况大多不会发生)
    extra_rc 19bit 对象的引用计数减1。比如,一个object对象的引用计数为7,则此时extra_rc的值为6。

    isa 的初始化

    我们可以通过 isa 初始化的方法 initIsa 来初步了解这 64 位的 bits 的作用:

    inline void 
    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
    {
        initIsa(cls, true, hasCxxDtor);
    }
    
    inline void 
    objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) 
    { 
        if (!indexed) {
            isa.cls = cls;
        } else {
            isa.bits = ISA_MAGIC_VALUE;
            isa.has_cxx_dtor = hasCxxDtor;
            isa.shiftcls = (uintptr_t)cls >> 3;
        }
    }
    
    indexed 和 magic

    当我们对一个 ObjC 对象分配内存时,其方法调用栈中包含了上述的两个方法,这里关注的重点是 initIsa 方法,由于在 initInstanceIsa 方法中传入了 indexed = true,所以,我们简化一下这个方法的实现

    inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) 
    { 
        //一共三部初始化objc对象中isa的值
        //1.初始设置nonpointer为1,magic47-52位,拆解0x001d800000000001中的47-52位为0x3b
        isa.bits = ISA_MAGIC_VALUE;
        //2.设置
        isa.has_cxx_dtor = hasCxxDtor;
        //3.设置
        isa.shiftcls = (uintptr_t)cls >> 3;
        //至此,isa初始化完成,没设置位为0,表示全部信息。
    }
    

    对整个 isa 的值 bits 进行设置,传入 ISA_MAGIC_VALUE:

    indexed即上表中的nonpointer位,用于表示是否开启isa优化。

    #define ISA_MAGIC_VALUE 0x001d800000000001ULL
    

    我们可以把它转换成二进制的数据,然后看一下哪些属性对应的位被这行代码初始化了(标记为红色):


    1510726F1615E872CF769DB9EE78F4C6.png

    从图中了解到,在使用 ISA_MAGIC_VALUE 设置 isa_t 结构体之后,实际上只是设置了 indexed 以及 magic 这两部分的值。

    • nonpointer 为0 表示 raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型。即32位下没有优化的原始类型。
    • nonpointer 为1 表示当前 isa 不是指针,而是 struct。但是其中也有 cls 的信息,只是其中关于类的指针都是保存在结构体的一部分—— shiftcls 中。
    • magic 的值为 0x3b 用于调试器判断当前对象是真的对象还是没有初始化的空间。(magic47-52位,拆解ISA_MAGIC_VALUE中的47-52位为0x3b)
    has_cxx_dtor

    在设置 indexed 和 magic 值之后,会设置 isa 的 has_cxx_dtor,这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。


    111590668338_.pic.jpg
    shiftcls

    在为 indexed、 magic 和 has_cxx_dtor 设置之后,我们就要将当前对象对应的类指针存入 isa 结构体中了

    isa.shiftcls = (uintptr_t)cls >> 3;
    

    将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。
    绝大多数机器的架构都是 byte-addressable 的,但是对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 000,我们只会用其中的 30 位来表示对象的地址。

    如果再尝试打印对象指针的话,会发现所有对象内存地址的后四位都是 0,说明 ObjC 在初始化内存时是以 16 个字节对齐的, 分配的内存地址后四位都是 0。如下图所示。


    101590668337_.pic_hd.jpg

    我尝试运行了下面的代码将 NSObject 的类指针和对象的 isa 打印出来,具体分析一下


    131590669338_.pic.jpg
    object_pointer: 0000000001011101100000000000000100000000001110101110000011111001 // 补全至 64 位
    class_pointer:                                 100000000001110101110000011111000
    

    编译器对直接访问 isa 的操作会有警告,因为直接访问 isa 已经不会返回类指针了,这种行为已经被弃用了,取而代之的是使用 ISA() 方法来获取类指针。

    代码中的 object 对象的 isa 结构体中的内容是这样的:


    121590669337_.pic.jpg

    其中红色的为类指针,与上面打印出的 [NSObject class] 指针右移三位的结果完全相同。这也就验证了我们之前对于初始化 isa 时对 initIsa 方法的分析是正确的。它设置了 indexed、magic 以及 shiftcls。

    ISA() 方法

    因为我们使用结构体取代了原有的 isa 指针,所以要提供一个方法 ISA() 来返回类指针。
    其中 ISA_MASK 是宏定义,这里通过掩码的方式获取类指针:

    #define ISA_MASK 0x00007ffffffffff8ULL
    inline Class 
    objc_object::ISA() 
    {
        return (Class)(isa.bits & ISA_MASK);
    }
    

    举例

    创建一个对象:

    NSObject *obj = [[NSObject alloc] init];
    
    • obj == NSObject * 即在数据段、BSS内存(具体不确定)中的NSObject声明了个指针变量,变量名为*obj,*obj存储在栈中。
    • obj == *isa_t。NSObject实际上是objc_class继承自objc_object,唯一的成员变量是isa_t isa。而isa_t如上文分析,并非存储地址,而是一个64位优化后的真实数据。

    个人理解:isa是所有对象共有的一个变量,指向该对象所属的类。一个64位的变量只是表示所属类太过浪费,因此还记录了引用计数等。
    当NSObject声明obj1和obj2时,分别在栈中连续开辟两个内存空间,他们拥有相同的isa指向。当obj1的引用计数发生改变时,它对应的isa值也发生改变,不再与obj2中的isa相同。
    实例方法被调用时,会通过其持有 isa _t指针shiftcls寻找对应的类,然后在其中的 class_data_bits_t (存储调用类的方法,属性,协议等信息的地方)中查找对应的方法,如果已经缓存过cache 则从cache中寻找。我们先来介绍下isa_t及他的初始化实现。


    image.png

    测试一下,并看一下打印:

        MyObj *obj1 = [[MyObj alloc] init];
        obj1.name = @"obj1";
        MyObj *obj2 = [[MyObj alloc] init];
        obj2.name = @"obj2";
        NSLog(@"obj1 isa_t = %p", *(void **)(__bridge void*)obj1);
        NSLog(@"obj2 isa_t = %p", *(void **)(__bridge void*)obj2);
        NSLog(@"obj1 栈中地址%p",&obj1);
        NSLog(@"obj2 栈中地址%p",&obj2);
        NSLog(@"obj1 堆中对象地址%p",obj1);
        NSLog(@"obj2 堆中对象地址%p",obj2);
        MyObj *tmpObj = obj1;
        NSLog(@"--------- 创建temp obj 增加引用计数后 ----------");
        NSLog(@"obj1 isa_t = %p", *(void **)(__bridge void*)obj1);
        NSLog(@"obj2 isa_t = %p", *(void **)(__bridge void*)obj2);
        NSLog(@"tempobj isa_t = %p", *(void **)(__bridge void*)tmpObj);
        NSLog(@"obj1 栈中地址%p",&obj1);
        NSLog(@"obj2 栈中地址%p",&obj2);
        NSLog(@"temp obj 栈中地址%p",&tmpObj);
        NSLog(@"obj1 堆中对象地址%p",obj1);
        NSLog(@"obj2 堆中对象地址%p",obj2);
        NSLog(@"temp obj 堆中地址%p",tmpObj);
    

    重要:isa-nopointer特性只在真机中有用,模拟器中isa的地址始终固定
    打印结果:

    2020-05-22 20:24:19.643314+0800 Zero[4013:882997] obj1 isa_t = 0x1a102b315b5
    2020-05-22 20:24:19.643370+0800 Zero[4013:882997] obj2 isa_t = 0x1a102b315b5
    2020-05-22 20:24:19.643394+0800 Zero[4013:882997] obj1 栈中地址0x16d2d9c38
    2020-05-22 20:24:19.643408+0800 Zero[4013:882997] obj2 栈中地址0x16d2d9c30
    2020-05-22 20:24:19.643420+0800 Zero[4013:882997] obj1 堆中对象地址0x281a5cba0
    2020-05-22 20:24:19.643432+0800 Zero[4013:882997] obj2 堆中对象地址0x281a5cbb0
    2020-05-22 20:24:19.643444+0800 Zero[4013:882997] --------- 创建temp obj 增加引用计数后 ----------
    2020-05-22 20:24:19.643464+0800 Zero[4013:882997] obj1 isa_t = 0x21a102b315b5
    2020-05-22 20:24:19.643688+0800 Zero[4013:882997] obj2 isa_t = 0x1a102b315b5
    2020-05-22 20:24:19.643769+0800 Zero[4013:882997] tempobj isa_t = 0x21a102b315b5
    2020-05-22 20:24:19.643884+0800 Zero[4013:882997] obj1 栈中地址0x16d2d9c38
    2020-05-22 20:24:19.644010+0800 Zero[4013:882997] obj2 栈中地址0x16d2d9c30
    2020-05-22 20:24:19.644112+0800 Zero[4013:882997] temp obj 栈中地址0x16d2d9c28
    2020-05-22 20:24:19.644205+0800 Zero[4013:882997] obj1 堆中对象地址0x281a5cba0
    2020-05-22 20:24:19.644317+0800 Zero[4013:882997] obj2 堆中对象地址0x281a5cbb0
    2020-05-22 20:24:19.644931+0800 Zero[4013:882997] temp obj 堆中地址0x281a5cba0
    

    SideTable

    其实在绝大多数情况下,仅用优化的isa_t来记录对象的引用计数就足够了。只有在19位的extra_rc盛放不了那么大的引用计数时,才会借助SideTable。
    SideTable是一个全局的引用计数表,它记录了所有对象的引用计数。

    其实将fastpath和slowpath去掉是完全不影响任何功能的。之所以将fastpath和slowpath 放到if语句中,是为了告诉编译器,if中的条件是大概率(fastpath)还是小概率(slowpath)事件,从而让编译器对代码进行优化

    在runtime中,通过SideTable来管理对象的引用计数以及weak引用。这里要注意,一张SideTable会管理多个对象,而并非一个。
    而这一个个的SideTable又构成了一个集合,叫SideTables。SideTables在系统中是全局唯一的。

    注意到这个SideTables哈希数组是全局的,因此,对于我们APP中所有的对象的引用计数,也就都存在于这64个SideTable中。

    SideTable包含三个成员:

    • spinlock_t slock:自旋锁。防止多线程访问SideTable冲突
    • RefcountMap refcnts:用于存储对象引用计数的map
    • weak_table_t weak_table: 用于存储对象弱引用的map
    RefcountMap refcnts

    对象引用计数map RefcountMap的key是:-(object *),就是对象地址取负。value就是该对象的引用计数。

    runtime在获取对象引用计数的时候,是考虑了三种情况:(1)tagged pointer, (2)优化的isa, (3)未优化的isa。

    inline uintptr_t 
    objc_object::rootRetainCount()
    {
        //case 1: 如果是tagged pointer,则直接返回this,因为tagged pointer是不需要引用计数的
        if (isTaggedPointer()) return (uintptr_t)this;
    
        // 将objcet对应的sidetable上锁
        sidetable_lock();
        isa_t bits = LoadExclusive(&isa.bits);
        ClearExclusive(&isa.bits);
        // case 2: 如果采用了优化的isa指针
        if (bits.nonpointer) {
            uintptr_t rc = 1 + bits.extra_rc; // 先读取isa.extra_rc
            if (bits.has_sidetable_rc) { // 如果extra_rc不够大, 还需要读取sidetable中的数据
                rc += sidetable_getExtraRC_nolock(); // 总引用计数= rc + sidetable count
            }
            sidetable_unlock();
            return rc;
        }
        // case 3:如果没采用优化的isa指针,则直接返回sidetable中的值
        sidetable_unlock(); // 将objcet对应的sidetable解锁,因为sidetable_retainCount()中会上锁
        return sidetable_retainCount();
    }
    

    我们来看一下(2)优化的isa 的情况下:
    首先,会读取extra_rc中的数据,因为extra_rc中存储的是引用计数减一,所以这里要加回去。
    如果extra_rc 不够大的话,还需要读取sidetable,调用sidetable_getExtraRC_nolock:

    Weekly reference

    weak_table_t weak_table,它记录了所有弱引用对象的集合。包含了被弱引用的对象,以及弱引用该对象的对象列表。

    关于weak引用数据,是存在于hash表中的。这关于hash算法映射到数组下标,以及hash表动态的扩容/收缩.

    autoreleasepool

    在iOS中,除了需要手动retain,release(现在已经交给了ARC自动生成)外,我们还可以将对象扔到自动释放池中,由自动释放池来自动管理这些对象。

    关于@autoreleasepool的代码可以被改写为:

    objc_autoreleasePoolPush();
    // Do your code
    objc_autoreleasePoolPop(atautoreleasepoolobj);
    

    置于@autoreleasepool的{}中的代码实际上是被一个push和pop操作所包裹。当push时,会压栈一个autoreleasepage,在{}中的所有的autorelease对象都会放到这个page中。当pop时,会出栈一个autoreleasepage,同时,所有存储于这个page的对象都会做release操作。这就是autoreleasepool的实现原理。

    objc_autoreleasePoolPush()和objc_autoreleasePoolPop(atautoreleasepoolobj)的实现如下:

    void *
    objc_autoreleasePoolPush(void)
    {
        return AutoreleasePoolPage::push();
    }
    
    void
    objc_autoreleasePoolPop(void *ctxt)
    {
        AutoreleasePoolPage::pop(ctxt);
    }
    

    它们都分别调用了AutoreleasePoolPage类的静态方法push和pop。AutoreleasePoolPage 是runtime中autoreleasepool的核心实现。

    AutoreleasePoolPage

    AutoreleasePoolPage在runtime中的定义如下:

    class AutoreleasePoolPage 
    {
        magic_t const magic;                // 魔数,用于自身的完整性校验                                                         16字节
        id *next;                           // 指向autorelePool page中的下一个可用位置                                           8字节
        pthread_t const thread;             // 和autorelePool page中相关的线程                                                  8字节
        AutoreleasePoolPage * const parent; // autoreleasPool page双向链表的前向指针                                             8字节
        AutoreleasePoolPage *child;         // autoreleasPool page双向链表的后向指针                                             8字节
        uint32_t const depth;               // 当前autoreleasPool page在双向链表中的位置(深度)                                   4字节
        uint32_t hiwat;                     // high water mark. 最高水位,可用近似理解为autoreleasPool page双向链表中的元素个数       4字节
    
        // SIZE-sizeof(*this) bytes of contents follow
    }
    

    每个AutoreleasePoolPage的大小为一个SIZE,即内存管理中一个页的大小。这在Mac中是4KB,而在iOS中,这里没有相关代码,估计差不多。

    相关文章

      网友评论

          本文标题:iOS底层与runtime

          本文链接:https://www.haomeiwen.com/subject/bkouohtx.html