- 对象的本质是什么?
- Clang和xcrun的基础定义。
- 如何将.m文件编译成.cpp文件。
- .cpp文件源码分析。
- 结构体和联合体的区别。
- 如何指定成员变量的位域?
- nonpointerIsa指针。
- isa的64个bit位都存了什么?
- 属性getter和setter是如何读写的。
对象的本质
探索方式:
由于Objective-C
语言是C
语言和C++
的一种超集,那么可以通过轻量编译器Clang
来对Objective-C
代码进行rewrite
,还原Objective-C
代码的底层实现来进行探索。Clang
可以将.m
文件编译成.cpp
文件。
Clang和xcrun
Clang:
Clang
是一个由Apple主导的,用C++
编写的,基于LLVM
的,发布于BSD
许可证下的C
、C++
、Objective-C
的轻量级编译器,其目标(之一)就是超越GCC
。
xcrun
xcrun
是一个Xcode的脚本指令集,其在Clang
的基础上进行了封装,更加方便使用。
准备阶段:
将.m
文件编译成.cpp
文件
Clang编译:
clang -rewrite-objc main.m -o main.cpp
重写带有UIKit
库的文件可能出现的问题:
In file included from ViewController.m:9:
./ViewController.h:9:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
^~~~~~~~~~~~~~~
1 error generated.
解决方法:
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /
Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m
xcrun编译:
// 模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
// 真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
分析阶段:
在main.m
文件里声明一个XJPerson
的类:
打开终端,进入main.m
所在文件夹,运行上面命令生成main.cpp
,打开main.cpp
搜索XJPerson
开始探索,会发现很多值得关注的东西:
关注点一:objc_object
在main.cpp
文件里发现了XJPerson
对象的内容,在.m
文件中XJPerson
继承自NSObject
,但是在.cpp
文件中却是objc_object
。
结论一:针对
.m
文件的声明和.cpp
文件的源码对比,可以得出结论,对象在底层的本质就是objc_object
结构体。
关注点二:isa
在XJPerson_IMPL
结构体内看到了struct NSObject_IMPL NSObject_IVARS
结构体,转而在.cpp
文件中搜索NSObject_IMPL
结构体(XJPerson_IMPL
结构体内嵌套NSObject_IMPL
结构体,就是结构体的伪继承)。
结论二:
NSObject_IVARS
就是成员变量isa
。
关注点三:Class
搜索objc_object
时在.cpp
文件中发现了
typedef struct objc_class *Class;
结论三:
Class
在底层的类型是objc_class *
这样的结构体指针。
关注点四:id
typedef struct objc_object *id;
结论四:
id
在底层的类型是objc_object *
,所以可以表示任意类型的对象。
关注点五:属性的getter
和setter
方法
getter
方法图解:
setter
方法图解:
结论五:在
getter
和setter
方法中看到了两个参数XJPerson * self, SEL _cmd
,在.m
文件中自定义getter
和setter
方法对比可知这是隐藏参数,Objective-C
创建的方法通常默认携带
这两个参数,这就是在每一个方法里都可以使用self
的原因;getter
和setter
方法是通过地址偏移的方式找到成员变量的位置进行存值、取值的。
结构体,联合体,位域
在探索isa
之前先补充点结构体、联合体和位域的知识。
案例:
#import <Foundation/Foundation.h>
// 不指定位域
// 4字节 * 每字节8位 = 32位 0000 0000 0000 0000 0000 0000 0000 0000
// 但BOOL值只有0和1,这4个BOOL值只需要用1个字节的后4位就可以表示了 0000 1111
// 只需要1个字节,却用了4个字节,浪费了3个字节
struct XJCar1 {
BOOL front; // 1字节
BOOL back;
BOOL left;
BOOL right;
};
// 指定位域后,每个BOOL值只占1位,整个结构体只需占用1字节
struct XJCar2 {
BOOL front: 1; // 指定位域为1bit,不是1字节
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
// 结构体:共存,占用内存为所有成员变量内存之和,然后以结构体内存对齐原则对齐
struct XJTeacher1 {
char *name; // 字符串,指针,8字节
int age; // 4字节
double height; // 8字节
}; // 8 + 4 + 8 = 20,再以8字节对齐,一共24字节
// 联合体:互斥,所有成员变量共用一块内存空间
union XJTeacher2 {
char *name; // 字符串,指针,8字节
int age; // 4字节
double height; // 8字节
}; // 8字节
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct XJCar1 car1;
struct XJCar2 car2;
NSLog(@"---- car1 sizeof = %ld ---- car2 sizeof = %ld ----",sizeof(car1), sizeof(car2));
struct XJTeacher1 teacher1;
teacher1.name = "Cooci";
NSLog(@"---- struct XJTeacher1 (name = %s, age = %d, height = %f)-----sizeof = %ld----", teacher1.name, teacher1.age, teacher1.height, sizeof(teacher1));
teacher1.age = 18;
NSLog(@"---- struct XJTeacher1 (name = %s, age = %d, height = %f)-----sizeof = %ld----", teacher1.name, teacher1.age, teacher1.height, sizeof(teacher1));
union XJTeacher2 teacher2;
teacher2.name = "Cooci";
NSLog(@"---- union XJTeacher2 (name = %s, age = %d, height = %f)-----sizeof = %ld----", teacher2.name, teacher2.age, teacher2.height, sizeof(teacher2));
teacher2.age = 18;
NSLog(@"---- union XJTeacher2 (name = %s, age = %d, height = %f)-----sizeof = %ld----", teacher2.name, teacher2.age, teacher2.height, sizeof(teacher2));
}
return 0;
}
********************** 运行结果 ************************
2021-06-17 19:01:57.983162+0800 001-联合体位域[3350:202587] ---- car1 sizeof = 4 ---- car2 sizeof = 1 ----
2021-06-17 19:01:57.983463+0800 001-联合体位域[3350:202587] ---- struct XJTeacher1 (name = Cooci, age = 0, height = 0.000000)-----sizeof = 24----
2021-06-17 19:01:57.983489+0800 001-联合体位域[3350:202587] ---- struct XJTeacher1 (name = Cooci, age = 18, height = 0.000000)-----sizeof = 24----
2021-06-17 19:01:57.983502+0800 001-联合体位域[3350:202587] ---- union XJTeacher2 (name = Cooci, age = 15791, height = 0.000000)-----sizeof = 8----
2021-06-17 19:01:57.983535+0800 001-联合体位域[3350:202587] ---- union XJTeacher2 (name = , age = 18, height = 0.000000)-----sizeof = 8----
图解:
结论:
- 结构体(
struct
)中所有变量是共存
的,优点是“有容乃大”,全面;缺点是内存空间的分配是粗放的
,不管是否使用,全部分配。- 结构体(
struct
)可以通过为成员变量指定位域的方式来优化内存。- 联合体(
union
)中各变量是互斥
的,优点是内存使用更为精细灵活,节省了内存空间,缺点就是不够包容
。
nonPointerIsa
前文OC底层原理初探之对象的本质(一)alloc探索上探索alloc
底层实现时发现进入objc_object::initIsa
函数后对象和类就绑定起来了,现在就来分析下isa
。
isa
从图中可以看出isa
的类型是isa_t
,control
+ command
+ 鼠标左键点击isa_t
,观看其定义,发现isa_t
是一个联合体(union
)。
ISA_BITFIELD
查看宏定义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…。
图解(arm64
):
isa
还原类信息
位运算还原:
上面已经分析了isa
指针内存放的数据内容,现在就通过位运算来验证一下(M1芯片iMac
,arm64
架构)。
案例:
#import <Foundation/Foundation.h>
#import "XJPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
XJPerson *p1 = [XJPerson alloc];
NSLog(@"%@", p1);
}
return 0;
}
********************** 位运算测试结果 ************************
(lldb) x/4gx p1
0x1009c2080: 0x000021a1000082c1 0x0000000000000000 // 格式化输出p1,拿到isa地址0x000021a1000082c1
0x1009c2090: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x000021a1000082c1 >> 3 // 右移三位,得到新的地址
(long) $1 = 0x0000043420001058
(lldb) p/x 0x0000043420001058 << 31 // 将新的地址左移31位,得到新的地址
(long) $2 = 0x1000082c00000000
(lldb) p/x 0x1000082c00000000 >> 28 // 将新的地址右移28位,得到新的地址
(long) $3 = 0x00000001000082c0
(lldb) po 0x00000001000082c0 // 输出新地址,得到XJPerson
XJPerson
图解:
测试所用电脑为M1芯片iMac
,arm64
架构,生成的对象的ISA_BITFIELD
第3位至第35位为XJPerson
的信息,还原方式说明:
- 通过
x/4gx p1
格式化输出p1
对象的内存信息,首地址为isa
指针0x000021a1000082c1
。 - 将
isa
的前3
个bit位移除,即0x000021a1000082c1
向右移3
位,通过p/x
得到新的指针0x0000043420001058
。 - 将
isa
指针的后28
个bit位移除,由于刚刚向右移了3
个bit位,那么现在就需要向左移31
个bit位,即0x0000043420001058
向左移28 + 3 = 31
位,通过p/x
得到新的指针0x1000082c00000000
。 - 最后将
isa
内代表XJPerson
信息的33
个bit位还原,即0x1000082c00000000
向右移28
个bit位,通过p/x
得到新的指针0x00000001000082c0
。 - 最后
po
输出0x00000001000082c0
,得到的结果为XJPerson
。
ISA_MASK
还原:
#import <Foundation/Foundation.h>
#import "XJPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
XJPerson *p1 = [XJPerson alloc];
NSLog(@"%@", p1);
}
return 0;
}
********************** 位运算测试结果 ************************
(lldb) x/4gx p1
0x1009c2080: 0x000021a1000082c1 0x0000000000000000 // 格式化输出p1,拿到isa地址0x000021a1000082c1
0x1009c2090: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x000021a1000082c1 & 0x0000000ffffffff8ULL // isa & ISA_MASK(0x0000000ffffffff8ULL),得到新的地址
(long) $1 = 0x00000001000082c0
(lldb) po 0x00000001000082c0 // 输出新地址,得到XJPerson
XJPerson
ISA_MASK
还原说明:
- 通过
x/4gx p1
格式化输出p1
对象的内存信息,首地址为isa
指针0x000021a1000082c1
。 - 将
isa
指针&
上ISA_MASK
,即0x000021a1000082c1
&0x0000000ffffffff8ULL
,通过p/x
得到新的指针0x00000001000082c0
。 po
输出0x00000001000082c0
,得到的结果为XJPerson
。
备注:iOS
属于小端模式,数据读写从右往左。
init
源码分析:
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
结论:通过源码可以看出来,
init
只是直接返回了对象本身而已,这是工厂设计模式的构造方法,用于提供接口给子类重写,便于扩展。
new
源码分析:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
结论:通过源码可以看出,
new
方法就等于alloc + init
。不建议直接使用new
方法来初始化对象,因为没法使用initWithXXX
这种构造方法。