底层原理-06-类的cache_t分析

274 阅读6分钟

1.cache_t的源码分析

之前我们探索了类的claas_data_bits_t ,今天来分析下cache_t ,先打印下LGPerson

(lldb) x/4gx LGPerson.class
0x100008408: 0x0000000100008430 0x000000010036a140
0x100008418: 0x0000000101b1bd40 0x0002802800000003
(lldb) p/x (cache_t *)0x0000000100008440
(cache_t *) $1 = 0x0000000100008440
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298515376
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32808
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802800000000
      }
    }
  }
}

进入源码:

 explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
    };

我们对缓存操作肯定是对属性或者方法进行存储和读取,思路是查找cache_t对应的方法

   void insert(SEL sel, IMP imp, id receiver);
   |
   void cache_t::insert(SEL sel, IMP imp, id receiver)
{。。。。

    bucket_t *b = buckets();
    mask_t m = capacity - 1; // 4-1=3
    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.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, 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));
    }

中间关键是对bucket_t 进行操作,存储方法 点击查看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
。。。
}

这里说明下:架构(真机) : arm64; 模拟器 : i386; mac : x86_64
__LP64__等一些宏定义的意义

截屏2021-06-23 上午11.48.23.png 继续之前的探索,我们之前探索class 中的bits 时候去找属性列表,方法列表都有对应的方法,我们继续查找cache_t 发现它有buckets() 方法 返回的是 bucket_t *,里面包含了_imp_sel。继续打印调试

(lldb) p $2.buckets()
(bucket_t *) $3 = 0x00000001003623b0
(lldb) p *$3
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}

是空的没有缓存的方法,我们调用一下方法:

p [p saySomething]
2021-06-23 14:55:25.722111+0800 KCObjcBuild[50955:1160095] -[LGPerson saySomething]
(lldb) p *$1
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4301690528
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000007
      }
    }
  }
}

_occupied变成了1,代表已占用的标识。_maybeMask 为7,发生了改变。我们继续打印

p $3.buckets()
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}

还是空的为什么?去源码中看 buckets()

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

buckets看起来是一个数组,复数s,这里说下数组和链表的概念
数组:

  • 在内存中,数组是一块连续的区域。
  • 数组需要预留空间,在使用前要先申请占内存的大小,可能会浪费内存空间。
  • 插入数据和删除数据效率低,插入数据时,这个位置后面的数据在内存中都要向后移。
  • 随机读取效率很高。因为数组是连续的,知道每一个数据的内存地址,可以直接找到给地址的数据。
  • 并且不利于扩展,数组定义的空间不够时要重新定义数组。 链表:
  • 在内存中可以存在任何地方,不要求连续。
  • 每一个数据都保存了下一个数据的内存地址,通过这个地址找到下一个数据。 第一个人知道第二个人的座位号,第二个人知道第三个人的座位号……
  • 增加数据和删除数据很容易。 再来个人可以随便坐,比如来了个人要做到第三个位置,那他只需要把自己的位置告诉第二个人,然后问第二个人拿到原来第三个人的位置就行了。其他人都不用动。
  • 查找数据时效率低,因为不具有随机访问性,所以访问某个位置的数据都要从第一个数据开始访问,然后根据第一个数据保存的下一个数据的地址找到第二个数据,以此类推。 要找到第三个人,必须从第一个人开始问起。
  • 不指定大小,扩展方便。链表大小不用定义,数据随意增删。
    那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”
    数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素
    buckets就是这样的方式进行存储 继续打印首位置进行内存平移
(lldb) p $3.buckets()[1]
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 49064
    }
  }
}
(lldb) p $4.sel()
(SEL) $7 = "saySomething"
(lldb) p $4.imp(nil,pClass)
(IMP) $8 = 0x0000000100003be0 (KCObjcBuild`-[LGPerson saySomething])

就是我们想要的结果 继续打印:

lldb) p/x pClass
(Class) $0 = 0x0000000100008448 LGPerson
(lldb) p (cache_t*)0x0000000100008458
(cache_t *) $1 = 0x0000000100008458
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298515376
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32808
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802800000000
      }
    }
  }
}
(lldb) p [p saySomething]
2021-06-23 15:43:43.577791+0800 KCObjcBuild[51131:1181293] -[LGPerson saySomething]
(lldb) p *$1
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4313870720
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000007
      }
    }
  }
}
(lldb) p [p saySomething]
2021-06-23 15:43:59.324172+0800 KCObjcBuild[51131:1181293] -[LGPerson saySomething]
(lldb) p *$1
(cache_t) $4 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4313870720
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 3
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0003802800000007
      }
    }
  }
}
(lldb) p [p saySomething]
2021-06-23 15:45:04.046302+0800 KCObjcBuild[51131:1181293] -[LGPerson saySomething]
(lldb) p *$1
(cache_t) $5 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4313870720
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 3
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0003802800000007
      }
    }
  }
}

开始么有调用方法的时候_occupied 为0,调用一次后_occupied 为1,再次调用后_occupied 为3,在之后调用不发生改变。探究下缓存的过程做了什么

2.cache_t的inset流程

我们第一次调用方法,所以没有缓存,会直接走入方法查找lookUpImpOrForward方法中接着会进入log_and_fill_cache,然后cache_fill,在cache_fill方法中我们找到了cache->insert

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
 mask_t newOccupied = occupied() + 1; // 1+1累加每次进来+1
    unsigned oldCapacity = capacity(), capacity = oldCapacity;计算容量。现在是否有空间有的+1,没有的话是0
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//4
        //创建4个容量的buckets
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    //CACHE_END_MARKER为1,在真机下arm64值为0.新的newOccupied+1(或者0)是否是之前开辟空间的小于等于75%是的话不用开辟了,
    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
    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 {// 4*2 = 8,是否有,有的话扩容*2,么有就是4
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;//是否超过最大,超过就按最大的32来
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1; // 4-1=3
    mask_t begin = cache_hash(sel, m);//用mask进行哈希
    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)) {
            incrementOccupied();// _occupied++;累加,开辟新空间的时候自动_occupied = 0并释放老的里面存储空间内容
            b[i].set<Atomic, Encoded>(b, 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_next进行循环查找

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

insert方法可以得知cache在一开始没有的话会分配了4个容量,在armi386(模拟器)、x86_64(mac)下需要特殊的结束标记存储区进行换行,所以CACHE_END_MARKER为1,arm64下不需要结束标记故为0,在newOccupied + 0 <= capacity / 4 * 3,大于时如果capacity有值就按2倍扩容,为空就设置为4。 还可以看到通过buckets中的根据i找到sel0对比来查找buckets中的空位置,找到后就把occupied加1,未使用的插槽并插入那里。存在的话就不需要处理了。 这里有个cache_next, 在arm64下是通过i-1来得出i, 而在arm、i386、x86_64下是通过(i + 1) & mask得出的i。

  • 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);//mask_t和newCapacity关联减1

    setBucketsAndMask(newBuckets, newCapacity - 1);//把newBuckets和mask_t进行关联 _occupied =0
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);//释放老的
    }
}
  • allocateBuckets
bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
    // Allocate one extra bucket to mark the end of the list.
    // This can't overflow mask_t because newCapacity is a power of 2.
    bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);

    bucket_t *end = endMarker(newBuckets, newCapacity);//进行标记的

#if __arm__
    // End marker's sel is 1 and imp points BEFORE the first bucket.
    // This saves an instruction in objc_msgSend.
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);//__arm__ 结束标记的sel是1,imp点在第一个桶之前。
#else
    // End marker's sel is 1 and imp points to the first bucket.
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);结束标记的sel是1,imp点在第一个桶
#endif
    
    if (PrintCaches) recordNewCache(newCapacity);

    return newBuckets;
}

allocateBuckets 方法在CACHE_END_MARKER为1才会进行end标记,否则直接开辟空间,真机的情况不会进行标记。

3.自定义方法,探究

我们开发中可以模仿系统的写法,重写或者自定义

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
struct kb_bucket{
    SEL _sel;
    IMP _imp;
};
struct kb_cache_t{
  
    struct kb_bucket *buckets;//8
    mask_t mask;//4
    uint16_t flags; //2
    uint16_t _occpied;//2
};

struct kb_bits
{
    uintptr_t bits;

    
};
struct kb_objc_class
{
    Class isa;
    Class superClass;
    struct kb_cache_t cache_t;
    struct kb_bits bits;
    
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        KBPerson *kb = [KBPerson alloc];
        [kb sayNB1];
        [kb sayNB2];
        [kb sayNB3];
//        [kb sayNB4];
//        [kb sayNB1];
//        [kb sayNB2];
//        [kb sayNB3];
        
        struct kb_objc_class *kb_class =(__bridge struct kb_objc_class*)(KBPerson.class);
        struct kb_cache_t kb_cache = kb_class->cache_t;
        NSLog(@"%u-%u",kb_cache._occpied,kb_cache.mask);
        
        for (mask_t i=0; i<kb_cache.mask; i++) {
            struct kb_bucket bucket = kb_cache.buckets[i];
            NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
        }
        NSLog(@"Hello, World!");
    }
    return 0;
}

运行结果: 没有经过lldb调试也验证了cache_t的变化

2021-06-24 13:51:36.250227+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB1]
2021-06-24 13:51:36.250663+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB2]
2021-06-24 13:51:36.250703+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB3]
2021-06-24 13:51:36.250728+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB4]
2021-06-24 13:51:36.250750+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB1]
2021-06-24 13:51:36.250771+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB2]
2021-06-24 13:51:36.250793+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB3]
2021-06-24 13:51:36.250817+0800 自定义Cache_t分析[58217:1606919] 4-7
2021-06-24 13:51:36.251113+0800 自定义Cache_t分析[58217:1606919] sayNB1 - 0xbd20f
2021-06-24 13:51:36.251170+0800 自定义Cache_t分析[58217:1606919] sayNB2 - 0xbcf0f
2021-06-24 13:51:36.251206+0800 自定义Cache_t分析[58217:1606919] (null) - 0x0f
2021-06-24 13:51:36.251255+0800 自定义Cache_t分析[58217:1606919] (null) - 0x0f
2021-06-24 13:51:36.251280+0800 自定义Cache_t分析[58217:1606919] (null) - 0x0f
2021-06-24 13:51:36.614742+0800 自定义Cache_t分析[58217:1606919] sayNB4 - 0xbc50f
2021-06-24 13:51:36.614781+0800 自定义Cache_t分析[58217:1606919] sayNB3 - 0xbc80f

_maybeMask代表可用的空间字节,_occupied已用空间字节,buckets是一个哈希表通过内存平移得到对应的bucketbucket里面存放了对应的SELIMP,存入的时候第一次开辟一个4字节的内存存入bucket,之后判断是否是_occupied +1 +CACHE_END_MARKER 是否小于等于当前空间的75%(#if arm || x86_64 || i386
#define CACHE_END_MARKER 1
#elif arm64 && !LP64
#define CACHE_END_MARKER 0)不小于就当前capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE扩容2倍,同是释放以前存储的buckets_occupied =0 之后_occupied继续累加

4.cache_t的补充

  • 之前我们在lldb调试的时候

截屏2021-06-25 上午7.23.11.png

_bucketsAndMaybeMask的Value是有值的,官方说_bucketsAndMaybeMask is a buckets_t pointer是一个buckets_t指针,所以

截屏2021-06-25 上午7.32.33.pngbuckets()一样,之后指针平移得到对应的bucket_t

  • bucket_tselimp的读取 sel:

截屏2021-06-25 上午7.42.09.png imp:

截屏2021-06-25 上午7.50.12.png

  • 类方法的调用 之前我们知道类方法存在元类中,那么类类方法的缓存是不是也在里面验证下

截屏2021-06-25 下午5.04.54.png

  • 最后写个大概的流程图

截屏2021-06-25 下午6.26.14.png