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
的工作原理流程图: