欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-24 14:00:16
...

 前言


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

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

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

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

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

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

输出结果: 

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

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

输出结果

iOS底层探索七(类的分析下) 获取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);
}

输出结果:

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

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

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

打印结果:

iOS底层探索七(类的分析下) 可以得出结论:

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

打印结果:

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

得出结论:

  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输出情况:

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

我们发现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处打断点

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

这样我们就看到了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中的内存情况

iOS底层探索七(类的分析下)
 
我们可以看出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这个方法下断点:

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

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方法:

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

进入

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

set完成后

iOS底层探索七(类的分析下) 我们可以看到和我们源码分析的一样mask为3 ,sel存放sayHello,

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

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

进入expand方法

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

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

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

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

继续我们直接看set后结果

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

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

总结:

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

写给自己

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