对象的本质
探索方式:
Clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下,由Apple主导编写,基于 LLVM
的C/C++/Objective-C
编译器
Clang编译:
clang -rewrite-objc main.m -o main.cpp
重写带有UIKit
库的文件可能出现的问题:
解决方案:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
xcrun编译
模拟器 xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main.cpp
真机 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
在 main.m
文件 声明一个 XQPerson
类
打开终端,打开到 main.m
所在文件夹运行上文中的指令,生成 main.cpp
文件,打开此文件,搜索XQPerson
,可以发现很多信息。
在main.cpp
文件里发现了XQPerson
的内容,在.m
文件中XQPerson
继承自NSObject
,但是在.cpp
文件中却是objc_object
。可以得出结论,对象的本质是objc_object
结构体
观察XQPerson_IMPL
结构体中嵌套了NSObject_IMPL
结构体,这是对NSObject_IMPL
结构体的伪继承,通过搜索NSObject_IMPL
,发现如下图信息:
由此可知:NSObject_IVARS
就是成员变量isa
。
继续搜索objc_object
可以发现如下信息:
可以发现id在底层的定义为struct objc_object *
类型,所以可以代表任意类型的对象。
成员变量和属性的区别
在XQPerson_IMPL
结构体中可以发现在XQPerson定义的属性,此时已经转换为成员变量_age
,_name
,_hobby
,而属性的 setter
,getter
方法我们通过搜索相关属性名称可以找到如下代码:
而搜索成员变量并不能找到相关的getter,setter方法,由此可以得出结论: 属性会自动生成成员变量同时实现了setter和getter方法
setter/getter解释
Type Encodings官方文档
getter/setter方法
我们可以发现对象的setter,getter方法是通过内存平移的方式找到成员变量的地址进行存值和取值的。
细心的我们可以发现,属性name和nickName的setter方法和hobby,age的是不一样的,调用的是objc_setProperty
函数,而nickName的getter方法和其他属性都不一样,调用的是 objc_getProperty
。
通过 objc4-818.2
源码里搜索objc_setProperty
函数,发现objc_setProperty
函数对copy修饰的属性进行了相关的copy
处理。
通过 objc4-818.2
源码里搜索此函数,发现objc_getProperty
函数对copy修饰的属性进行了相关的加锁
处理。
objc4-818.2
源码里只有objc_setProperty
函数的实现,却没有相关调用信息,通过llvm
源码逆向查找确定了objc_setProperty
函数的调用条件。
llvm
源码逆向验证流程:objc_setProperty
->getSetPropertyFn
->GetPropertySetFunction
->PropertyImplStrategy
->IsCopy(判断)
。
objc_setProperty
图解流程:
objc_getProperty
的逆向查找和objc_setProperty
接近,这里就不在赘述了。
结论:对象属性的setter,getter方法通过地址偏移的方式找到成员变量进行存取值,当属性由copy修饰时,无论是nonatomic
还是atomic
,setter方法都会被重定向到objc_setProperty
函数,而atomic
修饰的属性,getter方法会被重定向到objc_getProperty
函数。
结构体,联合体,位域
上文中提到,对象都有一个很关键的isa成员,在探索isa之前,需要补充一点关于,结构体,联合体,位域相关的知识。
案例:
此时XQCar1共占用4*8=32位,8字节
struct XQCar1 {
BOOL front; // 1字节
BOOL back;
BOOL left;
BOOL right;
};
// 指定位域后,XQCar2只需要使用1个字节,相比XQCar1节省了3个字节
struct XQCar2 {
BOOL front: 1; //1位
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
// 结构体:共存,占用内存为所有成员变量内存之和,然后以结构体内存对齐原则对齐
struct XQTeacher1 {
char *name;
int age;
};
// 联合体 : 互斥 所有成员变量共用一块内存空间
union XQTeacher2 {
char *name;
int age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct XQCar1 car1;
struct XQCar2 car2;
NSLog(@"%ld-%ld",sizeof(car1),sizeof(car2));
struct XQTeacher1 teacher1;
teacher1.name = "NB";
NSLog(@"name = %s , age = %d",teacher1.name,teacher1.age);
teacher1.age = 18;
NSLog(@"name = %s , age = %d",teacher1.name,teacher1.age);
union XQTeacher2 teacher2;
teacher2.name = "NB";
NSLog(@"name = %s , age = %d",teacher2.name,teacher2.age);
teacher2.age = 18;
NSLog(@"name = %s , age = %d",teacher2.name,teacher2.age);
NSLog(@"%ld-%ld",sizeof(teacher1),sizeof(teacher2));
}
return 0;
}
********************************输出结果********************************
2022-01-27 16:00:27.614838+0800 001-联合体位域[8631:278201] 4-1
2022-01-27 16:00:27.615297+0800 001-联合体位域[8631:278201] name = NB , age = 524304
2022-01-27 16:00:27.615349+0800 001-联合体位域[8631:278201] name = NB , age = 18
2022-01-27 16:00:27.615385+0800 001-联合体位域[8631:278201] name = NB , age = 18
2022-01-27 16:00:27.615465+0800 001-联合体位域[8631:278201] name = NB , age = 15990
2022-01-27 16:00:27.615513+0800 001-联合体位域[8631:278201] name = , age = 18
2022-01-27 16:00:27.615549+0800 001-联合体位域[8631:278201] 16-8
结论:
- 结构体(
struct
)中所有变量是共存
的,优点是“有容乃大”,全面;缺点是内存空间的分配是粗放的
,不管是否使用,全部分配。 - 结构体(
struct
)可以通过为成员变量指定位域的方式来优化内存。 - 联合体(
union
)中各变量是互斥
的,优点是内存使用更为精细灵活,节省了内存空间,缺点就是不够包容
。
接下来正式开始对 isa
的探索,在探索alloc
方法时调用了initInstanceIsa
函数初始化isa
,继续前面的逻辑,跳转到isa
的定义如下:
查看宏定义ISA_BITFIELD
,探索isa
指针内都存放了什么,分为两种架构模式。
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; // 是否为纯指针 \
uintptr_t has_assoc : 1; // 是否有关联对象 \
uintptr_t has_cxx_dtor : 1; // 是否有C++析构函数 \
uintptr_t shiftcls : 33; // 类的指针地址 /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; // 弱引用 \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; // 散列表 \
uintptr_t extra_rc : 19 // 引用计数
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; // 是否为纯指针 \
uintptr_t has_assoc : 1; // 是否有关联对象 \
uintptr_t has_cxx_dtor : 1; // 是否有C++析构函数 \
uintptr_t shiftcls : 44; // 类的指针地址 /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; // 弱引用 \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; // 散列表 \
uintptr_t extra_rc : 8 // 引用计数
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
__has_feature(ptrauth_calls)介绍
- __has_feature(ptrauth_calls)的作用是判断编译器是否支持指针身份验证功能。
- __has_feature:此函数的功能是判断编译器是否支持某个功能。
- ptrauth_calls:指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构。
- 参考链接developer.apple.com/documentati…。
图解isa
(以64位为例)
isa
还原类信息
以86_64为例
测试所用电脑为86_64
架构,生成的对象的ISA_BITFIELD
第3位至第46位为XQPerson
的信息,还原方式说明:
- 通过
x/2gx p1
格式化输出p1
对象的内存信息,首地址为isa
指针0x011d8001000080e9
。 - 将
isa
的前3
个bit位移除,即0x011d8001000080e9
向右移3
位,通过p/x
得到新的指针0x0023b0002000101d
。 - 将
isa
指针的后17
个bit位移除,由于刚刚向右移了3
个bit位,那么现在就需要向左移31
个bit位,即0x0023b0002000101d
向左移17 + 3 = 20
位,通过p/x
得到新的指针0x0002000101d00000
。 - 最后将
isa
内代表XQPerson
信息的17
个bit位还原,即0x0002000101d00000
向右移17
个bit位,通过p/x
得到新的指针0x00000001000080e8
。 - 最后
po
输出0x00000001000080e8
,得到的结果为XQPerson
。
ISA_MASK
还原:
ISA_MASK
还原说明:
- 通过
x/4gx p1
格式化输出p1
对象的内存信息,首地址为isa
指针0x011d8001000080e9
。 - 将
isa
指针&
上ISA_MASK
,即0x011d8001000080e9
&0x00007ffffffffff8
,通过p/x
得到新的指针0x00000001000080e8
。 po
输出0x00000001000080e8
,得到的结果为XQPerson
。
isa
掩码(ISA_MASK
)介绍
现行源码版本一共分为三种架构:
x86_64: define ISA_MASK 0x00007ffffffffff8ULL // ULL: unsigned long long 无符号长整形
arm64: define ISA_MASK 0x0000000ffffffff8ULL
arm64(simulators): define ISA_MASK 0x007ffffffffffff8ULL
isa
指向分析
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface XQPerson : NSObject
@end
@implementation XQPerson
@end
//0x00007ffffffffff8ULL
void testClassNum(void){...}
int main(int argc, const char * argv[]) {
@autoreleasepool {
XQPerson* xq = [[XQPerson alloc]init];
testClassNum();
}
return 0;
}
********************在xq初始化后打断点lldb调试输出结果**********************
(lldb) x/4gx xq
0x10150cc40: 0x011d800100008101 0x0000000000000000
0x10150cc50: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x011d800100008101 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100008100
(lldb) po $1
XQPerson
(lldb) x/4gx $1
0x100008100: 0x00000001000080d8 0x00007ff85e149b88
0x100008110: 0x000000010150cc50 0x0001801000000003
(lldb) p/x 0x00000001000080d8 & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 0x00000001000080d8
(lldb) po $2
XQPerson
由上面的输出可以看到,2地址不同,但是都输出了XQPerson,这是什么原因呢?难道类对象在运行时会和对象创建多份吗?
接下来,带着这个疑问,来验证一下:
void testClassNum(void){
Class class1 = [XQPerson class];
Class class2 = [XQPerson alloc].class;
Class class3 = object_getClass([XQPerson alloc]);
Class class4 = [XQPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
XQPerson* xq = [[XQPerson alloc]init];
testClassNum();
}
return 0;
}
***********************输出结果*****************
0x100008100
0x100008100
0x100008100
0x100008100
可以看到,无论创建多少个对象,类始终只有一个,那么$2 = 0x00000001000080d8
输出XQPerson是怎么回事呢?
下面用MachOView看看究竟
在MachOView里,我们可以看到除了_OBJC_CLASS_&_XQPerson
外还有一个_OBJC_METACLASS_&_XQPerson
,这就是 元类
到这里已经知道对象的isa
->类对象,类对象的isa
->元类(MetaClass),那么元类的isa
指向哪里呢?接下来继续探索:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface XQPerson : NSObject
@end
@implementation XQPerson
@end
@interface XQStudent : XQPerson
@end
@implementation XQStudent
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XQStudent* student = [[XQStudent alloc]init];
NSLog(@"%@",student);
}
return 0;
}
*****************************lldb调试结果**************************
(lldb) x/4gx XQPerson.class //输出XQPerson类的内存信息
0x100008178: 0x0000000100008150 0x00007ff85e149b88
0x100008188: 0x00007ff81c7fc800 0x0000801000000000
(lldb) p/x 0x0000000100008150 & 0x00007ffffffffff8 //isa & ISA_MASK
(long) $1 = 0x0000000100008150
(lldb) po $1
XQPerson //po输出得到XQPerson元类
(lldb) x/4gx 0x0000000100008150 //输出XQPerson元类的内存信息
0x100008150: 0x00007ff85e149b60 0x00007ff85e149b60
0x100008160: 0x0000000101416920 0x0002e03100000003
(lldb) p/x 0x00007ff85e149b60 & 0x00007ffffffffff8 //isa & ISA_MASK
(long) $2 = 0x00007ff85e149b60
(lldb) po $2
NSObject //此时输出的NSObject是类还是元类
(lldb) x/4gx 0x00007ff85e149b60
0x7ff85e149b60: 0x00007ff85e149b60 0x00007ff85e149b88
0x7ff85e149b70: 0x0000000101105e60 0x0003e03100000007
(lldb) p/x 0x00007ff85e149b60 & 0x00007ffffffffff8
(long) $3 = 0x00007ff85e149b60
(lldb) po $3 //$3与$2地址相同
NSObject
(lldb) x/4gx student //输出student内存信息
0x101519a90: 0x011d8001000081c9 0x0000000000000000
0x101519aa0: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x011d8001000081c9 & 0x00007ffffffffff8 //isa & ISA_MASK
(long) $5 = 0x00000001000081c8
(lldb) po 0x00000001000081c8
XQStudent //po输出得到XQStudent类
(lldb) x/4gx $5 //输出XQStudent类的内存信息
0x1000081c8: 0x00000001000081a0 0x0000000100008178
0x1000081d8: 0x0000000101519aa0 0x0001801000000003
(lldb) p/x 0x00000001000081a0 & 0x00007ffffffffff8 //isa & ISA_MASK
(long) $7 = 0x00000001000081a0
(lldb) po 0x00000001000081a0
XQStudent //po输出得到XQStudent元类
(lldb) x/4gx $7 //输出XQStudent元类的内存信息
0x1000081a0: 0x00007ff85e149b60 0x0000000100008150
0x1000081b0: 0x0000000101035ec0 0x0002e03100000003
(lldb) p/x 0x00007ff85e149b60 & 0x00007ffffffffff8 //isa & ISA_MASK
(long) $9 = 0x00007ff85e149b60
(lldb) po $9 //$9,$3与$2地址相同
NSObject
(lldb) p/x NSObject.class //16进制输出NSObject的类的
(Class) $10 = 0x00007ff85e149b88 NSObject
(lldb) x/4gx $10 // 得到NSObject类的内存信息
0x7ff85e149b88: 0x00007ff85e149b60 0x0000000000000000
0x7ff85e149b98: 0x000000010110ad70 0x0001801000000003
(lldb) p/x 0x00007ff85e149b60 & 0x00007ffffffffff8 //isa & ISA_MASK
(long) $12 = 0x00007ff85e149b60
(lldb) po $12 //¥12,$9,$3,$2地址相同,与¥10地址不同
NSObject
结论:
- 非根类
NSObject
类的实例对象的isa
指向类(Class
),类的isa
指向元类(MetaClass
),元类的isa
指向根元类(RootMetaClass
),而不是父类的元类。 - 根类
NSObject
类的实例对象的isa
指向根类(RootClass
),根类的isa
指向根元类(RootMetaClass
)。 - 根元类的
isa
还是指向根元类。
superclass
指向分析
void superClass(void){
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class class = object_getClass(object1);
// NSObject元类即为根元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
NSLog(@"\n 实例对象%p\n类%p \n元类%p \n根元类%p",object1,class,metaClass,rootMetaClass);
Class sMetaClass = object_getClass(XQStudent.class);
Class sSuperClass = class_getSuperclass(tMetaClass);
NSLog(@"XQStudent元类的父类%@ - %p",tSuperClass,sSuperClass);
Class pMetaClass = object_getClass(XQPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"XQPerson元类的父类%@ - %p",psuperClass,psuperClass);
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"NSObject的父类%@ - %p",nsuperClass,nsuperClass);
Class rnsuperClass = class_getSuperclass(metaClass);
NSLog(@"NSObject根元类的父类%@ - %p",rnsuperClass,rnsuperClass);
}
********************************lldb输出********************************
2022-01-28 14:30:36.468562+0800 isa还原类信息[2659:114708]
实例对象0x101212f10
类0x7ff85e149b88
元类0x7ff85e149b60
根元类0x7ff85e149b60
2022-01-28 14:30:36.469073+0800 isa还原类信息[2659:114708] XQStudent元类的父类XQPerson - 0x100008178
2022-01-28 14:30:36.469148+0800 isa还原类信息[2659:114708] XQPerson元类的父类NSObject - 0x7ff85e149b60
2022-01-28 14:30:36.469186+0800 isa还原类信息[2659:114708] NSObject的父类(null) - 0x0
2022-01-28 14:30:36.469216+0800 isa还原类信息[2659:114708] NSObject根元类的父类NSObject - 0x7ff85e149b88
结论:
- 元类之间也存在继承链,与类一样。
- 根元类的父类是根类。
- 根类的父类是nil。
isa
走位图与类的继承链图解
接下来看一个比较经典的面试题isKindOfClass
,isMemberOfClass
:
示例:
void kindofDemo(void){
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[XQPerson class] isKindOfClass:[XQPerson class]];
BOOL re4 = [(id)[XQPerson class] isMemberOfClass:[XQPerson class]];
NSLog(@"\n re1 :%d\n re2 :%d\n re3 :%d\n re4 :%d\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [(id)[XQPerson alloc] isKindOfClass:[XQPerson class]];
BOOL re8 = [(id)[XQPerson alloc] isMemberOfClass:[XQPerson class]];
NSLog(@"\n re5 :%d\n re6 :%d\n re7 :%d\n re8 :%d\n",re5,re6,re7,re8);
}
****************************输出结果********************************
2022-01-28 19:14:54.529647+0800 isa还原类信息[4054:198852]
re1 :1
re2 :0
re3 :0
re4 :0
2022-01-28 19:14:54.530123+0800 isa还原类信息[4054:198852]
re5 :1
re6 :1
re7 :1
re8 :1
分析:
在isKindOfClass
源码处打上断点,发现并不会执行到,此时我们打开汇编调试,会断到如下图代码:
由此可得出,isKindOfClass
方法被重定向到了objc_opt_isKindOfClass
函数。
找到objc_opt_isKindOfClass
的源码如下:
可以看到,在OBJC2优先执行上面的代码。
接下来看看isMemberOfClass
的源码:
接下来开始分析:
-
使用
NSObject(Class)
的元类即根元类NSObject(Root Meta Class)
与NSObject(Class)
进行比较,不相等,第二次取根元类的父类即NSObject(Class)
与NSObject(Class)
比较,相等,返回YES。 -
使用
NSObject(Class)
的元类即根元类NSObject(Root Meta Class)
与NSObject类进行比较,不相等。 -
使用XQPerson类的元类
XQPerson(Meta Class)
,元类的父类NSObject(Root Meta Class)
,NSObject(Root Meta Class
的父类NSObject(Class)
依次与XQPerson(CLASS)
进行比较均不相等,第四次取NSObject(Class)
的父类位nil
退出循环。 -
使用
XQPerson(Class)
的元类XQPerson(Meta Class)
与XQPerson(Class)
进行比较,不相等。 -
使用
NSObject
的对象的isa
即NSObject(Class)
与NSObject(Class)
进行比较,相等,返回YES. -
使用使用
NSObject
的对象的class
即NSObject(Class)
与NSObject(Class)
进行比较,相等,返回YES. -
使用
XQPerson
的对象的isa
即XQPerson(Class)
与XQPerson(Class)
进行比较,相等,返回YES. -
使用使用
XQPerson
的对象的class
即XQPerson(Class)
与XQPerson(Class)
进行比较,相等,返回YES.
这个面试题很好的考察了对isa走向和继承链的关系。