美文网首页
Objective-C Runtime —— 基础数据结构

Objective-C Runtime —— 基础数据结构

作者: Beyond_JG | 来源:发表于2018-05-18 18:29 被阅读84次

    iOS工作两年了,几乎每次面试都会被问到Runtime的东西,自己一直没有系统的学习过Runtime。正好这段时间比较闲,自己也想深入的学习下iOS。所以那就先了解下基础的东西吧。以前听到Runtime一直感觉很高深,什么黑魔法啊一堆名词,其实去了解下的话,没有那么神奇。这篇文章分三大部分来介绍Runtime,是一个循序渐进的过程。第一呢,我们需要了解Objective-C的对象模型,然后才能去理解消息发送的过程,再然后再去了解下Runtime都能干什么,毕竟如果一个技术没有落地的应用,那它就没啥价值了。这篇文章基于objc4-723源码进行分析,GitHub有一个可调试库,地址

    简介

    Runtime是什么?官方文档第一段就给出了答案。

    The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.

    其实Runtime就是一个运行时库,我们在编译期调用的函数最终都会通过Runtime库去进行分发。Runtime的核心就是发送消息。Objective-C从三种层级和Runtime交互。

    1. Objective-C源代码。

      一般情况,我们只需编写Objective-C代码,Runtime会默默的工作。当编译包含Objective-C代码时,编译器会创建实现动态特性的数据结构和函数调用。主要的函数就是发送消息。
    2. Runtime函数。

      Runtime系统是由一系列函数和数据结构构成的,它有一系列公共接口。头文件在/usr/include/objc目录下。许多函数允许你用C函数来实现Objective-C中的功能。但是谁会去费这个力气呢?一般在写一些与其它语言桥接或者debug时会用到。在Objective-C Runtime Reference中有对函数的详细解释。
    3. NSObject方法。

      在写Objective-C时,我们的对象大多数都继承自NSObject(NSProxy除外)。所以,我们可以通过NSObject的一些方法来与Runtime交互。比如:NSObject提供了一系列自省方法来查询Runtime信息。
    - (BOOL)isKindOfClass:(Class)aClass;
    - (BOOL)isMemberOfClass:(Class)aClass;
    - (BOOL)conformsToProtocol:(Protocol *)aProtocol;
    - (BOOL)respondsToSelector:(SEL)aSelector;
    - (Class)superclass;
    + (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead");
    

    基础数据结构

    类和对象的关系

    由于我们在Objective-C中大多数都继承NSObject类,那我们就以NSObject为切入点来分析对象的数据结构。NSObject定义如下:

    @interface NSObject <NSObject> {
        Class isa  OBJC_ISA_AVAILABILITY;
    }
    

    我们看到它有一个Class变量。那我们在看一下Class的定义:

    /// An opaque type that represents an Objective-C class.
    typedef struct objc_class *Class;
    

    可以看到他是一个objc_class指针,那objc_class是什么呢?在objc-runtime-new.h中找到了objc_class的定义,下面是一个简化版的定义:

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() { 
            return bits.data();
        }
    };
    

    我们可以看到objc_class是一个继承objc_object的结构体,下面是一个简化版objc_object定义,在文件中,我们还发现了id的定义。

    struct objc_object {
    private:
        isa_t isa;
    };
    typedef struct objc_object *id;
    

    从上面可以看出来,id是一个objc_object类型的指针,以前一直以为id和NSObject *等价,好无知啊。从上面看NSObject和objc_object的定义很像,只不过NSObject的isa是一个Class,而objc_object的isa是一个isa_t。下面,我们来看下isa_t是一个什么东西。isa_t定义如下:

    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    
    #if SUPPORT_PACKED_ISA
    
        // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
        // nonpointer must be the LSB (fixme or get rid of it)
        // shiftcls must occupy the same bits that a real class pointer would
        // bits + RC_ONE is equivalent to extra_rc + 1
        // RC_HALF is the high bit of extra_rc (i.e. half of its range)
    
        // future expansion:
        // uintptr_t fast_rr : 1;     // no r/r overrides
        // uintptr_t lock : 2;        // lock for atomic property, @synch
        // uintptr_t extraBytes : 1;  // allocated with extra bytes
    
    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
        struct {
            uintptr_t nonpointer        : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
            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__
    #   define ISA_MASK        0x00007ffffffffff8ULL
    #   define ISA_MAGIC_MASK  0x001f800000000001ULL
    #   define ISA_MAGIC_VALUE 0x001d800000000001ULL
        struct {
            uintptr_t nonpointer        : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
            uintptr_t magic             : 6;
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;
            uintptr_t extra_rc          : 8;
    #       define RC_ONE   (1ULL<<56)
    #       define RC_HALF  (1ULL<<7)
        };
    

    可以看出isa_t是一个union类型结构体,也就是说isa_t,cls,bits和结构体共用一块内存结构。isa总共占64为内存结构(取决于结构体)。好了,把上面的源码转换成类图就是下面这样子了。


    图片来自冰霜的blog

    图片来自冰霜的blog

    从上面源码我们可以看出,Objective-C中的对象都是一个objc_object结构体,objc_class继承objc_object,所以在Objective-C中,类也是一个对象。在objc_class中,除了isa还有superclass,cache,bits三个变量,分别表示父类指针,方法缓存,还有一个是类的数据区域。object和NSObject都包含一个objc_class的isa。

    类的isa指向的是什么呢?这就引出了元类(meta-class)的概念。关于元类可以看这篇文章What is a meta-class in Objective-C?

    为什么会有元类呢?

    因为在Objective-C中,对象的方法并没有存储在对象的结构体中(如果每个对象都存储自己的方法,那我们程序中无数对象就都要存储自己的方法,那内存肯定就不够用了)。当我们调用实例方法时,它通过自己的isa查找到对应的类,然后在class_data_bits_t结构体中查找对应的方法实现。每一个objc_class也有一个superClass指向自己的父类,可以查找到继承的方法。那么如果调用实例方法怎么查找呢?这时,就需要引入元类来保证无论调用类方法和实例方法,都可以以相同的方式来查找。

    • 调用实例方法,通过对象的isa在类中获取方法。
    • 调用类方法,通过类的isa在元类中获取方法。

    类和元类的关系如下图:


    类和元类

    图片来自Classes and metaclasses

    isa指针

    isa初始化

    下面我们来一步步解析类结构,先从isa指针来看起。上面我们说了isa是一个isa_t的union。那么我们看下isa是如何初始化的。

    inline void 
    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
    {
        assert(!cls->instancesRequireRawIsa());
        assert(hasCxxDtor == cls->hasCxxDtor());
    
        initIsa(cls, true, hasCxxDtor);
    }
    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
    { 
        assert(!isTaggedPointer()); 
        
        if (!nonpointer) {
            isa.cls = cls;
        } else {
            assert(!DisableNonpointerIsa);
            assert(!cls->instancesRequireRawIsa());
    
            isa_t newisa(0);
    
    #if SUPPORT_INDEXED_ISA
            assert(cls->classArrayIndex() > 0);
            newisa.bits = ISA_INDEX_MAGIC_VALUE;
            // isa.magic is part of ISA_MAGIC_VALUE
            // isa.nonpointer is part of ISA_MAGIC_VALUE
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.indexcls = (uintptr_t)cls->classArrayIndex();
    #else
            newisa.bits = ISA_MAGIC_VALUE;
            // isa.magic is part of ISA_MAGIC_VALUE
            // isa.nonpointer is part of ISA_MAGIC_VALUE
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.shiftcls = (uintptr_t)cls >> 3;
    #endif
            isa = newisa;
        }
    }
    

    这里调用initIsa的第二个参数nonpointer传入了true,所以会直接进入else。

    关于SUPPORT_INDEXED_ISA应该是对watchOS的优化,所以会走到#else

    我们看下isa_t内存布局:


    image

    图片来自霜神blog

    ISA_MAGIC_VALUE = 0x000001a000000001ULL,转换成二进制是11010000000000000000000000000000000000001,所以把ISA_MAGIC_VALUE赋值给bits,isa_t的结构如下图:


    image

    图片来自霜神blog

    从上图可以看出,调用newisa.bits = ISA_MAGIC_VALUE;这句代码是给nonpointer和magic进行了赋值。

    nonpointer和magic

    • nonpointer表示是否开启指针优化,1表示开启指针优化,0表示一个raw isa,也就是isa_t这个union没有结构体部分,访问对象的isa直接返回cls指针。不过现在都会开启指针优化。关于这个指针优化,后面会有一篇单独的文章来分析,就是关于Tagged Pointer技术。
    • magic判断对象是否初始化完成,在arm64中0x16是调试器判断当前对象是真的对象还是没有初始化的空间。

    has_cxx_dtor

    经过newisa.has_cxx_dtor = hasCxxDtor;这段代码,会设置isa_t的has_cxx_dtor,这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。设置完后结构如下:

    image

    图片修改自霜神blog

    shiftcls

    通过上面两行代码设置了nonpointer,magic,has_cxx_dtor。最后一行代码把当前对象的类指针存入了isa中。

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

    为什么右移三位呢,引用Draveness的blog中一段话来解释下:

    将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。

    绝大多数机器的架构都是 byte-addressable 的,但是对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 000,我们只会用其中的 30 位来表示对象的地址。

    Objective-C中类的指针后三位也是0,在_class_createInstanceFromZone中打印类的地址可以看到最后一位不是0就是8,所以类的指针后三位是0。

    image
    所以上面cls>>3没有问题。我们尝试运行下面代码来形象说明下,理解的可以更加深刻。
    NSString *binaryWithInteger(NSUInteger decInt) {
        NSString *string = @"";
        NSUInteger x = decInt;
        while (x > 0) {
            string = [[NSString stringWithFormat:@"%lu", x&1] stringByAppendingString:string];
            x = x >> 1;
        }
        return string;
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            struct objc_object *object = (__bridge struct objc_object *)([NSObject new]);
            NSLog(@"%@", binaryWithInteger(object->isa));
            NSLog(@"%@", binaryWithInteger([NSObject class]));
        }
        return 0;
    }
    //打印结果
    object_isa 0000000001011101100000000000000100000000011101000000000100111001 // 补全64位
    class  100000000011101000000000100111000
    
    

    打印出来object的isa的内容如下图所示:


    image

    图片修改自霜神blog

    其中浅蓝色部分是类指针,和上面打印的class指针右移三位一样。这就验证了我们之前对于初始化isa是设置了nonpointer,magic,shiftclass的分析。我们也可以通过这种方式分析上面类结构那个图,验证下NSObject的isa是指向NSObject元类,NSObject元类的isa指向自己,superclass指向NSObject类对象。感兴趣的可以自己验证下,在这里就不去分析了。

    isa总结

    下面我们总结下isa是如何初始化的。首先把ISA_MAGIC_VALUE赋值给bits设置了nonpoint和magic,然后设置has_cxx_dtor,最后把类指针右移三位设置给shiftcls。下面我们集中说一下结构体所有位的作用。

    代表信息
    nonpointer 0 表示普通的 isa 指针,1 表示使用优化
    has_assoc 表示该对象是否包含 associated object,如果没有,则析构时会更快
    has_cxx_dtor 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
    shiftcls 类的指针
    magic 固定值,用于在调试时分辨对象是否未完成初始化
    weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
    deallocating 表示该对象是否正在析构
    has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
    extra_rc 存储引用计数值减一后的结果

    解释来自http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html

    cache_t

    cache_t就比较简单了,先来看下具体代码

    struct cache_t {
        struct bucket_t *_buckets;
        mask_t _mask;
        mask_t _occupied;
    }
    struct bucket_t {
    private:
        cache_key_t _key;
        IMP _imp;
    }
    
    • _buckets是一个散列表,用来存储方法缓存。bucket_t是一个key和IMP的结构体。
    • _mask分配bucket的总数。
    • _occupied表示目前占用缓存bucket的个数。

    Cache的作用主要是优化方法调用的性能,当对象收到消息后,会优先去Cache中查找,如果Cache中没有才会去类的方法列表找。如果没有Cache,我们每次调用方法都去类的方法找,会严重影响性能。

    class_data_bits_t

    class_data_bits_t结构体解析

    在类的结构体中,isa我们已经解释过了,superclass是一个指向父类的指针,cache是为了加速方法调用,下面我们来分析一下class_data_bits_t,一个类的方法,属性和遵循的协议都在这个里面。我们先看下代码:

    struct class_data_bits_t {
    
        // Values are the FAST_ flags above.
        uintptr_t bits;
    }
    

    它只有一个64位bits用来存储和类有关的信息。它有一个data()方法来返回class_rw_t *指针。

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

    其中FAST_DATA_MASK = 0x00007ffffffffff8UL,bits与FAST_DATA_MASK位运算,取bits的[3,47]位转成class_rw_t。

    在 x86_64 架构上,Mac OS 只使用了其中的 47 位来为对象分配地址。而且由于地址要按字节在内存中按字节对齐,所以掩码的后三位都是 0。

    因为class_rw_t *指针只存于[3,47],所以后面三位可以存储当前类的的其他信息。


    image

    图片来自深入解析 ObjC 中方法的结构

    // class is a Swift class
    #define FAST_IS_SWIFT           (1UL<<0)
    // class or superclass has default retain/release/autorelease/retainCount/
    //   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
    #define FAST_HAS_DEFAULT_RR     (1UL<<1)
    // class's instances requires raw isa
    #define FAST_REQUIRES_RAW_ISA   (1UL<<2)
    
    • FAST_IS_SWIFT 用于判断 Swift 类
    • FAST_HAS_DEFAULT_RR 当前类或者父类含有默认的 retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法
    • FAST_REQUIRES_RAW_ISA 当前类的实例需要 raw isa

    当调用objc_class的data()方法也会返回class_rw_t *指针,这个方法也是调用了class_data_bits_t的data()方法。

    class_rw_t和class_ro_t

    我们来看下class_rw_t的结构:

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        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_rw_t包含一个class_ro_t *指针,class_ro_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_rw_t和class_ro_t的关系,然后再来验证。在编译期间,类结构中的class_data_bits_t *data指向的是class_ro_t *指针,用类图来表示一下:


    image

    图片来自深入解析 ObjC 中方法的结构

    然后在运行时会调用realizeClass方法:

    ro = (const class_ro_t *)cls->data(); // 调用class_data_bits_t的data()方法,强制转换成class_ro_t *指针。
    class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); 初始化class_rw_t结构体。
    rw->ro = ro; //设置ro
    rw->flags = RW_REALIZED|RW_REALIZING; //设置flag
    cls->setData(rw); // 把class_rw_t设置为class_data_bits_t旳data
    

    经过上面的方法类图会变为下面这样的:


    image

    图片来自深入解析 ObjC 中方法的结构

    代码运行到这里class_rw_t中的方法,属性和协议列表是空的。这时需要调用methodizeClass方法来将类自己实现的方法(包括分类),属性,协议加载到method,property,protocol列表中。

    验证

    验证类在编译期结构

    下面我们来分析一下TestObject在初始化过程中内存的修改,下面是TestObject的代码:

    //TestObject.h
    #import <Foundation/Foundation.h>
    
    @interface TestObject : NSObject
    -(void)sayHello;
    @end
    
    #import "TestObject.h"
    
    //TestObject.m
    @implementation TestObject
    - (void)sayHello
    {
        NSLog(@"Hello");
    }
    @end
    
    //main.m
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Class cls = [TestObject class];
            NSLog(@"%p\n",cls);
        }
        return 0;
    }
    

    因为类在内存中的地址是在编译期确定的,只要代码不变,类在内存中的位置不变。我们先来运行一次拿到类在内存中的位置。下面是打印结果:

    0x100002750
    

    接下来在_objc_init方法打一个断点,这个方法是运行时初始化之前调用的。


    image

    然后在lldb中输入下面的命令:


    image
    通过上面的图我们可以看到,在class_ro_t中,只有baseMethodList和name有值,其他的都没值。因为类里面只有一个方法,没有属性,协议什么的。下面我们看下baseMethodList里面的内容:
    image

    我们通过$5->get(0)可以获取到一个method_t结构体,可以看到就是我们在类中的sayHello方法。

    验证realizeClass

    下面我们来验证下realizeClass的作用,该方法的主要作用就是对类进行第一次初始化,包括:

    • 分配可读写内存空间
    • 返回真正的类结构
    static Class realizeClass(Class cls)
    

    我们需要在这个方法打一个条件断点:

    image
    这里使用指针相等的方式,来确定当前类是TestObject,之所以不用[NSStringFromClass(cls) isEqualToString:@"TestObject"],是因为在这个时间点上这些方法还不能用。
    image
    这个断点设置在这里,因为TestObject是一个正常类,所以会走else方法。运行代码时,因为每个类都会走这个方法,只有类是TestObject时才会进断点,所以可能会等会才进断点。
    在这时打印类结构,其中布局依然会是下图这样:
    image

    图片来自深入解析 ObjC 中方法的结构

    当运行完else中代码,我们再来打印下结构:

    image
    通过打印可以看出来,当运行完else中代码,类的class_ro_t和class_rw_t都被正确设置了,但是class_rw_t中的methods,properties和protocols均未设置,这些将在methodizeClass中设置。
    下面我们就去验证下:
    image
    当代码运行到这一行,我们再执行下面的命令:
    image
    在这里调用了method_array_tattachLists方法,将baseMethods中的方法添加到methods数组之后。我们访问methods才会获取当前类的实例方法。

    list_array_tt和method_t

    list_array_tt

    当我们再看class_rw_t时,发现它们的方法,属性,协议列表分别是method_list_t,property_array_t和protocol_array_t。他们都继承自list_array_tt。list_array_tt是一个泛型结构体,用于存储一些元数据,其实是一个二维数组。

    template <typename Element, typename List>
    class list_array_tt {
        struct array_t {
            uint32_t count;
            List* lists[0];
        };
    }
    class method_array_t : 
        public list_array_tt<method_t, method_list_t>
    

    其中Element表示元数据类型,比如method_t,而List表示用来转载元数据的一维数组,比如method_list_t。

    list_array_tt有三种状态:

    1. 自身为空,可以类比为[[]]
    2. 它只有一个指针,指向一个元数据的集合,可以类比为[[1, 2]]
    3. 它有多个指针,指向多个元数据的集合,可以类比为[[1, 2], [3, 4]]
      当一个类刚创建时,它可能处于1,2状态,如果使用class_addMethod或者category添加方法,就会进入3状态,而且一旦进入3状态,就再也不可能回到其他状态。即使新增的方法移除掉
    method_t

    下面来看下method_t的结构,method还是比较简单的,都比较容易理解。有关类型可以看官方文档的Type Encodings

    struct method_t {
        SEL name; // 方法名
        const char *types; // 类型   
        IMP imp; // 实现指针IMP
    }
    

    我们上面TestObject的method_t就是下面这样的:

    name = "sayHello"
    types = 0x0000000100001f9b "v16@0:8"
    imp = 0x0000000100001d10 (debug-objc`-[TestObject sayHello] at TestObject.m:11)
    

    总结

    1. isa是一个union,里面包含一个结构体。每一个字段代表一段信息,其中它的类信息存在于shiftcls中。我们可以通过object_getClass()函数获取一个对象的类信息。这里需要注意一点,对象的class实例方法会调用object_getClass()方法,所以对于实例方法object_getClass()class等价,但是同样还有一个class类方法,这个方法返回self,直接返回类对象。
    2. 类的内存中的位置在编译期间就决定了,在之后修改代码,也不会改变内存中的位置。
    3. 类的方法在编译期间存在于class_ro_t中,直到通过realizeClass方法后才放到class_rw_t指向的class_ro_t中,这样我们在运行时为class_rw_t添加方法,也不会影响类的只读结构。
    4. 在class_ro_t中的属性运行时就不会改变了,运行时添加方法,会修改class_rw_t的方法属性,对于方法添加,我们在消息发送的文章再来分析。
     +(Class)class {
        return self;
     }
     -(Class)class {
        return object_getClass(self);
     }
    

    参考资料

    相关文章

      网友评论

          本文标题:Objective-C Runtime —— 基础数据结构

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