美文网首页Runtime
探秘Runtime - 剖析Runtime结构体

探秘Runtime - 剖析Runtime结构体

作者: 刘小壮 | 来源:发表于2018-05-19 18:36 被阅读1696次
    该文章属于<简书 — 刘小壮>原创,转载请注明:

    <简书 — 刘小壮> https://www.jianshu.com/p/5b7e7c8075ef


    博客配图

    NSObject

    之前的定义

    OC1.0中,Runtime很多定义都写在NSObject.h文件中,如果之前研究过Runtime的同学可以应该见过下面的定义,定义了一些基础的信息。

    // 声明Class和id
    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    
    // 声明常用变量
    typedef struct objc_method *Method;
    typedef struct objc_ivar *Ivar;
    typedef struct objc_category *Category;
    typedef struct objc_property *objc_property_t;
    
    // objc_object和objc_class
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    struct objc_class {  
        Class isa  OBJC_ISA_AVAILABILITY;
        
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE;
        const char *name                                         OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
        
    } OBJC2_UNAVAILABLE;
    

    之前的Runtime结构也比较简单,都是一些很直接的结构体定义,现在新版的Runtime在操作的时候,各种地址偏移操作和位运算。

    之后的定义

    后来可能苹果也不太想让开发者知道Runtime内部的实现,所以就把源码定义从NSObject中搬到Runtime中了。而且之前的定义也不用了,通过OBJC_TYPES_DEFINED预编译指令,将之前的代码废弃调了。

    现在NSObject中的定义非常简单,直接就是一个Class类型的isa变量,其他信息都隐藏起来了。

    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
        Class isa  OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    

    这是最新的一些常用Runtime定义,和之前的定义也不太一样了,用了最新的结构体对象,之前的结构体也都废弃了。

    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    
    typedef struct method_t *Method;
    typedef struct ivar_t *Ivar;
    typedef struct category_t *Category;
    typedef struct property_t *objc_property_t;
    

    对象结构体

    objc_object定义

    在OC中每个对象都是一个结构体,结构体中都包含一个isa的成员变量,其位于成员变量的第一位。isa的成员变量之前都是Class类型的,后来苹果将其改为isa_t

    struct objc_object {
    private:
        isa_t isa;
    };
    

    OC中的类和元类也是一样,都是结构体构成的。由于类的结构体定义继承自objc_object,所以其也是一个对象,并且具有对象的isa特征。

    对象结构体

    所以可以通过isa_t来查找对应的类或元类,查找方法应该是通过uintptr_t类型的bits,通过按位操作来查找isa_t指向的类的地址。

    实例对象或类对象的方法,并不会定义在各个对象中,而是都定义在isa_t指向的类中。查找到对应的类后,通过类的class_data_bits_t类型的bits结构体查找方法,对象、类、元类都是同样的查找原理。

    isa_t定义

    isa_t是一个union的结构对象,union类似于C++结构体,其内部可以定义成员变量和函数。在isa_t中定义了clsbitsisa_t三部分,下面的struct结构体就是isa_t的结构体构成。

    下面对isa_t中的结构体进行了位域声明,地址从nonpointer起到extra_rc结束,从低到高进行排列。位域也是对结构体内存布局进行了一个声明,通过下面的结构体成员变量可以直接操作某个地址。位域总共占8字节,所有的位域加在一起正好是64位。

    小提示:unionbits可以操作整个内存区,而位域只能操作对应的位。

    下面的代码是不完整代码,只保留了arm64部分,其他部分被忽略掉了。

    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    
    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
        struct {
            uintptr_t nonpointer        : 1; // 是32位还是64位
            uintptr_t has_assoc         : 1; // 对象是否含有或曾经含有关联引用,如果没有关联引用,可以更快的释放对象
            uintptr_t has_cxx_dtor      : 1; // 表示是否有C++析构函数或OC的析构函数
            uintptr_t shiftcls          : 33; // 对象指向类的内存地址,也就是isa指向的地址
            uintptr_t magic             : 6; // 对象是否初始化完成
            uintptr_t weakly_referenced : 1; // 对象是否被弱引用或曾经被弱引用
            uintptr_t deallocating      : 1; // 对象是否被释放中
            uintptr_t has_sidetable_rc  : 1; // 对象引用计数太大,是否超出存储区域
            uintptr_t extra_rc          : 19; // 对象引用计数
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
        };
    
    # elif __x86_64__
    // ····
    # else
    // ····
    # endif
    };
    

    ARM64架构下,isa_t以以下结构进行布局。在不同的CPU架构下,布局方式会有所不同,但参数都是一样的。

    isa_t定义

    类结构体

    objc_class结构体

    Runtime中类也是一个对象,类的结构体objc_class是继承自objc_object的,具备对象所有的特征。在objc_class中定义了三个成员变量,superclass是一个objc_class类型的指针,指向其父类的objc_class结构体。cache用来处理已调用方法的缓存。

    bitsobjc_class的主角,其内部只定义了一个uintptr_t类型的bits成员变量,存储了class_rw_t的地址。bits中还定义了一些基本操作,例如获取class_rw_traw isa状态、是否swift等函数。objc_class结构体中定义的一些函数,其内部都是通过bits实现的。

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             
        class_data_bits_t bits;    
    
        class_rw_t *data() { 
            return bits.data();
        }
        void setData(class_rw_t *newData) {
            bits.setData(newData);
        }
        // .....
    }
    

    objc_class的源码可以看出,可以通过bits结构体的data()函数,获取class_rw_t指针。我们进入源代码中看一下,可以看出是通过对uintptr_t类型的bits变量,做位运算查找对应的值。

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    

    uintptr_t本质上是一个unsigned longtypedefunsigned long在64位处理器中占8字节,正好是64位二进制。通过FAST_DATA_MASK转换为二进制后,是取bits中的47-3的位置,正好是取出class_rw_t指针。

    在OC中一个指针的长度是47,例如打印一个UIViewController的地址是0x7faf1b580450,转换为二进制是11111111010111100011011010110000000010001010000,最后面三位是占位的,所以在取地址的时候会忽略最后三位。

    // 查找第0位,表示是否swift
    #define FAST_IS_SWIFT           (1UL<<0)
    // 当前类或父类是否定义了retain、release等方法
    #define FAST_HAS_DEFAULT_RR     (1UL<<1)
    // 类或父类需要初始化isa
    #define FAST_REQUIRES_RAW_ISA   (1UL<<2)
    // 数据段的指针
    #define FAST_DATA_MASK          0x00007ffffffffff8UL
    // 11111111111111111111111111111111111111111111000 总共47位
    

    因为在bits中最后三位是没用的,所以可以用来存储一些其他信息。在class_data_bits_t还定义了三个宏,用来对后三位做位运算。

    class_ro_t和class_rw_t

    class_data_bits_t相关的有两个很重要结构体,class_rw_tclass_ro_t,其中都定义着method listprotocol listproperty list等关键信息。

    struct class_rw_t {
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;
    };
    

    在编译后class_data_bits_t指向的是一个class_ro_t的地址,这个结构体是不可变的(只读)。在运行时,才会通过realizeClass函数将bits指向class_rw_t

    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
        uint32_t reserved;
    
        const uint8_t * ivarLayout;
        
        const char * name;
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    };
    

    在程序开始运行后会初始化Class,在这个过程中,会把编译器存储在bits中的class_ro_t取出,然后创建class_rw_t,并把ro赋值给rw,成为rw的一个成员变量,最后把rw设置给bits,替代之前bits中存储的ro。除了这些操作外,还会有一些其他赋值的操作,下面是初始化Class的精简版代码。

    static Class realizeClass(Class cls) 
    {
        const class_ro_t *ro;
        class_rw_t *rw;
        Class supercls;
        Class metacls;
        bool isMeta;
    
        if (!cls) return nil;
        if (cls->isRealized()) return cls;
    
        ro = (const class_ro_t *)cls->data();
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    
        isMeta = ro->flags & RO_META;
        rw->version = isMeta ? 7 : 0;
    
        supercls = realizeClass(remapClass(cls->superclass));
        metacls = realizeClass(remapClass(cls->ISA()))
    
        cls->superclass = supercls;
        cls->initClassIsa(metacls);
        cls->setInstanceSize(ro->instanceSize);
    
        if (supercls) {
            addSubclass(supercls, cls);
        } else {
            addRootClass(cls);
        }
    
        methodizeClass(cls);
        return cls;
    }
    

    在上面的代码中我们还发现了两个函数,addRootClassaddSubclass函数,这两个函数的职责是将某个类的子类串成一个列表,大致是下面的链接顺序。由此可知,我们是可以通过class_rw_t,获取到当前类的所有子类。

    superClass.firstSubclass -> subClass1.nextSiblingClass -> subClass2.nextSiblingClass -> ...
    

    初始化rwro之后,rwmethod listprotocol listproperty list都是空的,需要在下面methodizeClass函数中进行赋值。函数中会把rolist都取出来,然后赋值给rw,如果在运行时动态修改,也是对rw做的操作。所以ro中存储的是编译时就已经决定的原数据,rw才是运行时动态修改的数据。

    static void methodizeClass(Class cls)
    {
        bool isMeta = cls->isMetaClass();
        auto rw = cls->data();
        auto ro = rw->ro;
    
        method_list_t *list = ro->baseMethods();
        if (list) {
            prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
            rw->methods.attachLists(&list, 1);
        }
    
        property_list_t *proplist = ro->baseProperties;
        if (proplist) {
            rw->properties.attachLists(&proplist, 1);
        }
    
        protocol_list_t *protolist = ro->baseProtocols;
        if (protolist) {
            rw->protocols.attachLists(&protolist, 1);
        }
    
        if (cls->isRootMetaclass()) {
            // root metaclass
            addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
        }
    
        // Attach categories.
        category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
        attachCategories(cls, cats, false /*don't flush caches*/);
    }
    

    假设创建一个类LXZObject,继承自NSObject,并为其加入一个testMethod方法,不做其他操作。因为在编译后objc_classbits对应的是class_ro_t结构体,所以我们打印一下结构体的成员变量,看一下编译后的class_ro_t是什么样的。

    struct class_ro_t {
      flags = 128
      instanceStart = 8
      instanceSize = 8
      reserved = 0
      ivarLayout = 0x0000000000000000 <no value available>
      name = 0x0000000100000f7a "LXZObject"
      baseMethodList = 0x00000001000010c8
      baseProtocols = 0x0000000000000000
      ivars = 0x0000000000000000
      weakIvarLayout = 0x0000000000000000 <no value available>
      baseProperties = 0x0000000000000000
    }
    

    经过打印可以看出,一个类的class_ro_t中只会包含当前类的信息,不会包含其父类的信息,在LXZObject类中只会包含namebaseMethodList两个字段,而baseMethodList中只有一个testMethod方法。由此可知,class_rw_t结构体也是一样的。

    类结构体

    初始化过程

    下面是已经初始化后的isa_t结构体的布局,以及各个结构体成员在结构体中的位置。

    结构图

    union经常配合结构体使用,第一次使用union就是对结构体区域做初始化。在对象初始化时,会对isa_tbits字段赋值为ISA_MAGIC_VALUE,这就是对union联合体初始化的过程。

    // 在objc-723中已经没有了
    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;
        }
    }
    

    在对象通过initIsa()函数初始化时,会通过ISA_MAGIC_VALUEisa进行初始化。ISA_MAGIC_VALUE是一个16进制的值,将其转换为二进制后,会发现ISA_MAGIC_VALUE是对nonpointermagic做初始化。

    nonpointer是对之前32位处理器的兼容。在访问对象所属的类时,如果是32位则返回之前的isa指针地址,否则表示是64位处理器,则返回isa_t结构体。

    # define ISA_MAGIC_VALUE 0x000001a000000001ULL
    二进制:11010000000000000000000000000000000000001
    补全二进制:23个零+11010000000000000000000000000000000000001
    

    随后会通过位域,对has_cxx_dtorshiftcls做初始化,这时候就已经有四个字段被初始化了。has_cxx_dtor表示是否有C++或OC的析构方法,在打印方法列表时,经常能看到一个名为.cxx_destruct的方法,就和这个字段有关系。

    在计算机中为了对存储区(Memory or Disk)读取方便,所以在写入和读取时,会对内存有对其操作。一般是以字节为单位进行对其,这样也是对读写速度的优化。在对shiftcls进行赋值时,对Class的指针进行了位移操作,向右位移三位。这是因为类指针为了内存对其,将最后三位用0填充,所以这三位是没有意义的。

    isa结构体
    0000000001011101100000000000000100000000001110101110000011111001
    0x5d8001003ae0f8
    
    类对象地址
    100000000001110101110000011111000
    0x1003ae0f8
    
    将类对象地址右移三位为100000000001110101110000011111,正好符合isa_t地址中shiftcls的部分,前面不足补零。
    

    外界获取Class时,应该通过ISA()函数,而不是像之前一样直接访问isa指针。在ISA()函数中,是对isa_t的结构体做与运算,是通过ISA_MASK宏进行的,转换为二进制的话,正好是把shiftcls的地址取出来。

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

    Tagged Pointer

    iPhone5s开始,iOS设备开始引入了64位处理器,之前的处理器一直都是32位的。

    但是在64位处理器中,指针长度以及一些变量所占内存都发生了改变,32位一个指针占用4字节,但64位一个指针占用8字节;32位一个long占用4字节,64位一个long占用8字节等,所以在64位上内存占用会多出很多。

    苹果为了优化这个问题,推出了Tagged Pointer新特性。之前一个指针指向一个地址,而Tagged Pointer中一个指针就代表一个值,以NSNumber为例。

    NSNumber *number1 = @1;
    NSNumber *number2 = @3;
    NSNumber *number3 = @54;
    
    // 输出
    (lldb) p number1
    (__NSCFNumber *) $3 = 0xb000000000000012 (int)1
    (lldb) p number2
    (__NSCFNumber *) $4 = 0xb000000000000032 (int)3
    (lldb) p number3
    (__NSCFNumber *) $5 = 0xb000000000000362 (int)54
    

    通过上面代码可以看出,使用了Tagged Pointer新特性后,指针中就存储着对象的值。例如一个值为1的NSNumber,指针就是0xb000000000000012,如果抛去前面的0xb和后面的2,中间正好就是16进制的值。

    苹果通过Tagged Pointer的特性,明显的提升了执行效率并节省了很多内存。在64位处理器下,内存占用减少了将近一半,执行效率也大大提升。由于通过指针来直接表示数值,所以没有了mallocfree的过程,对象的创建和销毁速度提升几十倍。

    isa_t

    对于对象指针也是一样,在OC1.0时代isa是一个真的指针,指向一个堆区的地址。而OC2.0时代,一个指针长度是八字节也就是64位,在64位中直接存储着对象的信息。当查找对象所属的类时,直接在isa指针中进行位运算即可,而且由于是在栈区进行操作,查找速度是非常快的。

    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
    

    例如isa_t本质上是一个结构体,如果创建结构体再用指针指向这个结构体,内存占用是很大的。但是Tagged Pointer特性中,直接把结构体的值都存储到指针中,这就相当节省内存了。

    苹果不允许直接访问isa指针,和Tagged Pointer也是有关系的。因为在Tagged Pointer的情况下,isa并不是一个指针指向另一块内存区,而是直接表示对象的值,所以通过直接访问isa获取到的信息是错误的。

    Tagged Pointer


    简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我Github上,下载Runtime PDF合集。把所有Runtime文章总计九篇,都写在这个PDF中,而且左侧有目录,方便阅读。

    Runtime PDF

    下载地址:Runtime PDF
    麻烦各位大佬点个赞,谢谢!😁

    相关文章

      网友评论

      • 小包包包:1.0的 isa 指针, 与2.0下 的 isa_t ,不太懂
        小包包包:还是说第7步的 methodizeClass 只是让class 拥有了category数组,并没有
        将􏳧Category􏰒的method 􏱢protocol 􏱢property添加到 􏵬􏲚􏴭Class,添加的过程是 第9步干的?
        小包包包:这个懂,还有一块不明白的过程,是关于类别的处理

        是有一篇讲初始化类的时候,即在realizeClass过程中,调 methodizeClass(cls);然后在methodizeClass 中,执行 // Attach categories.
        category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/); attachCategories(cls, cats, false /*don't flush caches*/);,
        这里即处理一些类别,将类别的方法加入class中

        在看类加载的过程中,_read_images 􏲸􏰓􏴾􏷪函数内部,􏳯􏳰􏱥􏲺􏰱􏷼􏼀􏲚􏰡􏰒􏰺􏱲􏲐􏱺
        7.初始化所有非懒加载的类,进行rw、ro等操作。
        这里我理解的 是上面 methodizeClass 在这里处理了,

        但是到第9步,处理所有Category,包括Class和Meta Class。
        这里才处理 Category,那7步的 methodizeClass 处理的 Categories 从哪里来的呢?我对这个先后顺序有疑问
        刘小壮:1.0的isa是指针,2.0的isa是结构体指针,自身就存储值。这个特性叫做Tagged Pointer

      本文标题:探秘Runtime - 剖析Runtime结构体

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