iOS OC 类原理

iOS OC底层探索

  • iOS Objective -C alloc 调用流程

  • iOS Object-C init&new

  • iOS OC 对象的内存对齐原则

  • iOS Objective-C isa

  • iOS Objective-C isa 走位分析

  • iOS OC 类原理

  • iOS OC 方法的本质

  • iOS Objective-C 消息的查找

  • iOS Objective-C 消息的转发

  • iOS 应用加载dyld

  • Mach-O探索

  • iOS开发中『库』的区别与应用

  • iOS 应用的加载objc

  • iOS Objective-C 分类的加载

  • iOS Objective-C 类扩展

  • iOS Objective-C 关联对象

  • iOS Objective-C KVC 的常见用法

  • iOS Objective-C KVC 详解

  • iOS Objective-C KVO 常见用法

  • iOS Objective-C KVO 详解

  • iOS多线程 基础

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

1. 类

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

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

  • 我的另一篇文章:通过Clang 看OC对象的本质

在这篇文章中我们说道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 _buckets;
    explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic _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 _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 _imp;
    explicit_atomic _sel;
#else
    explicit_atomic _sel;
    explicit_atomic _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
    {
        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(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 类原理)