OC底层原理 学习大纲
对象的本质
1. Clang探索
Clang
是一个由Apple主导
编写,基于LLVM
的C/C++/Objective-C
轻量级编译器。源代码发布于LLVM BSD
协议下。 Clang将支持
其普通lambda
表达式、返回类型的简化处理
以及更好的处理constexpr
关键字。- 它与
GNU C
语言规范几乎完全兼容
(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载
(通过__attribute__((overloadable))
来修饰函数),其目标(之一)就是超越GCC
。
2. 操作指令:
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
3. 探索
- 构建测试代码
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation HTPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
打开
终端
,cd
到main.m
的文件夹。-
输入
clang
指令:clang -rewrite-objc main.m -o main.cpp
将objc语言的main.m文件重写为cpp格式的文件
-
忽略
warnings
警告,查看main.m
文件夹。发现已生成main.cpp
文件
(cpp
的意思是c plus plus
)
打开
main.cpp
文件。 内容很长。我们搜索自定义类HTPerson
- 可以发现,
对象
在底层已经编译成struct
结构体 -
_I_HTPerson_name
是属性name的get
方法 -
_I_HTPerson_setName_
是属性name的set
方法。 set方法内调用objc_setProperty
方法。
同时我们发现,HTPerson_IMPL
struct 中有一个NSObject_IMPL
struct。 这就是表明HTPerson继承自NSObject。
不相信?
我们在main.m
中创建一个HTCar
类继承自HTPerson
@interface HTCar : HTPerson
@end
@implementation HTCar
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
NSLog(@"%@", person);
}
return 0;
}
使用clang
将main.m
编译为main.cpp
clang -rewrite-objc main.m -o main.cpp
在main.cpp
中搜索HTCar
:
服不服?
结论
OC
结构体没有继承关系,但是C
和C++
语言中,结构体是有继承
关系的。OC对象
在底层被C
语言编程成结构体
。而C语言结构体的继承
方式,是每一个结构体第一个属性
都包含父结构体
的所有信息。如此,实现了OC类
的继承关系OC对象
的本质就是结构体,结构体中包含了所有属性方法和父类信息
4. 探究属性get、set方法
对象核心功能就是
信息的存取
,即get
和set
方法。我们没有独立实现
get
和set
方法,工程中也找不到属性的get
和set
方法,但这2个方法为什么可以直接使用呢?
其实上面已经有答案了。对象
在底层将属性都进行了记录。并自动实现了他们的get
和set
方法。
get方法我们看懂,就是直接访问指针地址,返回
指针地址的值
。set方法调用了
objc_setProperty
函数进行赋值。
我们打开objc4
源码。搜索objc_setProperty
。
-
点击进入内部,发现所有
objc_setProperty
方法都调用了reallySetProperty
:
所有值变更(
set
),都是reallySetProperty
在处理。(同时管理引用计数)
所有外层属性的set
方法。都会来到objc_setProperty
方法,调用了reallySetProperty
实现set功能。
- 外部
set
方法: 个性化定制层(例如setName、setAge等)
↓ -
objc_setProperty
:接口隔离层 (将外界信息转化为对内存地址和值的操作)
↓ -
reallySetProperty
:底层实现层 (赋值和内存管理)
这是一个典型的封装设计思维。
isa
oc对象
的本质是结构体
,我们在main.cpp
的文件中了解到。HTPerson_IMPL
继承自NSObject_IMPL
。我们搜索
struct NSObject_IMPL
- 发现
NSObject
对象在底层就是编译为isa
, 其类型为Class
到这里,我们可以肯定对象在底层,是通过继承isa来继承父类信息。
- 在OC底层原理三:探索alloc (你好,alloc大佬 )中,我们知道了alloc的三大核心函数,包含
initInstanceIsa
创建isa并完成类的关联。
现在,让我们揭开isa
的神秘面纱。
1. union联合体位域
首先了解union
联合体位域,isa
的类型结构就是union。
小案例:
如果我们创建Car
对象,我们需要控制它的前后左右
4个方向。我们可以这样定义:
@interface Car : NSObject
@property(nonatomic, assign) BOOL front; // 2字节
@property(nonatomic, assign) BOOL back; // 2字节
@property(nonatomic, assign) BOOL left; // 2字节
@property(nonatomic, assign) BOOL right; // 2字节
@end
-
BOOL
类型占用2字节
, 每个字节是8位(Bit)。 -
Car
对象所有属性占用内存大小:4属性 * 2字节 * 8位 = 64位
。
在系统层面
,我们会考虑极致的性能
。用4位
就实现前后左右的处理,每1位
记录一个方向
的信息。极大的节约内存空间
。
- 用
2位
更节省,每1位
可记录2个信息
.- 但使用
4位存储
,每1位独立记录1个信息。可以使用位运算
来高效处理,在性能
上更有优势
)
这就是我们要介绍的union
联合体位域。
-
结构体
的类型大小大于等于
内部所有变量的类型大小总和(参考结构体内存优化) -
联合体
类型大小等于
最大成员类型大小 -
位域
: 每一个二进制位
均表示不同信息
2. isa结构
我们在objc4
源码中找到initIsa
发现isa
的赋值是isa_t
结构,进入查看:
发现isa_t
就是使用的union联合体
结构。
通常来说,isa指针
占用内存大小是8
字节,即64位
。对于系统来说已经足够了。
- 我们知道
union联合体
内部属性是互斥
关系。 所以cls
和bits
不共存。
进入ISA_BITFILED
宏定义,可以看到isa
的全部结构
。 庐山真面目揭开了。
nonpointer
: 表示是否对 isa 指针开启0
:纯isa指针,1
:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
has_assoc
: 关联对象标志位,0
没有,1
存在
has_cxx_dtor
: 该对象是否有C++
或Objc
的析构器,如果有析构函数,则需要做析构逻辑
, 如果没有,则可以更快的释放
对象
magic
:用于调试器判断当前对象是真对象
还是未初始化
的空间
weakly_referenced
: 对象是否被指向或者曾经指向一个ARC
的弱变量
, 没有弱引用的对象可以更快释放
。
deallocating
:标志对象是否正在释放
内存
has_sidetable_rc
:当对象引用技术大于10
时,则需要借用该变量存储进位
extra_rc
:当表示该对象的引用计数
值,实际上是引用计数值减 1。
如: 如果对象的引用计数为 10,那么extra_rc
为 9。如果引用计数大于 10, 则需要使用到has_sidetable_rc
。
-
isa
中最重要的是shiftcls
,它存储了类指针
的值。但是除了这个信息,isa
还存储了很多其他标志性信息。
现在我们了解了isa
的结构,让我们运行objc4源码
来完整了解信息
3. 检验isa
-
main.m
中测试代码,在HTPerson
初始化一行加入断点
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
NSLog(@"%@", person);
}
return 0;
}
进入alloc
->_objc_rootAlloc
->callAlloc
->_objc_rootAllocWithZone
-> _class_createInstanceFromZone
,加入断点:
继续进入initInstanceIsa
->initIsa
,加断点:
需要确保cls
是HTPerson
。
SUPPORT_INDEXED_ISA
宏定义:
我们现在是电脑端
,所以是这个条件为false
,宏的值为0
isa_t
是上面讲到的isa
默认初始化的方法-
所以
isa_t newisa(0)
后,newisa
已经完成默认初始化,但还未赋值
,我们打印p newisa
:
-
进入
ISA_MAGIC_VALUE
宏,看到
-
继续断点,打印:
-
为何
magci
是59?
打开计算器,将显示改为编程器
,选择16进制,粘贴cls
的地址
我们看到1 1101 1
。
- 将计算器改为
10进制
,输入59
,看二进制结果:
这个59
是在默认值中设定的。
-
接着往下走,断点设在
shiftcls
后一行,打印:
此时,我们已将
HTPerson
类信息完整的存到isa
的shiftcls
中。isa
与HTPerson
完成绑定。
为何要右移3位?
因为(uintptr_t)cls
是将cls初始化为uintptr_t
格式。但是初始化时,前3位
是标记符,shiftcls
是从第四位才开始。所以要移除前三位。
我回到上一层_class_createInstanceFromZone
,加断点。继续走。
- 打印
obj
,x/4gx
查看地址信息。 首地址就是isa
。 - 我们取
isa
地址。按照isa
的初始化格式,我们&
mask偏移值(查上面isa结构图)。就得到了shiftcls
。 -
shiftcls
存储的就是类信息。 所以直接打印出了类信息。
当我们对
isa
的结构完全熟悉后。就能理解为什么首地址符有时候打印不出类名了。因为标记符可能存在数据,影响了地址的读取。类的信息只存储在isa的
shiftcls
中。我们可以手动左移右移,将前3后17位置的信息全部移除。这样就可以直接读取了。
拓展
runtime
运行时object_getClass(perosn)
返回的也是isa地址。
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
梳理了一份 【对象、类、isa 的逻辑关系】
拓展答疑:
- 属性修饰符
strong
、weak
、retain
、copy
、assign
:
clang
编译文件,打开cpp文件,可以发现:
retain
和copy
都是调用了objc_setProperty
。 不同的是objc_setProperty
内部实现不同
:
(详看objc4源码
中的objc_setProperty
代码)
copy
和mutableCopy
:是新开辟空间,旧值release;其他修饰类型
:是新值retain,旧值release。
strong
、assign
类型都是直接使用地址
进行赋值
(通过对象地址
偏移相应字节找到属性地址
)如果在
set方法
后加入断点
,可以在汇编层
看到所有属性赋值后
,会调用objc_storeStrong
。
运行
代码,进入断点
,可以看到:
(在所有赋值
完成后,objc_storeStrong
在最后执行一次)
- 在
objc4源码
中查看objc_storeStrong
代码。可以发现它内部就是对对象
进行了retain
和release
。
下一节:OC底层原理九:类的原理分析