iOS底层探索七(类的分析下)

 前言


    相关文章:   
       iOS底层探索一(底层探索方法)       

       iOS底层探索二(OC 中 alloc 方法 初探)

       iOS底层探索三(内存对齐与calloc分析)  

iOS底层探索四(isa初探-联合体,位域,内存优化)     

iOS底层探索五(isa与类的关系)  

iOS底层探索六(类的分析上)

iOS底层探索八(方法本质上)

iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)

iOS底层探索十(方法的本质下-消息转发流程)

    相关代码:
      objc4_752源码 

    温馨提示

这里先需要和大家解释一下,相关代码每次里面都只有objc4_752,因为这里探索的时候这份代码一直在github上,我这边会把每次探索中,需要对应的类会进行添加相应的注释,其中如果,有的人下载下来代码后,发现运行后结果不一样,例如XZPerson类中可能当前文章中需要里面有属性,成员变量,实例方法,类方法 等,会直接添加上,但是后续文章可能不需要,所以下载下来源码后,建议大家可以结合博文进行对应阅读,其中README.md文件中,也会添加上对应博文地址。

承接上篇文章

 上篇文章我们通过内存中的分析,分析到类中的:属性,成员变量,实例变量存储位置,首先我们找到了位置,也在内存中实实在在的看到了存在,到我们上层开发中怎么使用代码来查找

首先获取ro中 ivar中成员变量:

void testObjc_copyIvar(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);
}

输出结果: 

获取 ro中Properies属性

void testObjc_copyProperies(Class pClass){
    
    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);
}

输出结果

 获取MethodList 中的方法 

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);
}

输出结果:

查看元类和类中实例方法是否存在:

//获取类和元类中实例方法:
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__);
}

打印结果:

 可以得出结论:

  1. 类中只存储了sayHello 方法,元类中只存储了sayHappy方法,

  2. 元类中存储类方法是以实例方法的形式进行存储的

查看元类和类中类方法是否存在:

//查看类和元类中类方法查看方法
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__);
}

打印结果:

得出结论:

  1. sayhello作为实例方法不存在元类类方法
  2. sayHappy元类中都以类方法的形式存在;(这个在这篇文章中会进行分析)

这里其实上篇文章中探索的东西,上篇文章我们对类结构中的bits进行分析了,这篇文章我们继续对类结构中的另一个属性(cache)进行分析

struct objc_class : objc_object {
//    Class ISA;    //8字节
    Class superclass;  //父类  8字节
    cache_t cache; //缓存   结构体所占大小需要看内部定义16字节16字节
    // 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();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
...省略代码...

}

类结构中cache分析

要研究cache,首先我们先看一下cache_t结构体中都有什么东西:

struct cache_t {
    struct bucket_t *_buckets; // 结构体指针8字节
    mask_t _mask;  //typedef uint32_t mask_t;  4字节
    mask_t _occupied; // 4字节

public:
//    向下为函数,函数不占内存
    struct bucket_t *buckets();
...省略代码。。
}

和上篇文章一样,我们先看一下cache内存中都有什么,首先我们main函数中源码为:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        /***
         struct objc_class : objc_object {
         //    Class ISA;    //8字节
             Class superclass;  //父类  8字节
             cache_t cache; //缓存   结构体所占大小需要看内部定义16字节16字节
             // formerly cache pointer and vtable
             class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

         */
        XZPerson *p1 = [XZPerson alloc];
        Class pClass = [XZPerson class];
        NSLog(@"%@ --- %@",p1,pClass);
        
    }
    return 0;
}

NSLog处打断点,看下LLDB输出情况:

我们发现cache_t中没有任何出处东西,这就很奇怪了,调用之后没有做任何存储吗?

这里我们就要考虑一下了,缓存一般情况怎么才会做呢,第一次就会有缓存数据吗?显然不是的,一般第一次查询后,才会把数据缓存起来,后续进行查找的时候,就直接在缓存中查找了,那我们在main函数中做相应修改

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XZPerson *p1 = [XZPerson alloc];
        Class pClass = [XZPerson class];
        [p1 sayHello];

        NSLog(@"%@ --- %@",p1,pClass);
        
    }
    return 0;
}

多调用一个sayhello方法,然后看一下类的缓存情况,依然是在NSLog处打断点

这样我们就看到了 cache_t中的 bucket_t里面将 sayHello 方法进行缓存了!当我们第二次调用sayHello方法时就应该是从缓存中进行读取了。
我们尝试在XZPerson类中多添加几个方法:
@interface XZPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;

- (void)sayHello;
- (void)sayCat;
- (void)sayDog;
- (void)sayPig;
+ (void)sayHappy;

@end
#import "XZPerson.h"

@implementation XZPerson
- (void)sayHello{
    NSLog(@"XZPerson say : Hello!!!");
}
- (void)sayCat{
    NSLog(@"XZPerson say : Cat!!!");
}
- (void)sayDog{
    NSLog(@"XZPerson say : dog!!!");
}
- (void)sayPig{
    NSLog(@"XZPerson say : pig!!!");
}


+ (void)sayHappy{
    NSLog(@"XZPerson say : Happy!!!");
}

@end

在main函数中进行调用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        XZPerson *p1 = [XZPerson alloc];
        Class pClass = [XZPerson class];
        [p1 sayHello];
        [p1 sayCat];
        [p1 sayDog];
        [p1 sayPig];

        NSLog(@"%@ --- %@",p1,pClass);
        
    }
    return 0;
}

一样我们在NSlog处打断点,继续看一下cache中的内存情况

 
我们可以看出 mask的内存情况确实扩大了很多,但是在 bucket中之前存储的数据都没有了,这里我们就有疑问了,为什么存储的会没有呢,这就说明 cache这里就不是说来一个方法缓存一个而是有其他方式进行缓存的   cache的原理我们需要进行再次探究了。
 

cache缓存原理

1.0 cache猜测分析

 根据上面我们知道cache缓存不是来一个方法缓存一个方法,而是有特殊处理进行缓存的,但是我们应该怎么探究呢,我们可以看到_mask =7,说明_mask是有变化的,我们根据这个线索进行查找,首先查找一下_mask变化的方法:进入objc-runtime-new.h(cachet声明的类)其中找到mask变化方法mask_t mask();

mask_t cache_t::mask() 
{
    return _mask; 
}
  1. 找到这个方法为mask修改值的,继续查找有那些方法调用mask()方法并赋值,发现:mask_t cache_t::capacity方法:

  2. 继续寻找一下有那些方法调用capacity()这个方法:这里我们找到如下方法:cache_deletecache_erase_nolockcache_fill_nolockexpandisConstantEmptyCache:根据这几个方法意义我们可以看到肯定是和expand(扩容)方法有关,,

  3. 我们查看下有那些地方调用expand()方法,发现只有一处就是cache_fill_nolock()方法,说明这个方法很关键

2.0 cache断点跟踪+源码剖析

这里我们直接打断点到cache_fill_nolock这个方法进行跟踪一下进行分析;我们在main中调用第一个方法的时候打断点,走到这个方法后给cache_fill_nolock这个方法下断点:

2.1:cache_fill_nolock方法分析

调用sayHello方法cache_fill_nolock 断点正常进入我们对此代码进行分析

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.
//    从缓存中取IMP如果取到就直接返回
    if (cache_getImp(cls, sel)) return;
//如果没有回走下面
    cache_t *cache = getCache(cls);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
//    如果是第一次就直接创建空间
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
/**
 enum {
     INIT_CACHE_SIZE_LOG2 = 2,
     INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2) 1左移2位为4
 };
 */
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
//    如果这里小于3/4就继续了
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
//    如果大于3/4就需要扩容了
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // 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.
//    sel 为方法名 receiver 其实就是当前类
    bucket_t *bucket = cache->find(sel, receiver);
    if (bucket->sel() == 0) cache->incrementOccupied();
//    以sel为key imp为value进行写入数据
    bucket->set<Atomic>(sel, imp);
}

这个方法中我们需要关注的点:

  1. 系统在有缓存情况下直接获取缓存中方法IMP后会直接返回 if (cache_getImp(cls, sel)) return;

  2. 第一进入分配空间cache->reallocate

  3. 系统的扩容策略,第一次正常分配,当小于分配的3/4可以进行正常存放,当大于3/4时进行扩容 (expand)扩容方法

  4.  通过 sel 查找 bucket 。方法cache->find(sel, receiver)

2.2:reallocate方法分析

第一次进入cache中没有缓存肯定查找不到sayHello 方法,所以会进入cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)方法:

这里我们需要关注的进入(mask_t oldCapacity, mask_t newCapacity)是2个参数,

参数oldCapacity

cache_t::capacitp内部实现为:mask() ? mask()+1 : 0此时_mask是没有值的,所以这里传入值为0,即:oldCapacity:0

参数newCapacity

newCapacity因为oldCapacity为0所以必定取后面宏值

enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};

查看宏后发现为1左移2位就是4,所以第一次进入的时候这里传值为oldCapacity:0,newCapacity:4

//第一次进入old :0 New 为4
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);
//  这里将新的buckete和mask:4-1进行写入,这就可以看出第一个为mask =3
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

这个方法关注点:

  1. mask 被设置的值为开辟空间的 newCapacity-1:第一次为3

  2. 在开辟新的数组时,会释放旧 buckets (加入回收数组)这个是为什么我们调到第四个的时候,bucket里面没有存数据了

  3. 旧的缓存不会被计入新的数组中。保持局部性原理为最佳回到 cache_fill_nolock 的第二阶段,上面分析了第一个 if 即 reallocate ,在下面的 else if 中,也就是如果当前设置后的缓存数仍然小于总量的 3/4 ,则继续使用当前 buckets ,否则 else 进行扩容 expend

 

2.3、expend方法分析

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    /**
     enum {
         INIT_CACHE_SIZE_LOG2 = 2,
         INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2) 1左移2位为4
     };
     */
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; //4

    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);
}

这个方法比较简单。我们需要关注两点。

  1. 在不超过 uint32_t 的情况下,每次扩容为原来大小的 2 倍
  2. 如果超过了 uint32_t ,则重新申请跟原来一样大小的 buckets 。
  3. 扩容完成后会调用reallocate方法

2.4:find分析;

这里我们继续运行: bucket_t *bucket = cache->find(sel, receiver); 根据select和receiver进行查找bucket


bucket_t * cache_t::find(SEL s, id receiver)
{
    assert(s != 0);
 /***
  Method
  select 找到---- imp
  */ 
    bucket_t *b = buckets();
    mask_t m = mask();
    // 通过cache_hash函数【begin  = k & m】计算出key值 k 对应的 index值 begin,用来记录查询起始索引
    mask_t begin = cache_hash(s, m);
  // begin 赋值给 i,用于切换索引
    mask_t i = begin;
    do {
        if (b[i].sel() == 0  ||  b[i].sel() == s) {
            //用这个i从散列表取值,如果取出来的bucket_t的 key = k,则查询成功,返回该bucket_t,
            //如果key = 0,说明在索引i的位置上还没有缓存过方法,同样需要返回该bucket_t,用于中止缓存查询。

            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
       // 这一步其实相当于 i = i-1,回到上面do循环里面,相当于查找散列表上一个单元格里面的元素,再次进行key值 k的比较,
       //当i=0时,也就i指向散列表最首个元素索引的时候重新将mask赋值给i,使其指向散列表最后一个元素,重新开始反向遍历散列表,
       //其实就相当于绕圈,把散列表头尾连起来,不就是一个圈嘛,从begin值开始,递减索引值,当走过一圈之后,必然会重新回到begin值,
       //如果此时还没有找到key对应的bucket_t,或者是空的bucket_t,则循环结束,说明查找失败,调用bad_cache方法。
    
    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)s, cls);
}

从这里我们可以发现, cache_t 采用哈希表的方式来查找对应的 bucket ,哈希函数为 cache_hash ,目标数组为 buckets 。步骤如下:

  1.  准备哈希查找的 k 和目标数组 buckets 。

  2.  线性探测的方式查找目标 k ,直到找到 k 或者空的 bucket 。
  3.  异常处理。

到这里大体的流程已经梳理清晰,但还有一个比较重要的问题。我们知道 msgSend 是可多线程并发执行的,那么 cache_t 在更新缓存时,如何处理线程安全的问题呢。

  1. 在每次执行缓存填充,和扩充都会添加对应的互斥锁。
  2.  在更新 buckets 和 mask 时,会使用 mega_barrier 来保证 buckets 的更新一定早于 mask 。(如果不保证会有数组越界的问题)
  3. 在回收旧的 buckets 时,会把需要释放的 buckets 加入一个全局的数组 garbage_refs 中。等待真正没有其他线程使用数组中的元素时,在进行释放。

断点验证:

我们先用走一次放发第一次进入sayHello方法:

进入

set完成后

 我们可以看到和我们源码分析的一样mask为3 ,sel存放sayHello,

接下来我们直接看扩容情况走第四个方法时

进入expand方法

我们可以看到Newcapacity为8 继续进入reallocate方法

这里我们稍微说明下,为什么要把老数组进行清空,而不是直接扩容进行重新复制并添加上去,因为在调用方法,是需要一个很快速的流程,不能允许有读写操作,容易出错。

继续我们直接看set后结果

我们可以看到已经将之前存入的数据都已经清空了;

这里我们对cachet缓存流程进行梳理一下调用方法后底层通过objc_msg_send方法找到cache_fill_nolock方法后:

  1. 根据clssel进行缓存查找,找到后直接返回:cache_getImp(cls, sel)
  2.  对容量值进行初始化mask_t capacity = cache->capacity(),第一次创建为4

  3. 判断当前即将存储的缓存是有控件

         3.1:是否有空间存储,没有创建空间(reallocate)

         3.2:有空间后判断是否超过3/4剩余空间,未超过直接存储

         3.3:超过后进行2倍扩容存储expand

         3.3.1(这里需要注意的是实际扩容mask大小为2倍-1大小)后调用reallocate方法

     4.这里需要注意的是reallocate方法中会抹掉之前存储的数据,在这里没有对数据填充也没有对_occupied操作

     5.然后调用find使用selreceiver找到类中的方法存储为bucket位置(如果有就是原始位置,没有就是新位置)

          5.1:通过cache_hash(这个方法确实很牛,完全没这么理解,直接用字符串&数字)查找初始位置,一般情况就直接找到的第一个空位置,如果有方法,就直接到这个方法的位置(这个可能性不大,有方法第一步就直接返回了)

          5.2:do,while,进行循环遍历查找bucket 位置,有返回没有返回最近的一个sel为空(即最近的一个未赋值bucket)

          5.3: 还没有找到bad_cache就是损坏的缓存区域

     6.判断位置中的sel是否存在,不存在填充数量_occupied进行++操作

     7.将新的imp和sel存储到找到的bucket中

这里稍微分析下苹果大大为什么初始开辟为4 而不是直接开辟很大呢,因为如果直接开辟太大的话,太浪费空间了;

为什么扩容的时候要销毁之前存储的缓存呢,这里我们要考虑如果这里方法比较多呢,999个方法,扩容为1999,将999个方法进行读写,读写操作是比较耗时的,我们做缓存就是为了快,这里添加了读写操作,岂不是让速度慢了下来

总结:

此篇文章主要分析了下类结构中的cache中的属性,以及cache中取缓存的方式,以及扩容策略等,以上便是我对OC中类的cache探究,如果有错误的地方还请指正,大家一起讨论,开发水平一般(文章中有错误后,我发现会第一时间对博文进行修改),还望大家体谅,欢迎大家点赞,关注我的CSDN,我会定期做一些技术分享!

写给自己

人一辈子,就是这样:来是偶然,去是必然,尽其当然,顺其自然。得之淡然,失之泰然,争其必然,自然而然,未完待续。。。

 

 

原创文章 88 获赞 21 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZhaiAlan/article/details/104789766