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
中,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)
.
class_ro_t
中的baseProperties
属性,也可通过$13.get(index)
输出每个对应的属性
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)
打印,不要自己加)如下图:
针对上面得到的方法的types types = 0x0000000100001f62 "v16@0:8"
可参考下面官方给出的对应的类型编码表,需要的时候查:
二、通过RunTime的API直接打印
通过使用RunTime的APIclass_copyMethodList
拿到方法列表,再通过NSStringFromSelector
将方法转成字符串输出。为了区分两个实例方法和类方法,将实例方法改成-(void)runFast
+ (void) run
- (void) runFast
class_getInstanceMethod(Class, @selector(run))
--- 意思是在类/元类中查找实例方法run
class_getClassMethod(Class, @selector(run))
--- 意思是在类/元类中查找类方法run
所以验证结果:
- 在PSYPerson类中没有
- run
方法(实例方法存储在类中
) - 在元类PSYPerson中有
+ run
方法 (类方法存储在元类中
) - 在PSYPerson类中有
- runFast
方法(实例方法存储在类中
) - 在元类PSYPerson中没有
+ runFast
方法 (类方法存储在元类中
)
- 在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方法的查找流程,将会在后面篇章讲解....
小结
-
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);
}