美文网首页
iOS OC 类原理

iOS OC 类原理

作者: just东东 | 来源:发表于2020-08-04 16:31 被阅读0次

    iOS OC底层探索

    注: 本文使用的环境是objc4-779.1 Xcode 11.5 (11E608c)

    1. 类

    根据前面几篇文章的分析,我们知道Objective -C的对象通过isa与类关联起来,那么到底什么是类呢?下面我们来探索一下。

    我们知道Objective-C的基类是NSObject,日常开发中我们我们使用到的类基本都是用NSObject派生来的,那么在编译后,他到底是什么样子呢?

    在这篇文章中我们说道Class在底层是一个objc_class那么它到底是如何实现的呢?我们来到objc源码中一探究竟。我们知道objc_class是一个结构体我们搜索struct objc_class,我们发现会有很多结果,那么我们到底去分析那个版本呢,我们应该知道runtime有old和new两个版本,那么新版本当然作为我们的首选,所有我们打开objc-runtime-new.h进行一探究竟。

    image
    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() const {
            return bits.data();
        }
    }
    

    在这里我们再次看到了objc_object所以说在面向对象里面真的是万物皆对象。OC中的NSObject就是对底层objc_object的封装。

    C Objective-C
    objc_object NSObject
    objc_class Nsobjcet(Class)

    2. 类中包含的内容

    通过objc_class的源码我们知道类中包含:

    • ISA //isa指针 ,继承自objc_object
    • superclass // 父类指针
    • cache // cache_t类型的结构体
    • bits // class_data_bits_t结构体
    特别提醒
    
    

    isa 在源码中是以注释的形式体现出来的,并不是没有写,而是继承自objc_object

    struct objc_object {
    private:
        isa_t isa;
    }
    

    2.1 ISA指针

    在以前的文章中我们已经详细的介绍了isa,在对象初始化的时候通过isa使对象和类关联起来,那么类里面为什么还会有isa呢,通过我们的isa走位分析那篇文章我就可以知道,类里面的isa指向原类。原类与类同样通过isa进行了关联。

    2.2 superclass

    顾名思义,superclass就是指向父类,继承自哪个父类,一般来说根父类基本都是NSObject,根原类的父类也是NSObject。

    2.3 cache

    顾明思议,cach是缓存的意思,肯定存储的是类中的一些缓存。cache是一个cache_t类型的结构体。在objc-runtime-new.h中查看cache_t源码如下:

    主要有bucket_t的结构体指针,mask_tmaskuint16_t_flags_occupied

    类型 占用空间
    bucket_t* 8字节
    mask_t(uint32_t) 4字节
    uint16_t 2 字节

    总计是8+4+2+2=16字节

    cache_t 源码实现:

    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets;
        explicit_atomic<mask_t> _mask;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
        
        // How much the mask is shifted by.
        static constexpr uintptr_t maskShift = 48;
        
        // Additional bits after the mask which must be zero. msgSend
        // takes advantage of these additional bits to construct the value
        // `mask << 4` from `_maskAndBuckets` in a single instruction.
        static constexpr uintptr_t maskZeroBits = 4;
        
        // The largest mask value we can store.
        static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
        
        // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
        static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
        
        // Ensure we have enough bits for the buckets pointer.
        static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        // _maskAndBuckets stores the mask shift in the low 4 bits, and
        // the buckets pointer in the remainder of the value. The mask
        // shift is the value where (0xffff >> shift) produces the correct
        // mask. This is equal to 16 - log2(cache_size).
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
    
        static constexpr uintptr_t maskBits = 4;
        static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
        static constexpr uintptr_t bucketsMask = ~maskMask;
    #else
    #error Unknown cache mask storage type.
    #endif
        
    #if __LP64__
        uint16_t _flags;
    #endif
        uint16_t _occupied;
        、
        、
        、
        、省略代码
    }
    

    通过objc源码查看 cache_t 的实现,我们发现主要有

    • _buckets bucket_t类型的结构体指针
    • _mask mask_t类型的结构体
    • _flags
    • _occupied

    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
    }
    

    mask_t 源码实现:

    #if __LP64__
    typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    #else
    typedef uint16_t mask_t;
    #endif
    

    method_t 源码实现:

    struct method_t {
        SEL name;
        const char *types;
        MethodListIMP imp;
    
        struct SortBySELAddress :
            public std::binary_function<const method_t&,
                                        const method_t&, bool>
        {
            bool operator() (const method_t& lhs,
                             const method_t& rhs)
            { return lhs.name < rhs.name; }
        };
    };
    

    bucket_t源码我们大概就能够知道它是存储方法的,因为方法的本质就是SEL和IMP,这个有method_t源码也可以证实,所以cache的主要作用就是存储我们的方法的,下面我们通过lldb来进行验证一下:

    • bucket_t分析:
      首先我们在objc源码中实现一个LGPerson类,代码如下:
    @interface LGPerson : NSObject{
        NSString *hobby;
    }
    
    @property (nonatomic, copy) NSString *nickname;
    
    - (void)sayHello;
    
    - (void)sayCode;
    
    - (void)sayMaster;
    
    - (void)sayNB;
    
    + (void)sayHappy;
    
    @end
    

    main代码:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            LGPerson *person = [[LGPerson alloc] init];
            Class pClass = [LGPerson class];
     
            [person sayHello];
            [person sayCode];
            [person sayNB];
            
        }
        return 0;
    }
    

    分别在sayHello,sayCode, sayNB处断点进行lldb查看,结果如下:


    没执行方法前
    执行方法后
    执行多个方法后 执行多个方法2
    特别提醒!
    

    类的isa占用8字节,superclass占用8字节,所以我们对cache的分析由首地址加16进行分析,16字节在16进制中就是加10。

    根据图一我们可以看到在没有执行方法前我们的cache_tbuckets()里面是取不出数据的直接是个null,当我们执行完sayHello方法后再buckets里面取出的数据,并通过打印sel获取到了一个叫sayHello的SEL,在后面的两个图里面我们分别执行了sayCodesayNB方法后也分别获取到了sayCodesayNB的SEL,当我们越界获取的时候就是空了,所以我们通过lldb分析可知,cahce主要是缓存方法的。那么为什么没有找到allocclass方法呢?因为它们是类方法,会存储在原类里面,在本文的后续过程中我们会进一步分析。

    • 补充init
    init方法

    在我们没有执行任何自定义方法的时候,我们会发现cache里面有了一个数据,通过lldb打印我们看到其实是init,因为init是个实例方法,所以当我们调用了init后也可以在cache里面找到init方法。

    • _ocuupied

    lldb分析:

    未执行任何方法

    当我们没有执行任何方法的时候,我们通过lldb打印cache,我们发现_ocuupied的值为0,_mask的值为0。

    执行一个方法

    当我们执行了一个方法后再次通过lldb打印cache,我们发现_ocuupied的值为1_msak的值为3,那么_ocuupied是不是记录了我们缓存方法的个数呢?

    执行两个方法

    当我们执行了两个方法后再次通过lldb打印cache,我们发现_ocuupied的值为2_mask的值为3,这个时候我们肯定会觉得_ocuupied大概率是记录了我们缓存方法的个数,下面我们继续进行探索。

    执行三个方法

    当我们执行了三个方法后再次通过lldb打印cache,我们发现_ocuupied的值为3_mask的值为3,这个时候我们基本确定_ocuupied记录了我们缓存方法的个数,下面我们继续进行探索。

    执行四个方法

    当我们执行了四个方法后再次通过lldb打印cache,我们发现_ocuupied的值为1_mask的值为7,这个时候按照我们的猜想_ocuupied的值应该为4,但是他却成了1,那么到底是什么原因导致了这个情况呢?虽然_ocuupied的值变成了1但是_mask的值也变了,并且为7,刚刚一直是3的mask现在变成可7,而我们的ocuupied刚刚也是三,好像这个3记录在了mask里面,ocuupied重新开始计数一样,mask开始为3,当ocuupied为3后mask就满了,将mask进行扩容后,继续重新对ocuupied进行计数。这个很像哈希表这种数据结构,并且为了解决哈希冲突,使用的是开放寻址法,而开放寻址法必然要在合适的时机进行扩容,这个时机应该是表快满的时候。为了验证我们的猜想,还是查看cache_t的源码进行分析吧。

    在源码中我们发现了maskoccupied两个函数。

        mask_t mask();
        mask_t occupied();
    

    跳转进occupied函数后,源码如下,紧随其后的还有incrementOccupied()函数。

    mask_t cache_t::occupied() 
    {
        return _occupied;
    }
    
    void cache_t::incrementOccupied() 
    {
        _occupied++;
    }
    

    根据上面的源码,我们进行全局搜索,查找调用occupiedincrementOccupied()地方,发现occupied的调用有三处incrementOccupied()的调用有一处,但他们两个都同事出现在了一个insert函数中,看到这个函数后我们的第一想法就是,这个函数是cache缓存的核心函数,下面我们做进一步的验证,再分析一下mask函数。mask()函数的实现如下:主要就是返回_mask的值。

    mask_t cache_t::mask() 
    {
        return _mask.load(memory_order::memory_order_relaxed);
    }
    

    下面我们就搜索一下mask(),发现共有三处调用,有两处在同一函数内,有一处是返回值,所以我们重点分析两处在一起的那个函数,函数实现如下:

    unsigned cache_t::capacity()
    {
        return mask() ? mask()+1 : 0; 
    }
    

    既然没找到mask直接在cahce中的调用与影响,那么我们可以继续搜索一下capacity()函数,这里的mask()被间接调用的可能性很大,通过搜索capacity()函数后发现共有四处调用,其中一处就在insert函数内,这时我们上面的猜想又得到了一些可能性。下面我们直接上insert函数的源码作进一步的分析吧。

    insert函数源码:

    void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
    {
    #if CONFIG_USE_CACHE_LOCK
        cacheUpdateLock.assertLocked();
    #else
        runtimeLock.assertLocked();
    #endif
    
        ASSERT(sel != 0 && cls->isInitialized());
    
        // Use the cache as-is if it is less than 3/4 full
        mask_t newOccupied = occupied() + 1;
        unsigned oldCapacity = capacity(), capacity = oldCapacity;
        if (slowpath(isConstantEmptyCache())) {
            // Cache is read-only. Replace it.
            if (!capacity) capacity = INIT_CACHE_SIZE;
            reallocate(oldCapacity, capacity, /* freeOld */false);
        }
        else if (fastpath(newOccupied <= capacity / 4 * 3)) {
            // Cache is less than 3/4 full. Use it as-is.
        }
        else {
            capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
            if (capacity > MAX_CACHE_SIZE) {
                capacity = MAX_CACHE_SIZE;
            }
            reallocate(oldCapacity, capacity, true);
        }
    
        bucket_t *b = buckets();
        mask_t m = capacity - 1;
        mask_t begin = cache_hash(sel, m);
        mask_t i = begin;
    
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the
        // minimum size is 4 and we resized at 3/4 full.
        do {
            if (fastpath(b[i].sel() == 0)) {
                incrementOccupied();
                b[i].set<Atomic, Encoded>(sel, imp, cls);
                return;
            }
            if (b[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));
    
        cache_t::bad_cache(receiver, (SEL)sel, cls);
    }
    
      1. 排除前面的加锁和断言
      1. 首先获取了occupied的值在加1,在获取mask的值,放在变量capacity内
      1. 先判断cache是否为空,如果为空则初始化一个值INIT_CACHE_SIZE,这里的初始化值为4,源码放在后面,就是1左移2位,二进制为 100, 10进制为4,然后调用reallocate 函数开辟空间。
      1. 如果不大于四分之三则不作处理,(这里应该是个扩容算法,后面则进一步验证了)
      1. 其他情况,也就是大于四分之三后,则对capacity进行扩容,扩容为当前值的两倍,并且如果扩容后的值大于最大值MAX_CACHE_SIZE,也就是1左移16位,1 0000 0000 0000 0000, 对应的10进制的值是65536。则不再进行扩容。扩容完毕后调用reallocate 函数开辟空间。
      1. 执行完上述操作后,获取bucket_tmask,并通过cache_hash函数计算出一个begin(应该是缓存新调用方法的位置下标),把begin的值赋值给变量i
      1. 通过一个do while 循环,判断计算出的位置是否为空,不为空则occupied自增,通过set方法将改类的方法进行缓存到上面初始化的bucket里面,如果不为空则判断bucket内的sel是否等于要缓存的sel,直到通过cache_next计算出下一个位置不等于begin

    上面提到的源码

    INIT_CACHE_SIZEMAX_CACHE_SIZE

    /* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
    enum {
        INIT_CACHE_SIZE_LOG2 = 2,
        INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
        MAX_CACHE_SIZE_LOG2  = 16,
        MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
    };
    

    isConstantEmptyCache

    bool cache_t::isConstantEmptyCache()
    {
        return 
            occupied() == 0  &&  
            buckets() == emptyBucketsForCapacity(capacity(), false);
    }
    

    reallocate

    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
    {
        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_hashcache_next

    // Class points to cache. SEL is key. Cache buckets store SEL+IMP.
    // Caches are never built in the dyld shared cache.
    
    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask;
    }
    
    #if __arm__  ||  __x86_64__  ||  __i386__
    // objc_msgSend has few registers available.
    // Cache scan increments and wraps at special end-marking bucket.
    #define CACHE_END_MARKER 1
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask;
    }
    
    #elif __arm64__
    // objc_msgSend has lots of registers available.
    // Cache scan decrements. No end marker needed.
    #define CACHE_END_MARKER 0
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask;
    }
    
    #else
    #error unknown architecture
    #endif
    

    buckets

    struct bucket_t *cache_t::buckets() 
    {
        return _buckets.load(memory_order::memory_order_relaxed);
    }
    

    通过对核心方法insert的分析我们大概知道了cache的基本原理与实现,下面我们总结一下:
    1.当方法调用的时候会进行缓存
    2.缓存时需要判断缓存是否为空,空则初始化空间,不空则判断是否到达扩容临界点,到了则扩容,不到则直接缓存
    3.缓存时则通过哈希计算缓存位置进行存储

    我们知道在调用方法的时候会触发方法的缓存,那么这倒地是怎样一个调用堆栈呢,我么通过搜索insert(进行查看,我们发现在cache_fill函数内调用了insert

    cache_fill源码

    void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
    {
        runtimeLock.assertLocked();
    
    #if !DEBUG_TASK_THREADS
        // Never cache before +initialize is done
        if (cls->isInitialized()) {
            cache_t *cache = getCache(cls);
    #if CONFIG_USE_CACHE_LOCK
            mutex_locker_t lock(cacheUpdateLock);
    #endif
            cache->insert(cls, sel, imp, receiver);
        }
    #else
        _collecting_in_critical();
    #endif
    }
    

    通过cache_fill源码我们发现主要就是获取cache然后调用insert函数缓存方法,当我们想进一步通过搜索cache_fill进行查找调用关系时,却发现并没有相应的源码了,但是在``文件的注释中发下了一些东西:其中cache_fillcache_expandcache_createbcopyflush_cachescache_flushcache_collect_free、等,这些在新的objc-cache.mm中并没有什么踪迹,但是在objc-cache-old.mm中却能看见其踪影,其实在 objc4-756中这些实现还是在的,应该是苹果通过一些优化或者代码的整合成了现在的样子,虽然代码在修改,但是原理基本是不变的,都是为了缓存方法。也都是通过扩容和哈希去实现的。

    /***********************************************************************
    * objc-cache.m
    * Method cache management
    * Cache flushing
    * Cache garbage collection
    * Cache instrumentation
    * Dedicated allocator for large caches
    **********************************************************************/
    
    
    /***********************************************************************
     * Method cache locking (GrP 2001-1-14)
     *
     * For speed, objc_msgSend does not acquire any locks when it reads 
     * method caches. Instead, all cache changes are performed so that any 
     * objc_msgSend running concurrently with the cache mutator will not 
     * crash or hang or get an incorrect result from the cache. 
     *
     * When cache memory becomes unused (e.g. the old cache after cache 
     * expansion), it is not immediately freed, because a concurrent 
     * objc_msgSend could still be using it. Instead, the memory is 
     * disconnected from the data structures and placed on a garbage list. 
     * The memory is now only accessible to instances of objc_msgSend that 
     * were running when the memory was disconnected; any further calls to 
     * objc_msgSend will not see the garbage memory because the other data 
     * structures don't point to it anymore. The collecting_in_critical
     * function checks the PC of all threads and returns FALSE when all threads 
     * are found to be outside objc_msgSend. This means any call to objc_msgSend 
     * that could have had access to the garbage has finished or moved past the 
     * cache lookup stage, so it is safe to free the memory.
     *
     * All functions that modify cache data or structures must acquire the 
     * cacheUpdateLock to prevent interference from concurrent modifications.
     * The function that frees cache garbage must acquire the cacheUpdateLock 
     * and use collecting_in_critical() to flush out cache readers.
     * The cacheUpdateLock is also used to protect the custom allocator used 
     * for large method cache blocks.
     *
     * Cache readers (PC-checked by collecting_in_critical())
     * objc_msgSend*
     * cache_getImp
     *
     * Cache writers (hold cacheUpdateLock while reading or writing; not PC-checked)
     * cache_fill         (acquires lock)
     * cache_expand       (only called from cache_fill)
     * cache_create       (only called from cache_expand)
     * bcopy               (only called from instrumented cache_expand)
     * flush_caches        (acquires lock)
     * cache_flush        (only called from cache_fill and flush_caches)
     * cache_collect_free (only called from cache_expand and cache_flush)
     *
     * UNPROTECTED cache readers (NOT thread-safe; used for debug info only)
     * cache_print
     * _class_printMethodCaches
     * _class_printDuplicateCacheEntries
     * _class_printMethodCacheStatistics
     *
     ***********************************************************************/
    

    留下一些问题?

    为什么要在3/4处扩容?

    因为此处缓存使用的是哈希这种数据结构,哈希中有一个叫做装载因子的概念,表示空位的大小,在3/4处扩容则说明装载因子是1/4,装载因子越大说明可能产生的冲突越多,这里取1/4应该是苹果评估的一个合理的数值。

    方法缓存是有序的吗?

    因为用了哈希,所以肯定无序,这里也稍微做了一些验证,简单说说吧,就不上图已进行说了,验证的对错也不太敢保证,只是自己的一些想法。其实在扩容的时候原来缓存是清除了的,开辟了新的缓存来保存,在objc4-756中我记得是拷贝到新的缓存里,但是在objc4-779.1中我发现原来调用的方法已经不再缓存内了,就是通过上面的lldb验证的,而且第一次扩容后的第一方法会存储在最后一个位置,而不是扩容后的第一个位置,验证了几次都是这样,然后也没仔细探究了。我的想法是:

    • 扩容说明方法够多,如果都调用则需要这么多空间进行缓存
    • 但是既然用到了扩容则说明有些方法没有频繁调用,则触发的缓存
    • 扩容前缓存的方法再次被调用的概率不高了,所以就没有拷贝到新的缓存内,如果再次调用应该会存储到缓存内

    总结

    • cache_t就是缓存我们OC方法的,每调用一个OC方法他就会将该方法缓存;
    • 缓存的开辟从4个开始,到了3/4就开始扩容2倍,直到65536
    • 缓存内主要存储sel和imp

    2.4 bits

    bits是一个class_data_bits_t的结构体,在objc_class源码中很多方法的返回值都是bits中的例如:

    class_rw_t *data() const {
            return bits.data();
    }
    
    void setData(class_rw_t *newData) {
            bits.setData(newData);
    }
    
    #if FAST_HAS_DEFAULT_RR
        bool hasCustomRR() const {
            return !bits.getBit(FAST_HAS_DEFAULT_RR);
        }
        void setHasDefaultRR() {
            bits.setBits(FAST_HAS_DEFAULT_RR);
        }
        void setHasCustomRR() {
            bits.clearBits(FAST_HAS_DEFAULT_RR);
        }
    #else
        bool hasCustomRR() const {
            return !(bits.data()->flags & RW_HAS_DEFAULT_RR);
        }
        void setHasDefaultRR() {
            bits.data()->setFlags(RW_HAS_DEFAULT_RR);
        }
        void setHasCustomRR() {
            bits.data()->clearFlags(RW_HAS_DEFAULT_RR);
        }
    #endif
    

    放眼望去,还是那个data()最显眼,下面我们就来研究一下它。首先让我们来看看class_rw_t的源码:里面除了flagsversionwitness这些,主要还有个ro以及methodspropertiesprotocols,这个ro是一个class_ro_t类型的结构体指针,其他看样子是个数组,methods应该是存储方法的,properties应该是存储属性的,protocols应该是存储协议的。下面我们来进行验证。

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint16_t version;
        uint16_t witness;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;
    
    #if SUPPORT_INDEXED_ISA
        uint32_t index;
    #endif
    
        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));
        }
    };
    

    工程还是当前的工程,分析方法依旧使用lldb,断点打在获取pClass之后。首先说明一下,要想获取到class_data_bits_t的首地址,就要先获取到类的首地址,然后向下偏移32个字节,为什么呢?因为类的 isa 指针占用8字节,superClass占用8字节,cache通过我们的分析占用16字节所以拿到首地址后加32就是我们的bits的首地址。首先我们先打印一下data()的内容

    data内容.png

    打印完我们看到了源码中的很多东西都打印出来了,迫不及待的我们赶紧打印一下methods看看,看到打印出来的是个method_array_t的类型,里面还有个list我们不妨看看这个list里面是否存储的就是我们的方法,打印list后得到的是一个method_list_t类型的指针,既然是list指针,那么首地址应该是第一元素吧,我们通过p *去打印,发现确实打印出了我们的sayHello方法,我么继续打印,分别打印出了sayCodesayNBsayMaster以及一些C++的方法,还有属性的setter和get方法,那么我们的方法原理是什么呢,为什么要把方法存储在这里呢,我们还不知道,在后面的探索中我们会继续研究这些。

    rw-methods.jpg

    下面我们看看属性 properties,按照上述步骤打印,第一个就是我们的nickname,后面就没有了,那么我们测成员变量hobby去哪了呢?

    rw-properties.png

    暂时不探索protocols

    通过上面的探索,感觉确实是这样存储的,但是没有找到类方法sayHappy,我们的成员变量hobby也没有出现在其中。这个时候突然想起,刚才我们在class_rw_t中还发现了一个class_ro_t类型的ro,那么我们在探索一下这个ro吧,首先看看class_ro_t的源码吧:

    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #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;
    
        // 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;
            }
        }
    
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    
        class_ro_t *duplicate() const {
            if (flags & RO_HAS_SWIFT_INITIALIZER) {
                size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
                class_ro_t *ro = (class_ro_t *)memdup(this, size);
                ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
                return ro;
            } else {
                size_t size = sizeof(*this);
                class_ro_t *ro = (class_ro_t *)memdup(this, size);
                return ro;
            }
        }
    };
    

    这个源码跟刚才的class_rw_t有很多类似的地方,也有方法、属性和协议相关的东西,那么是不是这个ro也存储了一些方法和属性相关的东西呢?下面我们继续通过lldb去查看。

    查看ro.png

    通过lldb打印我们看见与其源码内的内容一样,下面我们来探索一下方法baseMethodList

    ro-baseMethodList.jpg

    通过上面的图片我们看到ro内部存储的方法月rw一样,也没有类方法,那么他为什么要存储两份呢?这个只能通过我们后续的探索进行考证了。那么类方法倒地存储在了哪里呢?其实山重水复疑无路,柳暗花明又一村啊,我们还有原类没探索呢,实例方法存储在类中,类方法是不是存储在原类中呢?

    我们通过上面的方法lldb去原类里面看看
    1.首先打印类的地址
    2.然后取出类的isa,类的isa指向原类
    3.&上isa_mask,就是原类
    4.查看原类的bits中的rwro

    原类探索.jpg

    果然我们在原类中找到了我们的sayHappy方法,这回就可以尽情happy了。其实实例方法都是由对象调用的,类方法由类调用,实例方法存储在类中,类方法存储在原类中也就不难理解了。

    这回我们就找到了实例方法和类方法的存储位置,下面继续在ro里面探索一下属性,以及我们还未找到的成员变量hobby

    ro-baseProperties.png

    通过查看存储在ro里面的baseProperties其内容跟rw也是一样的,依旧没有我们的hobby,这个和类方法的思路不太一样,成员变量也是类里面的,我们暂不考虑去原类里面找成员变量。这时候我们发现还有个ivars可能存在我们想要的东西,下面我们查看一下ivars:

    ro-ivars.png

    结论:
    果不其然,我们的成员变量hobby就存在于这里,并且我们还发现了_nickname,所以成员变量和属性自动生成的带下划线的成员变量都存储在ivars里面。

    至此我们的探索已经差不多了,下面我们通过代码来验证一下我们的上面的探索:

    首先我们打印一下ivarsproperties

    实现代码:

    void testObjc_copyIvar_copyProperies(Class pClass){
        
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList(pClass, &count);
        for (unsigned int i=0; i < count; i++) {
            Ivar const ivar = ivars[i];
            //获取实例变量名
            const char*cName = ivar_getName(ivar);
            NSString *ivarName = [NSString stringWithUTF8String:cName];
            NSLog(@"class_copyIvarList:%@",ivarName);
        }
        free(ivars);
    
        unsigned int pCount = 0;
        objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
        for (unsigned int i=0; i < pCount; i++) {
            objc_property_t const property = properties[i];
            //获取属性名
            NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
            //获取属性值
            NSLog(@"class_copyProperiesList:%@",propertyName);
        }
        free(properties);
    }
    

    方法调用:

    Class pClass = [LGPerson class];
    testObjc_copyIvar_copyProperies(pClass);
    

    打印结果:

    ivars和properties.png

    实例方法打印:

    实现代码:

    void testObjc_copyMethodList(Class pClass){
        unsigned int count = 0;
        Method *methods = class_copyMethodList(pClass, &count);
        for (unsigned int i=0; i < count; i++) {
            Method const method = methods[i];
            //获取方法名
            NSString *key = NSStringFromSelector(method_getName(method));
            
            NSLog(@"Method, name: %@", key);
        }
        free(methods);
    }
    

    打印结果:

    imethods.png

    判断该类是否包含该实例方法:
    实现代码:

    void testInstanceMethod_classToMetaclass(Class pClass){
        
        const char *className = class_getName(pClass);
        Class metaClass = objc_getMetaClass(className);
        
        Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
        Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
    
        Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
        Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
        
        NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
        NSLog(@"%s",__func__);
    }
    

    打印结果:

    实例方法是否在类里面.png
    这里我们用到了LGPerson的实例方法sayHello和其类方法sayHappy,在获取实例方法的时候,在类中获取到了实例方法sayHello,在原类在获取到了sayHappy,说明类方法也是以实例方法的形式存储在原类中。

    下面我们在看看类中是否包含类方法:
    实现代码:

    void testClassMethod_classToMetaclass(Class pClass){
        
        const char *className = class_getName(pClass);
        Class metaClass = objc_getMetaClass(className);
        
        Method method1 = class_getClassMethod(pClass, @selector(sayHello));
        Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
    
        Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
        Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
        
        // 类方法形式
        NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
        NSLog(@"%s",__func__);
    }
    

    打印结果:

    类方法打印查看.png

    由于sayHello不是类方法,所前两个打印是0x0,但是后面的就有些出乎我们的意料了,在上面我们通过lldb查看时,并没有在类中找到类方法,下载打印是居然是有的,那么这到底是为什么呢?我们查看了class_getClassMethod的源码

    源码:

    /***********************************************************************
    * class_getClassMethod.  Return the class method for the specified
    * class and selector.
    **********************************************************************/
    Method class_getClassMethod(Class cls, SEL sel)
    {
        if (!cls  ||  !sel) return nil;
    
        return class_getInstanceMethod(cls->getMeta(), sel);
    }
    
    // NOT identical to this->ISA when this is a metaclass
    Class getMeta() {
        if (isMetaClass()) return (Class)this;
        else return this->ISA();
     }
    

    源码一看,一目了然,获取类方法的实质就是去原类里面查找原类的实例方法,上面我们也提到了,类方法本来就是已实例方法的形式存储在原类中,并且在获取原类的时候做了判断,如果是原类就直接返回Class,如果不是就返回类的isa,其实类的isa就是原类。所以为什么打印结果是刚才的样子也就清楚了。

    全篇总结

    Objective-C类有四个属性

    1. isa 指向原类;
    2. superClass 指向父类;
    3. cache 缓存调用过的方法,并通过哈希这种数据结构进行扩容,从4到65536,其中的mask作为一个掩码,用作哈希计算时的盐,避免哈希冲突,一直是减一的状态,所以一直不会满,保证哈希安全,也用作记录缓存大小,mask一直是缓存大小减1,所以获取到mask加上1就是缓存的大小。occupied作为开辟新空间(新缓存方法)的计数,以及判断是否到了临界点3/4处需要扩容的重要条件;
    4. bits 其中有rw存储了类的实例方法(methods)和属性(properties),rw中有个ro存储了类的实例方法(baseMethodList)、属性(baseProperties)和成员变量(ivars);
    5. 类的类方法以实例方法的形式存储在原类中。

    相关文章

      网友评论

          本文标题:iOS OC 类原理

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