iOS底层探索--类底层分析
前期回顾
上个篇章 我们已经讲了对象的本质,isa的走位图,以及类的结构,下面进行简单的总结:
-
对象的本质
- 对象的本质:不管是实例对象和类对象,对象的本质是结构体。
objc对象的底层结构是objc_object结构体。类对象的底层结构是objc_class结构体。
- 对象的本质:不管是实例对象和类对象,对象的本质是结构体。
-
isa走位图
-
superclass走位- 实例对象没有继承关系,类对象才有继承关系
- 类的
superclass,指向父类,依次指到NSObject -
NSObject的superclass指向nil - 元类的
superclass指向上一级父的元类,依次指向根元类 - 根元类的
superclass指向NSObject
-
isa走位- 实例的
isa指向类 - 类的
isa指向元类 - 类的
isa指向根元类 - 根元类的
isa指向根元类自己
- 实例的
-
-
类的结构
- Class isa
- Class superclass
- cache_t cache
-
class_data_bits_t bits
image.png
类的属性、成员变量和方法(探究源码是objc4-756.2)
建议有条件的使用最新的源码探究,此处因为自己的Mac版本比较旧无法装最新系统,还因为自己的Mac配置了很多的环境变量
成员变量 & 属性
一、通过LLDB动态打印类的内部结构
-
使用
p/x PSYPerson.class在Xcode的控制台打印PSYPerson类的地址,得到$0,然后根据地址偏移0x20(为什么是0x20?答:因为isa占8个字节,superclass占8个字节,cache占16个字节,总共32个字节,使用十六进制表示就是0x20),将得到的地址0x00000001000025f0(0x00000001000025d0+0x20),强转成class_data_bits_t *得到$2($2表示一个变量,*$2,就可获取其值) -
打印
$2地址的值,在控制台输入p *$2即可输出class_rw_t结构体的结构,如下图:(最新的源码objc4-818.2,结构不是这个结构,这里可只关注探索过程,重要的是思路,换一份源码一样的思路)
找到class_rw_t地址
class_rw_t结构
因为在
class_rw_t中,methods、properties和protocols是运行时加载分类的时候或者开发者调用runtime的API动态添加方法,关联属性等的时候才需要用到,所以我们这里暂时先关注 ro。根据源码定义可以直接点出来p $3.ro 最新的需要 p $3.ro(),通过C++结构体内部的ro()函数获得 $6。
class_ro_t结构
通过p $6.ivars可获得ivar_list_t结构体的指针,然后再打印输出所有成员列表p *$10: 发现count有4个,第一个是_isWorking,大小size占1个字节,也可以通过p $11.get(0)打印第一个成员变量;第二个p $11.get(1),第三个p $11.get(2),第四个p $11.get(3).
ivars数组
所有ivars
class_ro_t中的baseProperties属性,也可通过$13.get(index)输出每个对应的属性
baseProperties数组
baseProperties
而我的代码的属性确是如LLDB打印的结果如下:
@interface PSYPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) BOOL isWorking;
+ (void)run;
- (void)run;
@end
二、通过clang查看类的内部结构
如下图,通过编译属性最后优化成了如下的成员变量,并且生成了get和set方法
仔细看四个属性的set方法,我们发现,为什么有些事通过调用
objc_setProperty方法实现,有些通过内存平移实现呢?
set和get
如果是我们自己设计底层,我们会怎么设计呢?开发者的属性名字随机性大,Xcode编译的时候自动转成
setProperty和getProperty,最终是如何关联到objc_setProperty方法的调用呢。通过源码全局检索objc_setProperty,发现其只有两个文件包含,一个是objc-accessors.mm文件,也就是objc_setProperty的实现,一个是objc_abi.h头文件暴露给外面使用,整个源码没有调用者,我们可以大胆猜测是编译器LLVM干了这个工作
image.png
继续深究会发现,是因为在LLVM编译器编译期就根据
IsCopy条件判断是否走objc_setProperty还是走属性内存平移方式。objc_setProperty是作为中间层,在编译器将setXxxx的IMP重定向到objc_setProperty然后再指向底层代码。
我们再来看objc_setProperty、objc_setProperty_atomic、objc_setProperty_nonatomic、objc_setProperty_atomic_copy、objc_setProperty_nonatomic_copy这几个方法的实现源码,最后都调用了reallySetProperty。
// reallySetProperty的源码解析如下注释:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) { // 如果偏移为0直接赋值
object_setClass(self, newValue);
return;
}
// 获取属性内存偏移地址
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return; // 如果新旧值一致则直接返回
newValue = objc_retain(newValue); // 对新值retain
}
if (!atomic) { // 非原子
oldValue = *slot; // 保存旧值用于后面release,因为每一个旧值都曾是新值,曾被objc_retain
*slot = newValue; // 对内存赋值
} else { // 原子需要加锁操作,保证安全性
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue); // 对旧值进行release
}
对象方法 & 类方法
一、通过LLDB动态打印
同样的我也将所有的methods打印出来,发现所有的方法都是对象方法- setProperty和-property(set 和 get 方法),加上一个C++的析构函数.cxx_destruct总,为毛没有我的自定义方法
+ (void) run和- (void) run,经查因为我没有写他们的实现(*_*)[尴尬]。
添加自定义方法的实现,重新再来一次,可得到10个methods,比原来多了一个对象方法- (void) run。没有+ (void) run类方法,既然对象的方法存在类中,类方法存在哪里呢?元类?
(lldb) p $5.baseMethodList
(method_list_t *const) $6 = 0x0000000100002338
(lldb) p *$6
(method_list_t) $7 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 10
first = {
name = "nickName"
types = 0x0000000100001f6a "@16@0:8"
imp = 0x0000000100001a50 (objc-debug`-[PSYPerson nickName] at PSYPerson.h:16)
}
}
}
(lldb) p $7.get(0)
(method_t) $8 = {
name = "nickName"
types = 0x0000000100001f6a "@16@0:8"
imp = 0x0000000100001a50 (objc-debug`-[PSYPerson nickName] at PSYPerson.h:16)
}
(lldb) p $7.get(1)
(method_t) $9 = {
name = "setNickName:"
types = 0x0000000100001f72 "v24@0:8@16"
imp = 0x0000000100001a80 (objc-debug`-[PSYPerson setNickName:] at PSYPerson.h:16)
}
(lldb) p $7.get(2)
(method_t) $10 = {
name = "setIsWorking:"
types = 0x0000000100001f98 "v20@0:8c16"
imp = 0x0000000100001ae0 (objc-debug`-[PSYPerson setIsWorking:] at PSYPerson.h:17)
}
(lldb) p $7.get(3)
(method_t) $11 = {
name = ".cxx_destruct"
types = 0x0000000100001f62 "v16@0:8"
imp = 0x0000000100001b00 (objc-debug`-[PSYPerson .cxx_destruct] at PSYPerson.m:10)
}
(lldb) p $7.get(4)
(method_t) $12 = {
name = "name"
types = 0x0000000100001f6a "@16@0:8"
imp = 0x00000001000019a0 (objc-debug`-[PSYPerson name] at PSYPerson.h:14)
}
(lldb) p $7.get(5)
(method_t) $13 = {
name = "setName:"
types = 0x0000000100001f72 "v24@0:8@16"
imp = 0x00000001000019d0 (objc-debug`-[PSYPerson setName:] at PSYPerson.h:14)
}
(lldb) p $7.get(6)
(method_t) $14 = {
name = "run"
types = 0x0000000100001f62 "v16@0:8"
imp = 0x0000000100001990 (objc-debug`-[PSYPerson run] at PSYPerson.m:16)
}
(lldb) p $7.get(7)
(method_t) $15 = {
name = "isWorking"
types = 0x0000000100001f90 "c16@0:8"
imp = 0x0000000100001ac0 (objc-debug`-[PSYPerson isWorking] at PSYPerson.h:17)
}
(lldb) p $7.get(8)
(method_t) $16 = {
name = "age"
types = 0x0000000100001f7d "q16@0:8"
imp = 0x0000000100001a10 (objc-debug`-[PSYPerson age] at PSYPerson.h:15)
}
(lldb) p $7.get(9)
(method_t) $17 = {
name = "setAge:"
types = 0x0000000100001f85 "v24@0:8q16"
imp = 0x0000000100001a30 (objc-debug`-[PSYPerson setAge:] at PSYPerson.h:15)
}
带着猜想紧接着通过x/4gx PSYPerson.class打印类的内存拿到 isa,之后可以通过两种方式拿到元类地址。
方式一:
由于__x86_64__的shiftcls类(元类)信息在中间占44位(第4位~第47位),所以将其他无关的低三位和高17位清零即可得到。
- 也就是右移3位
>>3,此时高位补0,补3位,得到addr1,此时高20位是无关信息 - 将
addr1左移20位,得到addr2 - 将
addr2右移17位,复位原来shiftcls的位置(第4位~第47位)
方式二:
- 直接将isa 与上
# define ISA_MASK 0x00007ffffffffff8ULL(__arm64__是ISA_MASK 0x0000000ffffffff8ULL)
拿到元类地址
拿到元类的地址0x00000001000025e0之后,顺着打印实例方法的方式找到baseMethodList,并打印出方法。(注意0x00000001000025e0偏移0x20结果是0x0000000100002600,不是,不是,不是0x0000000100002610,可通过p/x (0x00000001000025e0+0x20)打印,不要自己加)如下图:
image.png
针对上面得到的方法的types types = 0x0000000100001f62 "v16@0:8"可参考下面官方给出的对应的类型编码表,需要的时候查:
二、通过RunTime的API直接打印
通过使用RunTime的APIclass_copyMethodList拿到方法列表,再通过NSStringFromSelector将方法转成字符串输出。为了区分两个实例方法和类方法,将实例方法改成-(void)runFast
image.png
+ (void) run
- (void) runFast
class_getInstanceMethod(Class, @selector(run)) --- 意思是在类/元类中查找实例方法run
class_getClassMethod(Class, @selector(run))--- 意思是在类/元类中查找类方法run
所以验证结果:
- 在PSYPerson类中没有
- run方法(实例方法存储在类中) - 在元类PSYPerson中有
+ run方法 (类方法存储在元类中) - 在PSYPerson类中有
- runFast方法(实例方法存储在类中) - 在元类PSYPerson中没有
+ runFast方法 (类方法存储在元类中)
image.png
- 在PSYPerson类中没有
- run方法 - 在元类PSYPerson中有
+ run方法 - 在PSYPerson类中有
- runFast方法 - 在元类PSYPerson中没有
+ runFast方法
为什么在类中和元类中找的ClassMethod结果和在元类中查找实例方法结果都一样,都是0x100002400?
这时候看一下class_getClassMethod源码,发现在class_getClassMethod方法中调用了class_getInstanceMethod在元类中获取实例方法,因为类本身也是对象
下面再从类和元类中查找run和runFast的IMP验证一下 ---》其结果就是在元类中找到run方法,在类中找到runFast方法。但是为什么在类中找不到run方法和在元类中找不到runFast方法,却也返回了libobjc.A.dylib中的_objc_msgForward方法?这就涉及到IMP方法的查找流程,将会在后面篇章讲解....
IMP
小结
-
class_getInstanceMethod是获取的都是对象方法,这要看参数Class传入的是类还是元类。如果Class是类,则该方法是获取实例方法;如果Class是元类,则该方法是获取类方法。 - 底层是没有
+方法 和-方法之分的,但是其存储的区域不一样,-方法存储在类信息的内存中,+方法存储在元类信息内存中,体现到上层就是类方法和对象方法。
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// 也就是也调用了class_getInstanceMethod方法
return class_getInstanceMethod(cls->getMeta(), sel);
}
Class getMeta() {
if (isMetaClass()) return (Class)this; // 如果是元类,直接返回
else return this->ISA(); // 如果不是元类返回类的ISA
}
拓展
下面通过几个例子看一下下面几个函数原理:
- (BOOL)isKindOfClass:(Class)cls
+ (BOOL)isKindOfClass:(Class)cls
+ (BOOL)isMemberOfClass:(Class)cls
- (BOOL)isMemberOfClass:(Class)cls
例子:
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re3 = [(id)[PSYPerson class] isKindOfClass:[PSYPerson class]];
源码分析:
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
image.png
image.png
例子:
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re4 = [(id)[PSYPerson class] isMemberOfClass:[PSYPerson class]];
源码分析:
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
例子:
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re8 = [(id)[PSYPerson alloc] isMemberOfClass:[PSYPerson class]];
源码分析:
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
例子:
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re7 = [(id)[PSYPerson alloc] isKindOfClass:[PSYPerson class]];
源码分析:
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
isa走位图
结合源码,断点调试很容易就可以得到结果,但是其是遵循isa的走位图的,正好验证了isa走位图。再结合汇编查看确实也是走了isKindOfClass方法。但是在objc4-779.1以后,在汇编层就走的是objc_opt_isKindOfClass了。
其实源码是一样的是一样的,分析结果也是一样的,只是走的流程有些问题,所以还是要再运行时查看汇编调用函数,在去分析,省得走弯路。
源码:
BOOL objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
if (slowpath(!obj)) return NO;
Class cls = obj->getIsa();
if (fastpath(!cls->hasCustomCore())) {
for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}