iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
序言
在前面文章类的结构中,我们分析了bits
的结构,isa
以及superclass
是为指针类型,还剩下一个cache
没有分析,cache
顾名思义就是缓存相关的,今天就来看一下cache
是怎么个原理。
cache的数据结构
先自定义一个类LGPerson
,代码实现
LLDB输出数据结构
用LLDB
调试输出,查看cache
的数据
有几个关键数据:_bucketsAndMaybeMask
、_maybeMask
、_flags
、_occupied
、_originalPreoptCache
。
cache源码数据结构
然后我们再看cache_t
的源码结构
总结
_bucketsAndMaybeMask
:一个uintptr_t
类型的指针;- 联合体:一个结构体和
preopt_cache_t
结构体类型的指针变量_originalPreoptCache
;_maybeMask
:mask_t
泛型的变量;_flags
:uint16_t
类型变量;_occupied
:uint16_t
类型变量preopt_cache_t
:preopt_cache_t
结构体类型的指针变量;
这里我们还无从知道cache
是怎样缓存的数据,以及缓存的是什么数据,是属性
还是方法
呢?
cache缓存数据类型
既然通过cache_t
的数据结构看不出来,那我们就找方法
。
缓存应该有增删改查
等方法,那就从这些方法下手吧。通过阅读源码,我们看到有一个insert
方法和copyCacheNolock
方法
在insert
方法中,插入的是SEL
和IMP
,由此可以看出cache
缓存的数据是方法method
,然后再看一下insert
的实现,找一下SEL
和IMP
是缓存在哪里。
cache缓存的存储位置
这里很明显是一个bucket_t
类型的b
,调用set
方法插入SEL
和IMP
以及关联的Class
。
看一下bucket_t
的结构。
这里我们可以简单总结一下类中cache_t
的结构
cache缓存数据输出查看
现在我们已经找到了cache缓存的方法
是存在bucket_t
中,并且bucket_t
有成员变量_sel
和_imp
,在insert
中是通过方法buckets()
获取到的bucket_t
,那我们就找到输出一下。
LLDB找到cache缓存数据
在cache_t
的结构体定义中,正好有buckets()
方法,那我们在LLDB
中获取到cache
的地址变量就可以输出bucket_t
。
声明一个LGPerson
类型的变量p
,并调用对象方法sayHello
然后我们用LLDB
调试输出信息
我们成功获取到了bucket_t
类型的$3
,但当我查看$3
的内容是缺还是空值
。why!!!why!!!why!!!
还是回归到insert
源码,看一下到底是怎么插入的缓存吧。
天呢!漏了一个细节,这里缓存插入的时候是用了hash
算法取下标
的方式,那我们上面取到的第一个bucket_t
的就可能为空值
。
既然这样,buckets()
的存储结构是一个哈希数组,那我们就继续往下面找bucket_t
这里的_sel
中的Value
和_imp
中的Value
明显和上面的不一样了,不再是nil
和0
,那我们可以猜测这是一个有效的bucket_t
。
找到bucket_t
结构体中的方法sel()
和imp()
,输出一下
Done!!
这里我们成功找到了缓存的方法sayHello
,但是我发现在LLDB
这样调试很是麻烦,而且还依赖于源码
的运行环境,如果有系统升级
或者源码有更新
,编译不了源码,难道只能GG
吗,所以能不能脱离源码编译环境
也能搞定上面的步骤呢
脱离源码分析cache
我们的目的是获取cache_t
里面的bucket_t
,cache_t
是在objc_class
里面,那我们就按照源码objc_class
的结构去自定义一个相似的结构体,这样就可以通过NSLog
输出获取的内容信息。
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct lg_bucket_t {
SEL _sel;
IMP _imp;
};
struct lg_cache_t {
struct lg_bucket_t * _buckets;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct lg_class_data_bits_t {
uintptr_t bits;
};
struct lg_objc_class {
Class isa;
Class superclass;
struct lg_cache_t cache; // formerly cache pointer and vtable
struct lg_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson * p = [LGPerson alloc];
[p sayHello];
Class pClass = [LGPerson class];
struct lg_objc_class *lg_class = ( __bridge struct lg_objc_class *)(pClass);
NSLog(@" - %hu - %u",lg_class->cache._occupied,lg_class->cache._maybeMask);
for (int i = 0; i < lg_class->cache._maybeMask; i++) {
struct lg_bucket_t bucket = lg_class->cache._buckets[i];
NSLog(@"SEL = %@ --- IMP = %p", NSStringFromSelector(bucket._sel), bucket._imp);
}
NSLog(@"Hello, World!");
}
return 0;
}
运行上面的代码,查看输出
成功输出,这里的_occupied
为1,_maybeMask
为3,我们再调两个方法sayHello_1
和sayHello_2
验证一下。
这里发生了蹊跷,_occupied
为1,_maybeMask
变成了7,而缓存中只有方法sayHello_2
,我们调用的sayHello
和sayHello_1
却不在缓存中。既然这样,那就从头捋一遍源码
,看看是不是又漏下什么细节
了。
cache
底层原理分析
对于底层原理分析,就从cache_t
的插入方法insert
入手
insert
源码
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
// Never cache before +initialize is done
if (slowpath(!cls()->isInitialized())) {
return;
}
if (isConstantOptimizedCache()) {
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
ASSERT(sel != 0 && cls()->isInitialized());
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) { // 1.判断当前缓存是否为空的
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE; // 1 << 2 = 4
reallocate(oldCapacity, capacity, /* freeOld */false); //开辟内存
}
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 {
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.
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));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
整个代码流程我们按调用次数
分布解析
第一次调用方法insert
第一次插入缓存时
_occupied
的值为0,所以newOccupied
为1;capacity()
取的是_maybeMask
的值,所以oldCapacity
和capacity
值都为0;isConstantEmptyCache
判断当前缓存是否为空,条件成立,进入if
语句;capacity
值为0,赋值为INIT_CACHE_SIZE
,INIT_CACHE_SIZE = 1 << 2
值为4;- 调用
reallocate
开辟内存;
再reallocate
方法中,开辟新内存,然后调用setBucketsAndMask
方法,使cache_t
中的成员变量_bucketsAndMaybeMask
、_maybeMask
、_occupied
做关联
之后是再开辟的缓存空间中存入SEL
和IMP
在存入SEL
和IMP
的方法中,有对IMP
进行编码,实际上存入的是编码后的newImp
对imp
编码的源代码
我们用到的CACHE_IMP_ENCODING
情况为CACHE_IMP_ENCODING_ISA_XOR
,所以上面的编码算法是imp & cls
。
我们知道了
第一次
调用方法,会开辟空间为4
的缓存空间
,当我们调用更多方法的时候,应该在什么时候扩容呢?
四分之三扩容
当我们不是第一次
调用方法时,就会进入一个剩余空间容量
判断
newOccupied
:进入缓存的第几个方法;CACHE_END_MARKER
:宏定义值为1;cache_fill_ratio(capacity)
:capacity * 3 / 4
容量的3/4值;
这里我们知道当新的调用方法进入缓存时
- 如果
不满足扩容条件
,就会继续往开辟的缓存空间
插入一条缓存数据
。比如:调用sayHello_1
时,newOccupied
为2,capacity
为4,2 + 1 <= 4 *3 / 4
的条件满足。 - 如果到达
扩容
条件,就会先开辟2倍的新内存
,然后再插入新的缓存数据
。比如:调用sayHello_2
时,newOccupied
为3,capacity
为4,3 + 1 <= 4 *3 / 4
的条件不满足,就会进入else
语句,开辟2倍容量的新内存。
注
在开辟新内存中调用方法reallocate
时,传入的最后一个参数freeOld
为true
,会把旧的缓存空间清理释放
掉,不会copy
旧缓存数据
到新的缓存空间
,这也是为什么调用sayHello_2
时,输出的只有sayHello_2
。
总结
关于objc_class
中的cache
的原理分析,我们先是查看cache_t
的数据结构
,根据数据结构
我们无法知道其工作原理
,然后我们通过结构体中的方法
去找线索,最后锁定insert
方法,根据insert
方法来大致了解整个缓存插入的流程。cache_t
的工作原理流程图: