iOS底层探索七(类的分析下)
前言
相关文章:
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);
}
输出结果:
获取 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);
}
输出结果
获取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);
}
输出结果:
查看元类和类中实例方法是否存在:
//获取类和元类中实例方法:
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__);
}
打印结果:
可以得出结论:
-
类中只存储了sayHello 方法,元类中只存储了sayHappy方法,
-
元类中存储类方法是以实例方法的形式进行存储的
查看元类和类中类方法是否存在:
//查看类和元类中类方法查看方法
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__);
}
打印结果:
得出结论:
- sayhello作为实例方法不存在与类和元类的类方法中
- 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输出情况:
我们发现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处打断点
@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中的内存情况
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;
}
-
找到这个方法为mask修改值的,继续查找有那些方法调用mask()方法并赋值,发现:mask_t cache_t::capacity方法:
-
继续寻找一下有那些方法调用capacity()这个方法:这里我们找到如下方法:cache_delete,cache_erase_nolock,cache_fill_nolock,expand,isConstantEmptyCache:根据这几个方法意义我们可以看到肯定是和expand(扩容)方法有关,,
-
我们查看下有那些地方调用expand()方法,发现只有一处就是cache_fill_nolock()方法,说明这个方法很关键
2.0 cache断点跟踪+源码剖析
这里我们直接打断点到cache_fill_nolock这个方法进行跟踪一下进行分析;我们在main中调用第一个方法的时候打断点,走到这个方法后给cache_fill_nolock这个方法下断点:
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);
}
这个方法中我们需要关注的点:
-
系统在有缓存情况下直接获取缓存中方法IMP后会直接返回 if (cache_getImp(cls, sel)) return;
-
第一进入分配空间cache->reallocate
-
系统的扩容策略,第一次正常分配,当小于分配的3/4可以进行正常存放,当大于3/4时进行扩容 (expand)扩容方法
-
通过 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);
}
}
这个方法关注点:
-
mask
被设置的值为开辟空间的newCapacity-1:第一次为3
-
在开辟新的数组时,会释放旧
buckets
(加入回收数组)这个是为什么我们调到第四个的时候,bucket里面没有存数据了 -
旧的缓存不会被计入新的数组中。保持局部性原理为最佳回到
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);
}
这个方法比较简单。我们需要关注两点。
- 在不超过
uint32_t
的情况下,每次扩容为原来大小的2
倍 - 如果超过了
uint32_t
,则重新申请跟原来一样大小的buckets
。 - 扩容完成后会调用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
。步骤如下:
-
准备哈希查找的
k
和目标数组buckets
。 - 线性探测的方式查找目标
k
,直到找到k
或者空的bucket
。 - 异常处理。
到这里大体的流程已经梳理清晰,但还有一个比较重要的问题。我们知道 msgSend
是可多线程并发执行的,那么 cache_t
在更新缓存时,如何处理线程安全的问题呢。
- 在每次执行缓存填充,和扩充都会添加对应的互斥锁。
- 在更新
buckets
和mask
时,会使用mega_barrier
来保证buckets
的更新一定早于mask
。(如果不保证会有数组越界的问题) - 在回收旧的
buckets
时,会把需要释放的buckets
加入一个全局的数组garbage_refs
中。等待真正没有其他线程使用数组中的元素时,在进行释放。
断点验证:
我们先用走一次放发第一次进入sayHello方法:
进入
set完成后
我们可以看到和我们源码分析的一样mask为3 ,sel存放sayHello,
接下来我们直接看扩容情况走第四个方法时
进入expand方法
我们可以看到Newcapacity为8 继续进入reallocate方法
这里我们稍微说明下,为什么要把老数组进行清空,而不是直接扩容进行重新复制并添加上去,因为在调用方法,是需要一个很快速的流程,不能允许有读写操作,容易出错。
继续我们直接看set后结果
我们可以看到已经将之前存入的数据都已经清空了;
总结:
此篇文章主要分析了下类结构中的cache中的属性,以及cache中取缓存的方式,以及扩容策略等,以上便是我对OC中类的cache探究,如果有错误的地方还请指正,大家一起讨论,开发水平一般(文章中有错误后,我发现会第一时间对博文进行修改),还望大家体谅。
写给自己
人一辈子,就是这样:来是偶然,去是必然,尽其当然,顺其自然。得之淡然,失之泰然,争其必然,自然而然,未完待续。。。
推荐阅读