iOS的OC的方法缓存的源码分析
2020/6/2 23:26:45
本文主要是介绍iOS的OC的方法缓存的源码分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
笔者整理了一系列有关OC的底层文章,希望可以帮助到你。这篇文章主要讲解的是方法缓存的底层源码分析。
1.iOS的OC对象创建的alloc原理
2.iOS的OC对象的内存对齐
3.iOS的OC的isa的底层原理
4.iOS的OC源码分析之类的结构分析
在日常开发中,我们调用方法的时候有没有想过一个问题,在我们频繁地调用方法,为了高效苹果会不会对使用过的方法做缓存起来?如果有做缓存的话,具体是怎样做的呢?为了了解这块的内容,本篇文章就对cache_t
做源码分析。
1.cache_t
在上一篇文章iOS的OC源码分析之类的结构分析中知道cache_t是在objc_class
结构体中,占16个字节,cache_t
的源码如下:
struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; ... }; 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__ MethodCacheIMP _imp; cache_key_t _key; #else cache_key_t _key; MethodCacheIMP _imp; #endif } using MethodListIMP = IMP; typedef uintptr_t cache_key_t; 复制代码
从源码可以知道,通过将方法编号SEL
和函数地址IMP
缓存在bucket_t
(又称哈希桶)中。
为了方便接下来的内容,定义了一个TestObject
的类,具体的代码如下:
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface TestObject : NSObject{ NSString *nickName; } @property(nonatomic,copy) NSString *name; -(void)sayName; -(void)sayHello; -(void)sayTest; +(void)sayNickName; @end NS_ASSUME_NONNULL_END #import "TestObject.h" @implementation TestObject -(void)sayName{ NSLog(@"%p",__func__); } -(void)sayHello{ NSLog(@"%p",__func__); } -(void)sayTest{ NSLog(@"%p",__func__); } +(void)sayNickName{ NSLog(@"%p",__func__); } @end //实现的代码 TestObject *testObject = [TestObject alloc]; Class tClass = object_getClass(testObject); [testObject sayName]; [testObject sayHello]; NSLog(@"%@",testObject); 复制代码
因为实例对象里面的方法是在类里面调用的,为了验证实例方法是不是存在cache_t
里面,我们可以通过lldb
的指令来找到cache_t
然后深入进去查看,如下图所示
TestObject
类的三个方法(包括了init方法),图中的_mask
和_occupied
都为3。那么我们再调用多一个方法,如下所示
TestObject *testObject = [[TestObject alloc] init]; Class tClass = object_getClass(testObject); [testObject sayName]; [testObject sayHello]; [testObject sayTest]; NSLog(@"%@",testObject); 复制代码再次使用
lldb
的指令来查看,可以知道此时的_mask
为7,但是_occupied
为1,并且此时的buckets
的数组里面只有一个sayTest
方法,还是不是有序存放,此时其他的方法不存在了。所以由此可以知道,方法的缓存并不是有一个存一个的,里面是有对方法的缓存做一定的处理的。
1.1 cache_t的属性值
- _buckets:是
bucket_t
结构体的数组,bucket_t
是用来存放方法的SEL内存地址和IMP的。 - _mask:是数组容量的大小用作掩码。(因为这里维护的数组大小都是2的整数次幂,所以_mask的二进制位000011,000111,001111)刚好可以用作hash取余数的掩码。刚好保证相与后不超过缓存大小。
- _occupied:是当前已缓存的方法数,即数组中已使用了多少位置。
2.方法缓存的原理分析
OC方法的本质是消息发送
(即objc_msgSend),底层是通过方法的 SEL 查找 IMP。读取cache_t
缓存是通过objc_msgSend
的查找,cache_t
缓存的写首先是通过cache_fill
函数,如下源码:
* 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) 复制代码
2.1 cache_fill
方法的缓存首先是通过cache_fill
函数,源码如下
void cache_fill(Class cls, SEL sel, IMP imp, id receiver) { #if !DEBUG_TASK_THREADS mutex_locker_t lock(cacheUpdateLock); cache_fill_nolock(cls, sel, imp, receiver); #else _collecting_in_critical(); return; #endif } 复制代码
cache_fill
方法传入cls类的Class
和方法的SEL
,IMP
。
2.2 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. if (cache_getImp(cls, sel)) return; cache_t *cache = getCache(cls); cache_key_t key = getKey(sel); // 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. cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); } else if (newOccupied <= capacity / 4 * 3) { // Cache is less than 3/4 full. Use it as-is. } 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. bucket_t *bucket = cache->find(key, receiver); if (bucket->key() == 0) cache->incrementOccupied(); bucket->set(key, imp); } cache_t *getCache(Class cls) { assert(cls); return &cls->cache; } cache_key_t getKey(SEL sel) { assert(sel); return (cache_key_t)sel; } /* 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) }; #if __LP64__ typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits #else typedef uint16_t mask_t; #endif typedef uintptr_t cache_key_t; 复制代码
从源码中各个方法来分析一下,其中的getCache(cls)
通过cls
来获取到类的cache_t
。getKey(sel)
将SEL
转化为cache_key_t
的类型。下面是
cache->occupied()
和cache->capacity()
的源码。
mask_t cache_t::occupied() { return _occupied; } mask_t cache_t::capacity() { return mask() ? mask()+1 : 0; } mask_t cache_t::mask() { return _mask; } 复制代码
_occupied
是方法的数量,默认是0,所以一开始进来的话newOccupied
的值是1相当于占用1个缓存的数量来做缓存,而capacity()
是获取缓存的方法数量,默认也是0的,如果mask()
有值了就是在这个基础上加1
,这就相当于获取方法的容量。接下来就是三个的条件判断了,第一个判断isConstantEmptyCache()
是判断是否有缓存,第二个判断是判断占用的方法数量是否小于等于容量的3/4,如果是就什么都不做。否则就需要开始扩容expand
。如果没有缓存的话就需要执行reallocate
函数。其中reallocate
中的INIT_CACHE_SIZE
是4
,所以一开始传进去的reallocate
的值是0和4.
2.2.1 reallocate
从函数名的大概可以看出意思,就是重新初始化缓存的意思。这个函数的源码如下:
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { //判断是否可以释放旧的缓存的标示 bool freeOld = canBeFreed(); //获取旧的buckets bucket_t *oldBuckets = buckets(); //创建新的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); //设置新的buckets和赋值mask setBucketsAndMask(newBuckets, newCapacity - 1); if (freeOld) { //释放旧的buckets cache_collect_free(oldBuckets, oldCapacity); cache_collect(false); } } bool cache_t::canBeFreed() { return !isConstantEmptyCache(); } bucket_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. // fixme instead put the end mark inline when +1 is malloc-inefficient bucket_t *newBuckets = (bucket_t *) calloc(cache_t::bytesForCapacity(newCapacity), 1); bucket_t *end = cache_t::endMarker(newBuckets, newCapacity); #if __arm__ // End marker's key is 1 and imp points BEFORE the first bucket. // This saves an instruction in objc_msgSend. end->setKey((cache_key_t)(uintptr_t)1); end->setImp((IMP)(newBuckets - 1)); #else // End marker's key is 1 and imp points to the first bucket. end->setKey((cache_key_t)(uintptr_t)1); end->setImp((IMP)newBuckets); #endif if (PrintCaches) recordNewCache(newCapacity); return newBuckets; } void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask) { // objc_msgSend uses mask and buckets with no locks. // It is safe for objc_msgSend to see new buckets but old mask. // (It will get a cache miss but not overrun the buckets' bounds). // It is unsafe for objc_msgSend to see old buckets and new mask. // Therefore we write new buckets, wait a lot, then write new mask. // objc_msgSend reads mask first, then buckets. // ensure other threads see buckets contents before buckets pointer mega_barrier(); _buckets = newBuckets; // ensure other threads see new buckets before new mask mega_barrier(); _mask = newMask; _occupied = 0; } 复制代码
从源码中可以看到reallocate
获取旧的buckets和创建新的buckets,因为旧的buckets在判断可以释放的时候是需要抹掉的。创建新的buckets在allocateBuckets
函数可以知道,通过calloc
函数来申请cache_t
类型的内存空间,并且对key
和imp
都设置了默认值。在setBucketsAndMask
函数中对buckets和_mask赋值,因为一开始传进来的newMask
为3,_occupied为0之所以为0是因为此时还没有对方法做缓存只是初始化值。这就很好地说明了上面第一次用lldb
指令的时候得到的mask为3.
2.2.2 expand
在newOccupied
的值大于capacity
的3/4,这时候就需要扩容,这时候就需要执行expand()
函数
void cache_t::expand() { cacheUpdateLock.assertLocked(); uint32_t oldCapacity = capacity(); uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; 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); } mask_t cache_t::capacity() { return mask() ? mask()+1 : 0; } 复制代码
在需要扩容的时候,此时的capacity()
值为4了,所以oldCapacity
为4,newCapacity
为8,然后会继续执行reallocate
函数,传进去的参数分别为4和8。根据上面的reallocate
函数的执行流程会将旧的buckets清空,修改mask
的值为7,然后occupied的值为0.但是为什么会在lldb
的指令的时候看到的occupied
为1呢?在这个流程走完之后,执行完判断的流程之后,会执行到
// 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. bucket_t *bucket = cache->find(key, receiver); if (bucket->key() == 0) cache->incrementOccupied(); bucket->set(key, imp); void cache_t::incrementOccupied() { _occupied++; } 复制代码
其中find
函数通过上面的key
和receiver
来查找bucket_t。如果key()
为0的时候,这时会对_occupied
数量+1。并且对bucket的key和imp进行填充。
2.2.3 find函数
bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k != 0); bucket_t *b = buckets(); mask_t m = mask(); // 通过cache_hash函数【begin = k & m】计算出key值 k 对应的 index值 begin,用来记录查询起始索引 mask_t begin = cache_hash(k, m); // begin 赋值给 i,用于切换索引 mask_t i = begin; do { if (b[i].key() == 0 || b[i].key() == k) { //用这个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)k, cls); } static inline mask_t cache_hash(cache_key_t key, mask_t mask) { return (mask_t)(key & mask); } 复制代码
从find
函数可以知道,通过mask的大小与获取的key用hash函数的形式得到begin下标来得到bucket_t的地址进行返回,因为hash函数是无序的,所以在buckets
里面存放的位置也是无序的。
在类的
cache_t
中是找不到类方法的,因为类方法都是缓存在元类
中,所以如果想通过lldb
指令来查找类方法,可以先通过isa
找到元类,可以根据上面的流程来验证元类中是不是存放类方法。
3.最后
OC方法的本质是消息发送
(即objc_msgSend),底层是通过方法的 SEL 查找 IMP。
- 1.方法缓存在
cache_t
中,分别用buckets
指针地址来存方法数组,mask
来存放方法数组的容量大小,occupied
来存放当前的方法占用数量。 - 2.在方法的
newOccupied
新的方法占用数量大于当前的方法数量capacity()
的3/4就需要扩容。 - 3.在扩容的过程中,会设置
mask
为capacity() * 2 - 1
即方法的数量的2倍减1,例如第一次为3,第二次为7。最后都会将旧的buckets
列表清空。但是最后都会将执行到需要扩容的方法加入到buckets
里面。
这篇关于iOS的OC的方法缓存的源码分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值