美文网首页iOS runtimeruntime
深入浅出 Runtime(二):数据结构

深入浅出 Runtime(二):数据结构

作者: 师大小海腾 | 来源:发表于2019-11-16 20:15 被阅读0次

    Runtime 系列文章

    深入浅出 Runtime(一):初识
    深入浅出 Runtime(二):数据结构
    深入浅出 Runtime(三):消息机制
    深入浅出 Runtime(四):super 的本质
    深入浅出 Runtime(五):具体应用
    深入浅出 Runtime(六):相关面试题

    网络配图

    目录

    • 1. objc_object
    • 2. objc_class
      2.1 class_data_bits_t
      2.2 cache_t
       2.2.1 缓存查找流程
       2.2.2 缓存添加流程
       2.2.3 缓存扩容流程
    • 3. isa指针
      3.1 isa 与 superclass 指针指向
      3.2 类对象(class)与元类对象(meta-class)
      3.3 获得 class 或者 meta-class 的方式
    • 4. method_t
      4.1 SEL
      4.2 IMP
      4.3 Type Encodings

    1. objc_object

    Objective-C的面向对象都是基于C/C++的数据结构——结构体实现的。
    我们平时使用的所有对象都是id类型,id类型对象对应到runtime中,就是objc_object结构体。

    // A pointer to an instance of a class.
    typedef struct objc_object *id;
    
    struct objc_object {
    private:
        isa_t isa;
        /*...
          isa操作相关
          弱引用相关
          关联对象相关
          内存管理相关
          ...
         */
    };
    

    2. objc_class

    Class指针用来指向一个 Objective-C 的类,它是objc_class结构体类型,所以class、meta-class底层结构都是objc_class结构体,objc_class继承自objc_object,所以它也有isa指针,它也是对象。

    // An opaque type that represents an Objective-C class.
    typedef struct objc_class *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();
        }
    };
    

    2.1 class_data_bits_t

    • class_data_bits_t主要是对class_rw_t的封装,可以通过bits & FAST_DATA_MASK获得class_rw_t
    struct class_data_bits_t {
        // Values are the FAST_ flags above.
        uintptr_t bits;
    public:
        class_rw_t* data() {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
    };
    
    • class_rw_t代表了类相关的读写信息,它是对class_ro_t的封装;
    • class_rw_t中主要存储着类的方法列表、属性列表、协议列表等;
    • class_rw_t里面的methodspropertiesprotocols都继承于list_array_tt二维数组,是可读可写的,包含了类的初始内容、分类的内容。
    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_ro_t代表了类相关的只读信息;
    • class_ro_t中主要存储着类的成员变量列表、类名等;
    • class_ro_t里面的baseMethodListbaseProtocolsivarsbaseProperties是一维数组,是只读的,包含了类的初始内容;
    • 一开始类的信息都存放在class_ro_t里,当程序运行时,经过一系列的函数调用栈,在realizeClass()函数中,将class_ro_t里的东西和分类的东西合并起来放到class_rw_t里,并让bits指向class_rw_t
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;  // instance对象占用的内存空间
    #ifdef __LP64__
        uint32_t reserved;
    #endif
        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;
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    };
    
    • method_array_tmethod_list_t
    method_array_t 与 method_list_t

    2.2 cache_t

    • 用于快速查找方法执行函数;
    • 是可增量扩展的哈希表结构,用哈希表来缓存曾经使用过的方法,可以提高方法的查找速度(空间换时间:牺牲内存空间来换取执行效率);
    • 是局部性原理的最佳应用(比如一些方法调用的频率高,存放到cache中,下一次调用这些方法的命中率就会更高些);
    • hash 函数式为 f(@selector()) = index, @selector() & _mask
    • 当我们调用过一个方法后,runtime会将这个方法缓存到cache中,下次再调用此方法的时候,runtime会优先去cache中查找。
    struct cache_t {
        struct bucket_t *_buckets;  // 哈希表
        mask_t _mask;               // 哈希表的长度 - 1
        mask_t _occupied;           // 已经缓存的方法数量
    };
    
    struct bucket_t {
    private:
        cache_key_t _key;  // SEL
        IMP _imp;          // IMP 函数的内存地址
    };
    

    2.2.1 缓存查找流程

    //objc-cache.mm(objc4)
    bucket_t * cache_t::find(cache_key_t k, id receiver)  // 根据 k 即 @selector 进行查找
    {
        assert(k != 0);
    
        bucket_t *b = buckets();          // 获取_buckets
        mask_t m = mask();                // 获取_mask
        mask_t begin = cache_hash(k, m);  // 计算起始索引
        mask_t i = begin;
        do {
            // 根据索引 i 从 _buckets 哈希表中取值
            // 如果取出来的 bucket_t 的 _key = 0,说明在索引的位置上还没有缓存过方法,返回该 bucket_t,中止缓存查询,用于 cache_fill_nolock() 函数
            // 如果取出来的 bucket_t 的 _key = k,说明查询成功,返回该 bucket_t
            if (b[i].key() == 0  ||  b[i].key() == k) {
                return &b[i];
            }
          // 在 __arm64__ 下将索引 i -1,继续查找,反向遍历 _buckets 哈希表
          // 直到 i 指向首个元素即索引 = 0 时,将 mask 赋值给 i,使其指向哈希表最后一个元素,继续反向遍历
          // 如果此时还没有找到 k 对应的 bucket_t ,或者是空的 bucket_t ,则循环结束,查找失败,调用 bad_cache() 函数
          // 接下来去类对象中 class_rw_t 中的 methods 查找
        } while ((i = cache_next(i, m)) != begin);
    
        // hack
        Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
        cache_t::bad_cache(receiver, (SEL)k, cls);
    }
    
    
    static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
    {
        return (mask_t)(key & mask);
    }
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        // return (i+1) & mask;  // __arm__  ||  __x86_64__  ||  __i386__
        return i ? i-1 : mask;   // __arm64__
    }
    

    2.2.2 缓存添加流程

    //objc-cache.mm(objc4)
    static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
    {
        cacheUpdateLock.assertLocked();
    
        // Never cache before +initialize is done
        if (!cls->isInitialized()) return;   // 如果类还未初始化,直接返回
    
        // Make sure the entry wasn't added to the cache by some other thread 
        // before we grabbed the cacheUpdateLock.
        if (cache_getImp(cls, sel)) return;  // 可能有其它线程抢先将该方法缓存了,所以要检查一次缓存,如果存在,直接返回
    
        cache_t *cache = getCache(cls);  // ⚠️取出该 class 的 cache_t
        cache_key_t key = getKey(sel);   // ⚠️根据 sel 获得 _key
    
        // Use the cache as-is if it is less than 3/4 full
        mask_t newOccupied = cache->occupied() + 1;  // 将 cache_t 的 _occupied 即已经缓存的方法数量 + 1,这里只是为了判断 +1 后缓存容量是否满
        mask_t capacity = cache->capacity();  // 获得缓存容量 = _mask + 1
        if (cache->isConstantEmptyCache()) {  // 如果缓存是只读的,重新申请缓存空间
            // Cache is read-only. Replace it.
            cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);  // 申请新的缓存空间,并释放旧的
        }
        else if (newOccupied <= capacity / 4 * 3) {  // ⚠️如果当前已经缓存的方法数量 +1 <= 缓存容量的 3/4,就继续往下操作
            // Cache is less than 3/4 full. Use it as-is.
        }
        else {  // ⚠️如果以上条件不满足,说明缓存已满,进行缓存扩容
            // Cache is too full. Expand it.
            cache->expand();
        }
    
        // Scan for the first unused slot and insert there.     // 扫描第一个未使用的插槽(bucket_t)并将其插入
        // There is guaranteed to be an empty slot because the  // 必然会有一个空的插槽(bucket_t)
        // minimum size is 4 and we resized at 3/4 full.        // 因为最小大小是4,我们调整为3/4满
        bucket_t *bucket = cache->find(key, receiver);       // ⚠️调用 find() 函数进行一次缓存查找,必然会得到一个空的 bucket_t
        if (bucket->key() == 0) cache->incrementOccupied();  // ⚠️如果该 bucket_t 为空,将 _occupied 即已经缓存的方法数量 + 1
        bucket->set(key, imp);  // ⚠️添加缓存
    }
    
    void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
    {
    #if !DEBUG_TASK_THREADS
        mutex_locker_t lock(cacheUpdateLock);
        cache_fill_nolock(cls, sel, imp, receiver);
    #else
        _collecting_in_critical();
        return;
    #endif
    }
    

    2.2.3 缓存扩容流程

    • ① 设置新的缓存bucket_t,容量 = 旧的两倍;
    • ② 设置新的_mask=bucket_t长度 - 1;
    • ③ 释放旧的缓存(在runtime动态交换方法实现时也会释放缓存)。
    //objc-cache.mm(objc4)
    void cache_t::expand()
    {
        cacheUpdateLock.assertLocked();
        
        uint32_t oldCapacity = capacity();
        // ⚠️将缓存扩容为原来的两倍,如果是首次调用,设置缓存容量的初始值为 4
        uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    
        if ((uint32_t)(mask_t)newCapacity != newCapacity) {
            // mask overflow - can't grow further
            // fixme this wastes one bit of mask
            newCapacity = oldCapacity;
        }
    
        reallocate(oldCapacity, newCapacity);  // ⚠️申请新的缓存空间,并释放旧的
    }
    
    enum {
        INIT_CACHE_SIZE_LOG2 = 2,
        INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
    };
    
    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
    {
        bool freeOld = canBeFreed();  // ⚠️判断一下缓存是不是空的,如果为空,就没必要释放空间
    
        bucket_t *oldBuckets = buckets();
        bucket_t *newBuckets = allocateBuckets(newCapacity);
    
        // Cache's old contents are not propagated. 
        // This is thought to save cache memory at the cost of extra cache fills.
        // fixme re-measure this
    
        assert(newCapacity > 0);
        assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    
        setBucketsAndMask(newBuckets, newCapacity - 1);
        
        if (freeOld) {
            cache_collect_free(oldBuckets, oldCapacity);
            cache_collect(false);
        }
    }
    
    bool cache_t::canBeFreed()
    {
        return !isConstantEmptyCache();
    }
    
    bool cache_t::isConstantEmptyCache()
    {
        return 
            occupied() == 0  &&  
            buckets() == emptyBucketsForCapacity(capacity(), false);
    }
    

    3. isa 指针

    • isa指针用来维护对象和类之间的关系,并确保对象和类能够通过isa指针找到对应的方法、实例变量、属性、协议等;
    • 在 arm64 架构之前,isa就是一个普通的指针,直接指向objc_class,存储着ClassMeta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;
    • 从 arm64 架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西,其中的 33 位才是拿来存储classmeta-class对象的内存地址信息。要通过位运算将isa的值& ISA_MASK掩码,才能得到classmeta-class对象的内存地址。
    struct objc_object {
        Class isa;  // 在 arm64 架构之前
    };
    
    struct objc_object {
    private:
        isa_t isa;  // 在 arm64 架构开始
    };
    
    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  // 用来取出 Class、Meta-Class 对象的内存地址
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
        struct {
            uintptr_t nonpointer        : 1;  // 0:代表普通的指针,存储着 Class、Meta-Class 对象的内存地址
                                              // 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;  // 如果为1,代表引用计数过大无法存储在 isa 中,那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)哈希表中
            uintptr_t extra_rc          : 19; // 里面存储的值是引用计数 retainCount - 1
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
        };
    };
    

    3.1 isa 与 superclass 指针指向

    isa 与 superclass 指针指向

    3.2 类对象(class)与元类对象(meta-class)

    • classmeta-class底层结构都是objc_class结构体,objc_class继承自objc_object,所以它也有isa指针,所以它也是对象;
    • class中存储着实例方法、成员变量、属性、协议等信息,
      meta-class中存储着类方法等信息;
    • isa指针和superclass指针的指向(如上图);
    • 基类的meta-classsuperclass指向基类的class
      决定了一个性质:当我们调用一个类方法,会通过classisa指针找到meta-class,在meta-class中查找有无该类方法,如果没有,再通过meta-classsuperclass指针逐级查找父meta-class,一直找到基类的meta-class如果还没找到该类方法的话,就会去找基类的class中同名的实例方法的实现。

    3.3 获得 class 或者 meta-class 的方式

    • 获得 class 有 3 种方式
    - (Class)class;
    + (Class)class;
    Class object_getClass(id obj);  // 传参:instance 对象
    
    • 获得 meta-class 只有 1 种方式
    Class object_getClass(id obj);  // 传参:Class 对象
    

    示例代码如下

        NSObject *object1 = [NSObject alloc] init];
        NSObject *object2 = [NSObject alloc] init];
        // objectClass1 ~ objectClass5 都是 NSObject 的类对象
        Class objectClass1 = [object1 class];
        Class objectClass2 = [object2 class];
        Class objectClass3 = [NSObject class];
        Class objectClass4 = object_getClass(object1);
        Class objectClass5 = object_getClass(object2);  
        // objectMetaClass1 ~ objectMetaClass4 都是 NSObject 的元类对象
        Class objectMetaClass1 = object_getClass([object1 class];    
        Class objectMetaClass2 = object_getClass([NSObject class]);    
        Class objectMetaClass3 = object_getClass(object_getClass(object1));    
        Class objectMetaClass4 = object_getClass(objectClass5);    
    

    方法实现

    - (Class)class {
        return object_getClass(self);
    }
    
    + (Class)class {
        return self;
    }
    
    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    objc_object::getIsa() 
    {
        if (!isTaggedPointer()) return ISA();
        ......
    }
    objc_object::ISA() 
    {
        assert(!isTaggedPointer()); 
    #if SUPPORT_INDEXED_ISA
        if (isa.nonpointer) {
            uintptr_t slot = isa.indexcls;
            return classForIndex((unsigned)slot);
        }
        return (Class)isa.bits;
    #else
        return (Class)(isa.bits & ISA_MASK);
    #endif
    }
    #if __ARM_ARCH_7K__ >= 2
    #   define SUPPORT_INDEXED_ISA 1
    #else
    #   define SUPPORT_INDEXED_ISA 0
    #endif
    

    3.4 为什么要设计 meta-class ?

    目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则。

    4. method_t

    • Method就是method_t类型的指针;
    • method_t是对方法/函数的封装(函数四要素:函数名、返回值、参数、函数体)。
    typedef struct method_t *Method;
    
    struct method_t {
        SEL name;  // 方法名
        const char *types;  // 编码(返回值类型、参数类型)
        IMP imp;   // 方法的地址/实现
    };
    

    4.1 SEL

    • SEL 又称“选择器”,它是一个指向方法的selector的指针,代表方法/函数名;
    • SEL 维护在一个全局的 Map 中,所以它是全局唯一的,不同类中相同名字的方法的 SEL 是相同的。
    typedef struct objc_selector *SEL;
    
    • SEL 可以通过以下方式获得
        SEL sel1 = @selector(selector);
        SEL sel2 = sel_registerName("selector");
        SEL sel3 = NSSelectorFromString(@"selector");
    
    • SEL 可以通过以下方式转换成字符串
        char *string1 = sel_getName(sel1);
        NSString *string2 = NSStringFromSelector(sel1);
    

    4.2 IMP

    • IMP 是指向方法实现的函数指针;
    • 我们调用方法,实际上就是根据方法 SEL 查找 IMP;
    • method_t实际上相当于在 SEL 和 IMP 之间做了一个映射。
    #if !OBJC_OLD_DISPATCH_PROTOTYPES
    typedef void (*IMP)(void /* id, SEL, ... */ ); 
    #else
    typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
    #endif
    

    4.3 Type Encodings

    • Type Encodings 编码技术就是配合runtime的技术,把一个方法的返回值类型、参数类型通过字符串的形式描述;
    • @encode()指令可以将类型转换为 Type Encodings 字符串编码,
      @encode(int)=i
    • OC方法都有两个隐式参数,方法调用者(id)self和方法名(SEL) _cmd,所以我们才能在方法中使用self_cmd
    • -(void)test,它的编码为“v16@0:8”,可以简写为“v@:
      v:代表返回值类型为 void
      @:代表参数 1 类型为 id
      ::代表参数 2 类型为 SEL
      16:代表所有参数所占的总字节数
      0:代表参数 1 从第几个字节开始存储
      8:代表参数 2 从第几个字节开始存储
    • 下图为类型对应的 Type Encodings 编码:
    Objective-C type encodings
    • Type Encodings 在runtime的消息转发中会使用到;
    • 更多关于 Type Encodings 的内容,可以查看官方文档 Type Encodings

     

    下一篇

    深入浅出 Runtime(三):消息机制

    相关文章

      网友评论

        本文标题:深入浅出 Runtime(二):数据结构

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