美文网首页
Objective-C 类的底层探索

Objective-C 类的底层探索

作者: 顶级蜗牛 | 来源:发表于2022-04-25 17:23 被阅读0次

    苹果官方资源opensource
    objc4-838可编译联调源码

    上一章节说到了类的对象的前8个字节是isa结构体指针,它指向的是类对象。这章节就来探究类的底层。

    本章节研究类对象的底层探索:
    1.类的本质
    2.isa的走向
    3.元类的继承链
    4.内存平移
    5.objc_class的bits成员里有什么?
    6.类的实例方法、类方法存储在哪里?
    7.苹果为什么要设计元类?
    8.获取类的成员变量列表
    9.ro、rw、rwe之间的关系
    10.objc_class的cache成员里有什么?
    11.cache_t结构解析
    12.cache的扩容规则

    一、类的本质

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Class class1 = [MyPerson class];
            Class class2 = [MyPerson alloc].class;
            Class class3 = object_getClass([MyPerson alloc]);
            NSLog(@"%p--%p--%p", class1, class2, class3);
            // 0x100008400--0x100008400--0x100008400
        }
        return 0;
    }
    

    有三种方式可以获取类对象,并且它们的地址是一样的,可以得出:
    类对象在内存里有且只存在一个。

    可以看到类对象其实是Class类型的,找到objc4源码中Class的声明:

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

    于是我继续找到objc_class的声明:

    objc_class

    类的本质就是objc_class
    为什么说类对象是一个对象呢?首先objc_class继承自objc_object,所以objc_object也拥有一个isa成员变量,对象的本质就是objc_object

    二、isa的走向

    类的实例isa是指向类对象,那么类对象也有isa是指向哪里呢?

    我创建一个MyPerson实例,输出他们之间的地址关系

    ps: 注意ISA_MASK是区分框架的,在源码里可以找到。
    可以发现po类对象地址 和 po元类对象地址 都叫MyPerson,但他俩的内存地址不一样,所以不是一个东西。

    实例中找到isa,实例isa & ISA_MASK = 类对象
    类对象找到isa,类对象isa & ISA_MASK = 元类对象
    元类对象找到isa, 元类对象isa & ISA_MASK = 根元类对象
    根元类对象找到isa, 根元类对象isa & ISA_MASK = 根元类对象

    可以发现po根源类的地址也叫NSObject,但是又跟NSObject类对象地址不一样,说明它们也不是一个东西。

    NSObject类对象isa也指向根元类对象

    结论:
    类的实例isa --> 类对象;
    类对象isa --> 元类对象;
    元类对象isa --> 根元类对象;
    根元类对象isa --> 根元类对象自己。

    类对象和元类对象不是一个东西;根元类和根类不是一个东西。

    注意:
    isa是一个结构体指针,只是用来存储内存地址的;并且类对象、元类和根元类并没有引用计数和弱引用相关的,它们的isa不是和实例的nonPointerIsa一样的东西。(不懂实例的nonPointerIs进入快门

    三、元类的继承链

    创建一个MyTeacher类,继承自MyPerson。看看MyTeacher、MyPerson、NSObject它们的元类之间的继承关系:

    // MARK: - 分析元类继承链
    void MetaInherit(void) {
        // NSObject实例对象
        NSObject *object = [NSObject alloc];
        // NSObject类对象
        Class class = object_getClass(object);
        // NSObject元类(根元类)
        Class metaClass = object_getClass(class);
        NSLog(@"NSObject实例对象: %p", object);
        NSLog(@"NSObject类对象: %p", class);
        NSLog(@"NSObject元类(根元类): %p", metaClass);
        
        // NSObject元类(根元类)的父类
        Class metaClassSuper = class_getSuperclass(metaClass);
        NSLog(@"NSObject元类(根元类)的父类: %@--%p", metaClassSuper, metaClassSuper);
        
        NSLog(@"-----------------------------------");
        
        // MyPerson元类
        Class pMetaClass = objc_getMetaClass("MyPerson");
        // MyPerson元类的父类
        Class pMetaClassSuper = class_getSuperclass(pMetaClass);
        NSLog(@"MyPerson元类: %@ - %p", pMetaClass, pMetaClass);
        NSLog(@"MyPerson元类的父类: %@ - %p", pMetaClassSuper, pMetaClassSuper);
        
        NSLog(@"-----------------------------------");
        
        // ps: MyTeacher继承自MyPerson
        // MyTeacher元类
        Class tMetaClass = objc_getMetaClass("MyTeacher");
        Class tMetaClassSuper = class_getSuperclass(tMetaClass);
        NSLog(@"MyTeacher元类: %@ - %p", tMetaClass, tMetaClass);
        NSLog(@"MyTeacher元类的父类: %@ - %p", tMetaClassSuper, tMetaClassSuper);
    }
    

    MyTeacher元类的父类,就是MyPerson元类,而MyPerson元类的父类就是NSObject元类(根元类);NSObject元类(根元类)的父类就是NSObject类对象。

    // NSObject类对象
    Class class = object_getClass([NSObject alloc]);
    // NSObject父类是nil
    Class a = class_getSuperclass(class);
    NSLog(@"%@", a); // nil   
    

    结论:
    一个类的元类的父类就是那个类的父类的元类
    NSObject特殊:NSObject元类(根元类)的父类就是NSObject类对象。

    针对第二、第三节总结出官方isa走位图

    isa走位图

    四、内存平移

    int a[4] = {10, 20, 30, 40}; // a看成是数组指针
    int *b = a; // b指向了a
    NSLog(@"%p - %p  - %p - %p", &a, &a[1], &a[2], &a[3]);
    NSLog(@"%p - %p  - %p - %p", b, b+1, b+2, b+3);
    
    for (int i=0; i<4; i++) {
        int value = *(b+i);
        NSLog(@"%d", value);
    }
    
    0x7ff7bfeff2e0 - 0x7ff7bfeff2e4  - 0x7ff7bfeff2e8 - 0x7ff7bfeff2ec
    0x7ff7bfeff2e0 - 0x7ff7bfeff2e4  - 0x7ff7bfeff2e8 - 0x7ff7bfeff2ec
    10
    20
    30
    40
    

    b+1 也就是在b的首地址上平移了4个字节,因为这是一个int数组指针。可理解为b+1就是找到下一个指针,*(b+1) 取出下一个指针地址的内容。

    五、objc_class的bits成员里有什么

    类对象的本质是objc_class,其成员里有isa、superclass、cache、bits。那我是不是可以通过内存平移的方式,得到bits的首地址?

    #import <Foundation/Foundation.h>
    @interface MyPerson : NSObject {
        int _sum;
    }
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, copy) NSString *hobby;
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) double height;
    @property (nonatomic, assign) short number;
    - (void)speak;
    + (void)walk;
    @end
    
    @implementation MyPerson
    - (void)speak {
        NSLog(@"%s", __func__);
    }
    + (void)walk {
        NSLog(@"%s", __func__);
    }
    @end
    

    在objc4源码中获取MyPerson类对象(objc_class)首地址:0x1000084e8
    注意:我这里输出的是 p.class(类对象)

    获取MyPerson类对象首地址

    那bits距离objc_class首地址还需要偏移 32字节 = isa + superclass + cache
    bits的地址 = 0x1000084e8 + 0x20 = 0x100008508
    把这个bits强转成class_data_bits_t:

    我们来看看class_data_bits_t的声明,里面确实有个bits成员

    struct class_data_bits_t {
        // c++提供的friend,用于访问受保护的objc_class
        friend objc_class;
    
        // Values are the FAST_ flags above.
        uintptr_t bits;
    private:
        bool getBit(uintptr_t bit) const
        {
            return bits & bit;
        }
    
        // Atomically set the bits in `set` and clear the bits in `clear`.
        // set and clear must not overlap.
        void setAndClearBits(uintptr_t set, uintptr_t clear)
        {
            ASSERT((set & clear) == 0);
            uintptr_t newBits, oldBits = LoadExclusive(&bits);
            do {
                newBits = (oldBits | set) & ~clear;
            } while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
        }
    
        void setBits(uintptr_t set) {
            __c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
        }
    
        void clearBits(uintptr_t clear) {
            __c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
        }
    
    public:
    
        class_rw_t* data() const {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
        void setData(class_rw_t *newData)
        {
            ASSERT(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
            // Set during realization or construction only. No locking needed.
            // Use a store-release fence because there may be concurrent
            // readers of data and data's contents.
            uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
            atomic_thread_fence(memory_order_release);
            bits = newBits;
        }
    
        // Get the class's ro data, even in the presence of concurrent realization.
        // fixme this isn't really safe without a compiler barrier at least
        // and probably a memory barrier when realizeClass changes the data field
        const class_ro_t *safe_ro() const {
            class_rw_t *maybe_rw = data();
            if (maybe_rw->flags & RW_REALIZED) {
                // maybe_rw is rw
                return maybe_rw->ro();
            } else {
                // maybe_rw is actually ro
                return (class_ro_t *)maybe_rw;
            }
        }
    
    #if SUPPORT_INDEXED_ISA
        void setClassArrayIndex(unsigned Idx) {
            // 0 is unused as then we can rely on zero-initialisation from calloc.
            ASSERT(Idx > 0);
            data()->index = Idx;
        }
    #else
        void setClassArrayIndex(__unused unsigned Idx) {
        }
    #endif
    
        unsigned classArrayIndex() {
    #if SUPPORT_INDEXED_ISA
            return data()->index;
    #else
            return 0;
    #endif
        }
    
        bool isAnySwift() {
            return isSwiftStable() || isSwiftLegacy();
        }
    
        bool isSwiftStable() {
            return getBit(FAST_IS_SWIFT_STABLE);
        }
        void setIsSwiftStable() {
            setAndClearBits(FAST_IS_SWIFT_STABLE, FAST_IS_SWIFT_LEGACY);
        }
    
        bool isSwiftLegacy() {
            return getBit(FAST_IS_SWIFT_LEGACY);
        }
        void setIsSwiftLegacy() {
            setAndClearBits(FAST_IS_SWIFT_LEGACY, FAST_IS_SWIFT_STABLE);
        }
    
        // fixme remove this once the Swift runtime uses the stable bits
        bool isSwiftStable_ButAllowLegacyForNow() {
            return isAnySwift();
        }
    
        _objc_swiftMetadataInitializer swiftMetadataInitializer() {
            // This function is called on un-realized classes without
            // holding any locks.
            // Beware of races with other realizers.
            return safe_ro()->swiftMetadataInitializer();
        }
    };
    
    bits

    class_data_bits_t里确实有个bits成员但是不是我们想要的内容,然后看到了这个data()方法,是把bits成员通过位运算得到class_rw_t

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

    于是乎,继续我的探索,class_data_bits_t bits去调用data()

    bits.data()

    输出了class_rw_t的内容,可还是看不出啥东西。
    看看class_rw_t声明的最后发现了methodspropertiesprotocolsro

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint16_t witness;
    #if SUPPORT_INDEXED_ISA
        uint16_t index;
    #endif
    
        explicit_atomic<uintptr_t> ro_or_rw_ext;
    
        Class firstSubclass;
        Class nextSiblingClass;
    
    private:
        using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
    
        const ro_or_rw_ext_t get_ro_or_rwe() const {
            return ro_or_rw_ext_t{ro_or_rw_ext};
        }
    
        void set_ro_or_rwe(const class_ro_t *ro) {
            ro_or_rw_ext_t{ro, &ro_or_rw_ext}.storeAt(ro_or_rw_ext, memory_order_relaxed);
        }
    
        void set_ro_or_rwe(class_rw_ext_t *rwe, const class_ro_t *ro) {
            // the release barrier is so that the class_rw_ext_t::ro initialization
            // is visible to lockless readers
            rwe->ro = ro;
            ro_or_rw_ext_t{rwe, &ro_or_rw_ext}.storeAt(ro_or_rw_ext, memory_order_release);
        }
    
        class_rw_ext_t *extAlloc(const class_ro_t *ro, bool deep = false);
    
    public:
        void setFlags(uint32_t set)
        {
            __c11_atomic_fetch_or((_Atomic(uint32_t) *)&flags, set, __ATOMIC_RELAXED);
        }
    
        void clearFlags(uint32_t clear) 
        {
            __c11_atomic_fetch_and((_Atomic(uint32_t) *)&flags, ~clear, __ATOMIC_RELAXED);
        }
    
        // set and clear must not overlap
        void changeFlags(uint32_t set, uint32_t clear) 
        {
            ASSERT((set & clear) == 0);
    
            uint32_t oldf, newf;
            do {
                oldf = flags;
                newf = (oldf | set) & ~clear;
            } while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
        }
    
        class_rw_ext_t *ext() const {
            return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
        }
    
        class_rw_ext_t *extAllocIfNeeded() {
            auto v = get_ro_or_rwe();
            if (fastpath(v.is<class_rw_ext_t *>())) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
            } else {
                return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
            }
        }
    
        class_rw_ext_t *deepCopy(const class_ro_t *ro) {
            return extAlloc(ro, true);
        }
    
        const class_ro_t *ro() const {
            auto v = get_ro_or_rwe();
            if (slowpath(v.is<class_rw_ext_t *>())) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
            }
            return v.get<const class_ro_t *>(&ro_or_rw_ext);
        }
    
        void set_ro(const class_ro_t *ro) {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro = ro;
            } else {
                set_ro_or_rwe(ro);
            }
        }
    
        const method_array_t methods() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
            } else {
                return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
            }
        }
    
        const property_array_t properties() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
            } else {
                return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
            }
        }
    
        const protocol_array_t protocols() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
            } else {
                return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
            }
        }
    };
    
    1.获取类的实例方法列表

    把得到class_rw_t去调用methods()得到 method_array_t这个类

    rw.methods()

    method_array_t是rw.methods()的内部结构
    看看method_array_t声明:

    class method_array_t : 
        public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
    {
        typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;
    
     public:
        method_array_t() : Super() { }
        method_array_t(method_list_t *l) : Super(l) { }
    
        const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
            return beginLists();
        }
        
        const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
    };
    

    拿到method_array_t里的ptr指针得到的是method_list_t
    看看method_list_t声明:

    method_list_t

    method_list_t继承自entsize_list_tt模板

    entsize_list_tt

    于是乎我就可以通过method_list_t实例去调用get(下标)来访问,得到一个method_t类型的实例

    method_list_t.get(index)

    看看method_t的声明,一个method最主要的信息:SEL、签名、IMP

    于是乎我就可以通过method_t实例去调用big()方法

        big &big() const {
            ASSERT(!isSmall());
            return *(struct big *)this;
        }
    

    由于我的电脑是Inter芯片的就是大端字节,直接调用就可以出来了

    注意: method_t里有声明big和small去区分大端和小端
    大端:低地址段存高位字节,高地址段存低位字节;
    小端:低地址段存低位字节,高地址段存高位字节。

    举例小端字节图:

    小端字节

    如果是M1芯片的电脑,使用big会报错,需要调用small()

    p $9.get(0).small()
    
        small &small() const {
            ASSERT(isSmall());
            return *(struct small *)((uintptr_t)this & ~(uintptr_t)1);
        }
    

    如果你不知道你的电脑是大端小端没关系,它还提供了一个getDescription(),然后取值就可以了。

    于是就探索出来方法列表存储在objc_class -> bits -> class_rw_t -> methods里。

    提问:为什么method_list_t里的count = 12?
    12 = (五个属性的getter/setter) + speak + 析构函数.cxx_destruct

    为什么这12个方法里没有类方法?类方法是存在元类里的。下面有内容说到。

    注意:不是每个类都有析构函数.cxx_destruct的!析构函数的作用是用来释放属性的,如果一个类没有属性,那就不需要析构函数。
    验证:把属性全部注释掉即可,输出方法列表里的符号即可。

    Runtime API 获取类的方法:

    //获取类的方法列表
    -(void)wj_class_copyMethodList:(Class)pClass {
        unsigned int outCount = 0;
        Method *methods = class_copyMethodList(pClass, &outCount);
        for (int i = 0; i < outCount; i++) {
            Method method = methods[i];
            NSString *name = NSStringFromSelector(method_getName(method));
            const char *cType = method_getTypeEncoding(method);
            NSLog(@"name = %@ type = %s",name,cType);
        }
        free(methods);
    }
    
    2.获取类的属性列表

    和方法列表一样的步骤一样的配方

    上面就说了entsize_list_tt是专门提供给method_list_tivar_list_tproperty_list_t的模板类,给予其访问成员的方法。

    ivar_list_tproperty_list_t的声明:

    Runtime API 获取类的属性:

    // 获取类的属性
    -(void)wj_class_copyPropertyList:(Class)pClass {
        unsigned int outCount = 0;
        objc_property_t *perperties = class_copyPropertyList(pClass, &outCount);
        for (int i = 0; i < outCount; i++) {
            objc_property_t property = perperties[i];
            const char *cName = property_getName(property);
            const char *cType = property_getAttributes(property);
            NSLog(@"name = %s type = %s",cName,cType);
        }
        free(perperties);
    }
    
    3.获取类的类方法列表

    总结:类的类方法存储在元类里

    面试题:苹果为什么要设计元类?
    设计元类目的是为了复用消息机制。单一职责。

    4.获取类的成员变量列表

    上面看到了rw里面有methodspropertiesprotocolsro,但是就是没有成员变量,我就看看ro的源码声明:

    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
        union {
            const uint8_t * ivarLayout;
            Class nonMetaclass;
        };
    
        explicit_atomic<const char *> name;
        WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    
        // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
        _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
    
        _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
            if (flags & RO_HAS_SWIFT_INITIALIZER) {
                return _swiftMetadataInitializer_NEVER_USE[0];
            } else {
                return nil;
            }
        }
    
        const char *getName() const {
            return name.load(std::memory_order_acquire);
        }
    
        class_ro_t *duplicate() const {
            bool hasSwiftInitializer = flags & RO_HAS_SWIFT_INITIALIZER;
    
            size_t size = sizeof(*this);
            if (hasSwiftInitializer)
                size += sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
    
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
    
            if (hasSwiftInitializer)
                ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
    
    #if __has_feature(ptrauth_calls)
            // Re-sign the method list pointer.
            ro->baseMethods = baseMethods;
    #endif
    
            return ro;
        }
    
        Class getNonMetaclass() const {
            ASSERT(flags & RO_META);
            return nonMetaclass;
        }
    
        const uint8_t *getIvarLayout() const {
            if (flags & RO_META)
                return nullptr;
            return ivarLayout;
        }
    };
    
    ro

    输出ro里的ivars:

    于是我就在ro里找到了ivars,逐一输出了成员变量:
    (还找到了baseMethodsbaseProtocolsbaseProperties

    Runtime API 获取类的成员变量:

    // 获取类的成员变量
    -(void)wj_class_copyIvarList:(Class)pClass {
        unsigned int  outCount = 0;
        Ivar *ivars = class_copyIvarList(pClass, &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            const char *cName =  ivar_getName(ivar);
            const char *cType = ivar_getTypeEncoding(ivar);
            NSLog(@"name = %s type = %s",cName,cType);
        }
        free(ivars);
    }
    

    六、rw、ro、rwe之间的关系

    rw里面有methods、properties、protocols和ro;
    ro里面有ivars、baseMethods、baseProtocols、baseProperties

    有兴趣的可以自己输出一下,baseMethods、baseProtocols、baseProperties保存的东西,rw里面有自己一套逻辑去判断是从rwe还是ro获取这些方法协议属性等等。

    来看看rw的获取这些的源码:

    获取rwe

    比如methods函数,每次去取函数列表的时候,都会先从rwe里边取函数列表,如果rwe不存在,则会去ro上面去取函数列表。

    rwe的源码声明:

    struct class_rw_ext_t {
        DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
        class_ro_t_authed_ptr<const class_ro_t> ro;
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
        char *demangledName;
        uint32_t version;
    };
    

    我先提个疑问:为什么苹果要设计这套东西?rwe是什么时候产生的,有什么作用呢?
    苹果在WWDC20视频就给出了答案。

    ro是在类第一次从磁盘被加载到内存中产生,它是一块纯净的内存clear memory(只读的),它保存了类最纯净的成员变量、实例方法、协议、属性等等。
    当进程内存不够时候,ro可以被移除,在需要的时候再去磁盘中加载,从而节省更多内存。

    class_ro_t

    rw:程序运行就必须一直存在,在进程运行时类一经使用后,runtime就会把ro写入新的数据结构dirty memory(读写的),这个数据结构存储了只有在运行时才会生成的新信息。(例如创建一个新的方法缓存并从类中指向它)

    于是所有类都会链接成一个树状结构,这是通过First SubclassNext Sibling Class指针实现的,这就决定了runtime能够遍历当前使用的所有类。

    class_rw_t

    类的方法和属性保存在ro了,为什么还要在rw里有呢?因为它们可以在运行时被修改,当category被加载时,它可以向类中添加新的方法,也可以通过Runtime API去动态添加和修改,因为ro是只读的,所以需要再rw去跟踪这些东西。

    但是上述这样做的结果会导致占用相当相当多的内存,因为据苹果官方统计只有大约10%的类需要真正地去修改它们的方法。
    那么如何缩小这些结构呢?于是就设计出了rwe,从而减少rw的大小。

    rwe:是在category被加载或者通过 Runtime API 对类的方法属性协议等等进行修改后产生的,它保存了原本rw中类可能会被修改的东西(Methods / Properties / Protocols / Demangled Name),它的作用是给rw瘦身。(rwe是类被修改才会产生,没有被修改的类不会产生。注意:category被加载时候产生的rwe的条件是:分类和本类是必须是非懒加载类(重写+load)

    class_rw_ext_t

    在进程运行期间,没有被修改的类的rw里只有ro,类的读取只会读取ro的东西,在类被修改的时候rw新增了rwe,类优先读取rwe的东西。当进程内存不足的时候,ro可以被移除。

    Swift的类的结构依旧保留着和OC一样的互通。

    七、objc_class的cache成员里有什么、cache_t结构剖析、cache的扩容规则

    类对象的本质是objc_class,其成员里有isasuperclasscachebits

    cache前面有isasuperclass各占8个字节,于是可以通过内存平移的方式取到cache在内存上的内容:

    objc_class的cache在内存上的内容

    看到cache的内容没有一个是有用的东西,于是看看cache_t里的源码声明:

    cache_t的成员变量

    输出的cache在内存上的内容恰巧是cache_t的成员变量的值。然后我还看到了一个insert方法,既然这个cache是缓存,那总要有数据来源和保存到哪里。

    cache_tinsert方法:(向buckets指针数组插入方法的部分)

    image.png

    这行正是插入的代码 b[i].set<Atomic, Encoded>(b, sel, imp, cls());,插入的是selimpcls,保存到b,它是bucket_t类型的,再看看cache_t它是有一个bucket()函数去获取bucket_t对象的

    struct bucket_t *cache_t::buckets() const
    {
        uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
        return (bucket_t *)(addr & bucketsMask);
    }
    

    猜测不出所料的话cache_t的成员_bucketsAndMaybeMask肯定与缓存的实质cache_t有着直接关联(后面会讲),我只需要通过调用buckets()就可以获得缓存bucket_t这个数组指针了。

    获取bucket_t在内存上的内容

    bucket_t看到了sel和imp,但是输出的内容又看不懂了,来看看bucket_t的源码声明:

    struct bucket_t {
    private:
        // IMP-first is better for arm64e ptrauth and no worse for arm64.
        // SEL-first is better for armv7* and i386 and x86_64.
    #if __arm64__
        explicit_atomic<uintptr_t> _imp;
        explicit_atomic<SEL> _sel;
    #else
        explicit_atomic<SEL> _sel;
        explicit_atomic<uintptr_t> _imp;
    #endif
    
        // Compute the ptrauth signing modifier from &_imp, newSel, and cls.
        uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
            return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
        }
    
        // Sign newImp, with &_imp, newSel, and cls as modifiers.
        uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
            if (!newImp) return 0;
    #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
            return (uintptr_t)
                ptrauth_auth_and_resign(newImp,
                                        ptrauth_key_function_pointer, 0,
                                        ptrauth_key_process_dependent_code,
                                        modifierForSEL(base, newSel, cls));
    #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
            return (uintptr_t)newImp ^ (uintptr_t)cls;
    #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
            return (uintptr_t)newImp;
    #else
    #error Unknown method cache IMP encoding.
    #endif
        }
    
    public:
        static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
        inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    
    #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
    #define MAYBE_UNUSED_ISA
    #else
    #define MAYBE_UNUSED_ISA __attribute__((unused))
    #endif
        inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
            uintptr_t imp = _imp.load(memory_order_relaxed);
            if (!imp) return nil;
    #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
    #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
            imp ^= (uintptr_t)cls;
    #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
    #else
    #error Unknown method cache IMP encoding.
    #endif
            return (IMP)imp;
        }
    
        inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
            uintptr_t imp = _imp.load(memory_order_relaxed);
            if (!imp) return nil;
    #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
            SEL sel = _sel.load(memory_order_relaxed);
            return (IMP)
                ptrauth_auth_and_resign((const void *)imp,
                                        ptrauth_key_process_dependent_code,
                                        modifierForSEL(base, sel, cls),
                                        ptrauth_key_function_pointer, 0);
    #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
            return (IMP)(imp ^ (uintptr_t)cls);
    #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
            return (IMP)imp;
    #else
    #error Unknown method cache IMP encoding.
    #endif
        }
    
        inline void scribbleIMP(uintptr_t value) {
            _imp.store(value, memory_order_relaxed);
        }
    
        template <Atomicity, IMPEncoding>
        void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
    };
    

    bucket_t里找到了set函数给bucket_t设置值,调用sel()函数可以返回SEL,调用imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)可以返回IMP,于是就可以试试在内存上获取到的bucket_t去调用这两个函数:

    获取到第一个bucket_t指针数组存储的sel和imp,它是 class 方法

    通过内存平移的方式获取bucket_t对象,并调用sel()和imp()函数:

    总结cache_t的结构如下:

    思考:
    1.为什么它的缓存的方法不是连续的?
    2.buckets是如何扩容的?
    3.为什么我没调用class和respondsToSelector方法它们就缓存到了buckets里面了?

    来看看cache_tinsert函数的全部实现代码:

    void cache_t::insert(SEL sel, IMP imp, id receiver)
    {   
        runtimeLock.assertLocked();
    
        // Never cache before +initialize is done
        if (slowpath(!cls()->isInitialized())) {
            return;
        }
    
        if (isConstantOptimizedCache()) {
            _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                        cls()->nameForLogging());
        }
    
    #if DEBUG_TASK_THREADS
        return _collecting_in_critical();
    #else
    #if CONFIG_USE_CACHE_LOCK
        mutex_locker_t lock(cacheUpdateLock);
    #endif
    
        ASSERT(sel != 0 && cls()->isInitialized());
    
        // Use the cache as-is if until we exceed our expected fill ratio.
        mask_t newOccupied = occupied() + 1; // 第一次insert的时候occupied()即_occupied会是0,newOccupied会是1
        // capacity的值就是buckets的长度
        unsigned oldCapacity = capacity(), capacity = oldCapacity;
        // 如果cache为空,则分配 arm64下长度为2 x86_64下长度为4的buckets,reallocate里无需释放老buckets
        if (slowpath(isConstantEmptyCache())) {
            // Cache is read-only. Replace it.
            // 给容量附上初始值,x86_64为4,arm64为2
            if (!capacity) capacity = INIT_CACHE_SIZE;
            reallocate(oldCapacity, capacity, /* freeOld */false);
        }
        // 在arm64下,缓存的大小 <= buckets长度的7/8  不扩容
        // 在x86_64下,缓存的大小 <= buckets长度的3/4  不扩容
        else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
            // Cache is less than 3/4 or 7/8 full. Use it as-is.
        }
    #if CACHE_ALLOW_FULL_UTILIZATION // 只有arm64才需要走这个判断
        // 在arm64下,buckets的长度 < = 8 时,不扩容
        else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
            // Allow 100% cache utilization for small buckets. Use it as-is.
        }
    #endif
        else { // 除却上面的逻辑,就是扩容逻辑了
            // 对当前容量的2倍扩容,并且如果扩容后容量大小 大于 一个最大阈值,则设置为这个最大值
            capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
            if (capacity > MAX_CACHE_SIZE) {
                capacity = MAX_CACHE_SIZE;
            }
            // 创建新的扩容后的buckets,释放旧的bukets
            reallocate(oldCapacity, capacity, true);
        }
    
        bucket_t *b = buckets(); // 获取buckets数组指针
        mask_t m = capacity - 1; // m是buckets的长度-1
        mask_t begin = cache_hash(sel, m);// 通过hash计算出要插入的方法在buckets上的起始位置(begin不会超过buckets的长度-1)
        mask_t i = begin;
    
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot.
        do {
            if (fastpath(b[i].sel() == 0)) { // 当前hash计算出来的buckets在i的位置它有没有值,如果没有值就去存方法
                incrementOccupied();
                b[i].set<Atomic, Encoded>(b, sel, imp, cls());
                return;
            }
            if (b[i].sel() == sel) { // 当前hash计算出来的buckets在i的位置sel有值,并且这个值等于要存储的sel,说明该方法已有缓存
                // The entry was added to the cache by some other thread
                // before we grabbed the cacheUpdateLock.
                return;
            }
        } while (fastpath((i = cache_next(i, m)) != begin)); // 如果计算出来的起始位置i存在hash冲突的话,就通过cache_next去改变i的值(增大i)
    
        bad_cache(receiver, (SEL)sel);
    #endif // !DEBUG_TASK_THREADS
    }
    

    这个insert函数分成扩容条件判断和插入条件判断两个部分:1.扩容判断 2.插入缓存

    1.先来看看插入缓存的时候是苹果如何操作的:(解答第一个思考问题)

    1.插入方法部分

    a.首先获取到buckets数组指针,得到buckets最大的下标 m,并且通过cache_hash函数计算出要缓存的方法在buckets中的起始位置;(可以把buckets看做是hash表)

    看看是如何计算起始位置的:

    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        uintptr_t value = (uintptr_t)sel;// 把SEL转化成unsigned long类型,一个很大的数字
    #if CONFIG_USE_PREOPT_CACHES
        value ^= value >> 7;
    #endif
        // value是一串很大的数,去&上 buckets长度-1
        // 一个很大的数 & 一个小的数 得到的结果最大值是 这个小的数
        return (mask_t)(value & mask);
    }
    

    cache_hash保证起始位置一定在buckets最大下标里。

    b.然后进入do...while循环,判断计算的起始位置存在hash冲突的话,则通过调用cache_next去增大i的值;

    看看在hash冲突时,如何改变起始位置的:(这个cache_next函数是区分平台的)

    #if CACHE_END_MARKER
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask;
    }
    #elif __arm64__
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask;
    }
    #else
    #error unexpected configuration
    #endif
    

    c.最后判断该起始位置下没有值则去存储方法,接着判断在计算出来的buckets起始位置中有相同的sel则说明已有缓存。

    为什么它的缓存的方法不是连续的?
    因为要缓存的方法在buckets的起始位置是通过hash算出来的,所以它的起始位置不是固定步长的。

    2.看看buckets是如何扩容的:(解答第二个思考问题)

    buckets扩容逻辑

    a.首先如果缓存为空的话,就给buckets分配初始化的长度(x86_64为4,arm为2)并且创建一个buckets

    b.在arm64框架下,缓存的大小 <= buckets长度的7/8,并且buckets长度<=8没装满8,不扩容,在x86_64下,缓存的大小 <= buckets长度的3/4 ,不扩容。

    c.除却b的逻辑,就是扩容逻辑:对当前容量的2倍扩容,并且如果扩容后容量大小 > MAX_CACHE_SIZE,则设置为MAX_CACHE_SIZE;计算出扩容大小后,以这个容量去创建新的buckets,和释放旧的buckets

    注意:扩容后先创建新的buckets,然后释放旧的buckets,那以前的缓存的方法就没有了!

    试想一下为什么要把旧的释放掉,而不是把旧的内容赋值给新的?
    如果每次调用一次方法都把实例方法存到类对象,如果这个类有极其多的实例方法呢?像这样的类就有好多好多呢?(虽然这是不实际的)一个app的内存就那么大,那些方法如果只调用了一次就在缓存里储存着那就有不异于浪费内存,而方法缓存的初衷是快速调用频繁使用的方法。所以每次扩容的时候,都会把旧的数据丢弃。空间换时间,但也需节约空间!

    3.为什么我没调用class和respondsToSelector方法它们就缓存到了buckets里面了?(解答第二个思考问题)

    我在insert函数里去打印这个sel名称:

    这个东西是我在lldb的时候调用了 p.class 系统调用了这两个方法。

    相关文章

      网友评论

          本文标题:Objective-C 类的底层探索

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