这里有一篇apple官方关于runtime底层优化的思路,为什么要定义class_rw_t、class_ro_t等等这么多结构体:
https://developer.apple.com/videos/play/wwdc2020/10163/
一、类的isa分析
新建一个空工程,创建LGPerson类,直接通过clang -rewrite-objc main.m生成编译后的文件;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 0x00007ffffffffff8
LGPerson *p = [LGPerson alloc];
NSLog(@"%@",p);
}
return 0;
}
因为我要分析类,所以要看Class,再main.cpp中发现以下代码,这里可以看出Class的本质是objc_class结构体指针;
typedef struct objc_class *Class;
在objc源码中(https://opensource.apple.com/releases/ 可以找到不同版本的源码)可以找到objc_class结构体具体定义,如下;
之前在分析对象原理的时候,有提过影响对象内存大小的因素主要是成员变量,所以这里也可以类比对象,主要分析类的成员变量,即
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
这里还加了个注释Class ISA,提示我们存在隐藏变量isa,这个主要是父结构体中有isa这个变量;
通过打印class的内存地址会发现,第一个地址isa指向的是LGPerson,第二个地址指向NSObject,即superclass,到这里都还比较明朗,但是打印第三个开始,就是一串莫名其妙的数字了,接下来就只能进一步探索了;
二、内存偏移分析
这里先做一个简单的分析,通过创建a、b两个int类型数据,分别打印他们的值跟地址;通过打印我们会发现a、b两个指针内存地址只相差4字节,而我们打印int的size会发现它刚好需要4字节,这与我们的两个指针偏移大小吻合;
这里就可以得出一个结论,首先是a、b两个指针内存都是在栈上,栈内存连续的两个地址,指向同一片内存空间10;
接下来我们创建两个对象看一下,打印对象的地址会发现,他们其实是不连续的,这也可以说明我们的对象其实是在堆中开辟的内存;
打印对象指针会发现,他们相差符合对象内存对齐的原则,所以对象的指针还是在栈上的;
简单的画一下对象内存指向:
数组指针
下面换种方式,通过数组指针来类比当前对象,我们会发现数组指针地址跟对象的的第一个元素C[0]是同一个地址0x16fdff210,即他们都指向数组的首地址;
然后换种方式,通过一个int指针d,执行c,打印结果如上图d=&c,d+1 = &c[1],这个时候我们大胆的猜测,可以通过指针地址平移的方式,即增加步长,遍历得到数组指针相同的内容,如下图所示;
根据以上分析,OC类也可以类比上诉方式,OC 类 结构 首地址->isa, 平移一些大小也可以得到我们想要的内容;
接下来我们就可以通过,LGPerson.class地址 -> 平移得到所有我们想要值;
三、类的结构属性分析
上一节我们有提到,可以通过指针平移的方式,得到我们想要的内容,
首先尝试一下通过指针首地址,平移8个字节(因为isa大小是8),打印出我们的父类NSObject,得到它的指针地址0x100008320,当我们打印NSObject.class类对象地址时,会发现跟我们x/4gx打印出来的第二个值一样,就证明当前的类第一个指向isa,偏移8得到NSObject的指针地址,指向后面的值地址0x0000000100721140,这跟我们上面猜想的是一致的;
接下来就是要探索我们之前剩下的几个成员变量cache_t、class_data_bits_t了,这里比较关键的就是要知道cache_t的大小,才能知道bits的位置;
通过查看objc源码我们会发现cache_t本质是个结构体,而影响结构体大小的因素是成员变量,所以这里我们得找一下它里面的成员变量;
通过分析会发现,成员变量就俩,一个是uintptr_t类型_bucketsAndMaybeMask,大小是8,另一个是联合体,联合体里面包含一个结构体跟_originalPreoptCache指针类型,因为联合体内部是互斥的,所以这里可以分析union大小是8,所以分析到这里,可以得到cache_t大小是16;
所以这个时候,我们就可以通过指针偏移32位(isa是8,superclass也是8,cache_t是16)得到我们想要的class_data_bits_t;这个时候又会发现一个问题,通过打印这片内存无法得到里面具体的内容,这又是个难题了;
通过观察class_data_bits_t结构体内部会发现,它有提供api,这个可以获取class_rw_t,这个是类里面非常关键的结构体,也是我们要着重研究的对象;
class_rw_t* data() const {
#if __BUILDING_OBJCDT__
return (class_rw_t *)((uintptr_t)ptrauth_strip((class_rw_t *)bits,
CLASS_DATA_BITS_RW_SIGNING_KEY) & FAST_DATA_MASK);
#else
return (class_rw_t *)((uintptr_t)ptrauth_auth_data((class_rw_t *)bits,
CLASS_DATA_BITS_RW_SIGNING_KEY,
ptrauth_blend_discriminator(&bits,
CLASS_DATA_BITS_RW_DISCRIMINATOR)) & FAST_DATA_MASK);
#endif
}
class_rw_t内部也有api可以获取到class_ro_t结构体;
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
通过底层api调用,我们可以打印出想要探索的结构体信息;
这个时候可以尝试打印一下成员变量值,看看是不是我们定义的属性;
通过打印会发现,ivars跟baseProperties分别是ivar_list_t、property_list_t结构体,他们都是继承自entsize_list_tt,这是结构体设计的一种复用方式,所以这时真正要探索的是entsize_list_tt;
struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
bool containsIvar(Ivar ivar) const {
return (ivar >= (Ivar)&*begin() && ivar < (Ivar)&*end());
}
};
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
entsize_list_tt中的两个成员变量entsizeAndFlags、count,上图都有打印出来了,我们现在比较关心的就是是否有获取当前属性的方法;
通过api可以找到get、getOrEnd方法,这俩方法意思比较明显,传入参数也是个序号,尝试调用该方法,很惊喜也在情理之中,成功获取到我们定义的两个属性;
四、类方法归属分析
分析完属性成员变量,最让我好奇的也是最值得探究的当属OC的方法,lldb打印的字面分析,baseMethods应该就是存储方法的结构体;
(lldb) p (class_data_bits_t *)0x00000001000082e0
(class_data_bits_t *) $4 = 0x00000001000082e0
(lldb) p $4->data()->ro()
(const class_ro_t *) $5 = 0x0000000100008228
(lldb) p *$5
(const class_ro_t) $6 = {
flags = 388
instanceStart = 8
instanceSize = 40
reserved = 0
= {
ivarLayout = 0x0000000100003e9f "\U00000012"
nonMetaclass = 0x0000000100003e9f
}
name = {
std::__1::atomic<const char *> = "LGPerson" {
Value = 0x0000000100003e96 "LGPerson"
}
}
baseMethods = {
ptr = 0x0000000100008078
}
baseProtocols = nil
ivars = 0x0000000100008158
weakIvarLayout = 0x0000000000000000
baseProperties = 0x00000001000081e0
_swiftMetadataInitializer_NEVER_USE = {}
}
用同样的方法,打印baseMethods,发现它是一个方法列表指针method_list_t,但是它外层包装了一层WrappedPtr,通过.ptr取出当前列表指针,再通过*读取这片内存地址内容,出现了一个熟悉的东西entsize_list_tt,这跟我们之前分析属性成员列表的时候是一样的东西,所以就尝试着用同样的的方式,get看能不能获取我们想要的方法名,结果很遗憾,打印出来都是空的;
唯一的可能就是property_t跟method_t的结构不一样,才会导致lldb的调试结果不一样,这个时候只能从源码分析,查看其二者的区别了;
struct property_t {
const char *name;
const char *attributes;
};
通过查看发现,property_t结构体内部就是简单的两个成员变量,而method_t内部确实好多的结构体跟方法;从官方注释可以知道,这么做是为了更好的保护我们的方法,因为方法的存储比较复杂,存储在方法区,不像成员变量,跟着我们的对象;
method_t根据架构的不同会有Small跟big类型;
通过big()方法读取,会惊奇的发现我们想要的结果,所以我们尝试打印我们所有的方法(因为此处测试写的不多,所以就这么干);
比较我们在类当中定义的方法,会发现类方法没有被打印出来,即不在方法列表里面(cxx_destruct这个是在OC下层编译的时候加的析构方法,编译过程中就会添加的);
sayHello跟sayNB之所以能出现在method_list中,是因为他们是对象方法,对象方法存在类当中的,通过isa指向类获取方法,我们此时分析的是类,所以此时能打印出来对象方法也是情理之中;而类方法其实是存在元类中的,这里也进一步解释了为什么要有元类;
对象方法 (对象 --> isa --> 类) - 类方法 (类 --> isa --> 元类)
为了验证这一点,lldb通过类的isa查找元类,看是否与我猜想的一致,isa指向元类首地址,通过指针平移的方式,找到元类的bits,再找到原来的ro;
(lldb) x/4gx LGPerson.class
0x1000081f8: 0x00000001000081d0 0x0000000108d68140
0x100008208: 0x0000000108d60cb0 0x801c000000000000
(lldb) po 0x00000001000081d0
LGPerson
(lldb) x/4gx 0x00000001000081d0
0x1000081d0: 0x0000000108d680f0 0x0000000108d680f0
0x1000081e0: 0x0000600001704100 0xe035000100000003
(lldb) p/x 0x1000081d0+32
(long) $5 = 0x00000001000081f0
(lldb) p (class_data_bits_t *)0x00000001000081f0
(class_data_bits_t *) $6 = 0x00000001000081f0
(lldb) p $6->data()->ro()
(const class_ro_t *) $7 = 0x0000000100008070
(lldb) p *$7
(const class_ro_t) $8 = {
flags = 389
instanceStart = 40
instanceSize = 40
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "LGPerson" {
Value = 0x0000000100003f08 "LGPerson"
}
}
baseMethods = {
ptr = 0x0000000100008038
}
baseProtocols = nil
ivars = nil
weakIvarLayout = 0x0000000000000000
baseProperties = nil
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $8.baseMethods
(const WrappedPtr<method_list_t, method_list_t::Ptrauth>) $9 = {
ptr = 0x0000000100008038
}
(lldb) p $9.ptr
(method_list_t *const) $10 = 0x0000000100008038
(lldb) p $10
(method_list_t *const) $10 = 0x0000000100008038
(lldb) p $10.get(0)
(method_t) $11 = {}
Fix-it applied, fixed expression was:
$10->get(0)
(lldb) p $10.get(0).big()
(method_t::big) $12 = {
name = "class_sayHello"
types = 0x0000000100003f7d "v16@0:8"
imp = 0x0000000100003ca0 (KCObjcBuild`+[LGPerson class_sayHello])
}
Fix-it applied, fixed expression was:
$10->get(0).big()
(lldb) p $10.get(1).big()
(method_t::big) $13 = {
name = "class_sayNB:"
types = 0x0000000100003f85 "@20@0:8i16"
imp = 0x0000000100003cd0 (KCObjcBuild`+[LGPerson class_sayNB:])
}
Fix-it applied, fixed expression was:
$10->get(1).big()
Fix-it applied, fixed expression was:
$10->get(2).big()
error: execution stopped with unexpected state.
error: Execution was interrupted, reason: breakpoint 2.1.
The process has been returned to the state before expression evaluation.
Program ended with exit code: 6
当然,如果对lldb调试不是很熟悉的话,也可以通过API调用进行分析,得到的结果也是一样的;
void lgObjc_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));
LGLog(@"Method, name: %@", key);
}
free(methods);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// LGTeacher *teacher = [LGTeacher alloc];
LGPerson *person = [LGPerson alloc];
Class pClass = object_getClass(person);
lgObjc_copyMethodList(pClass);
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
NSLog(@"*************");
lgObjc_copyMethodList(metaClass);
NSLog(@"Hello, World!");
}
return 0;
}