在探讨这个问题前,我们首先要弄清楚对象的本质什么
编译器clang
clang
是一个由Apple
主导编写,基于LLVM
的C/C++/OC
的编译器
操作指令
//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
//以下两种方式是通过指定架构模式的命令行,使用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
探索对象本质是什么
- 在
main
中自定义一个HLPerson
类,有一个属性name
@interface HLPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation HLPerson
@end
- 打开终端,
cd
到main.m
的文件夹,输入clang
指令:clang -rewrite-objc main.m -o main.cpp
将main.m
编译成main.cpp
- 打开编译好的
main.cpp
,搜索HLPerson
,找到HLPerson
的定义HLPerson
在底层被编译成struct
结构体,属性name
还生成了相应的get
方法_I_HLPerson_name
以及set
方法_I_HLPerson_setName_
在结构体中,我们看到第一个属性为struct NSObject_IMPL NSObject_IVARS
,其实它是继承于NSObject
,这种方式属于伪继承
,伪继承是直接将结构体
定义为HLPerson
中的第一个属性
,意味着HLPerson
拥有该结构体
中的所有成员变量
。
然后我们搜索NSObject_IMPL
struct NSObject_IMPL {
Class isa;
};
发现NSObject_IMPL
中的第一个属性其实就是isa
总结
- OC对象的本质就是
结构体
- 每个对象都有一个
isa
,继承于NSObject
objc_setProperty 源码探索
在上面我们看到除了HLPerson
的底层定义
外,还有其属性
对应的get
和set
方法,其中set
方法其实是依赖于runtime
中objc_setProperty
所实现的
接下来我们来看看objc_setProperty
的底层原理
- 在
objc4-781
中全局搜索objc_setProperty
,找到objc_setProperty
的源码实现 - 跳转至
reallySetProperty
,其方法的原理就是新值retain,旧值release
总结
所有外层属性的set
方法。都会来到objc_setProperty
方法,调用了reallySetProperty
实现set
功能。
这是一种
适配器设计模式(即将底层接口适配为客户端需要的接口)
,对外提供一个接口,供上层的set方法使用,对内调用底层的set方法,使其相互不受影响
,即无论上层怎么变,下层都是不变的,或者下层的变化也无法影响上层,主要是达到上下层接口隔离
的目的
构造数据类型
构造数据类型的方式有以下两种:
-
结构体
(struct
) -
联合体
(union
,也称为共用体
)
结构体struct
结构体
是指把不同的数据组合成一个整体,其变量是共存
的,变量不管是否使用,都会分配内存。 - 缺点:所有属性都分配内存,比较
浪费内存
,假设有4个int成员,一共分配了16字节的内存,但是在使用时,你只使用了4字节,剩余的12字节依旧会分配,这就属于内存的浪费 - 优点:存储
容量较大
,包容性强
,且成员之间不会相互影响
(占用不同内存)
联合体 union
联合体
也是由不同的数据类型组成,但其变量是互斥
的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉 - 缺点:每个变量是
互斥
的,且包容性差
- 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
两者的区别
- 内存占用情况
-
结构体
的各个成员会占用不同的内存
,互相之间没有影响
-
联合体
的所有成员占用同一段内存
,修改一个成员会影响
其余所有成员
-
- 内存分配大小
-
结构体
内存>=
所有成员占用的内存总和
(成员之间可能会有缝隙) -
共用体
占用的内存等于
最大的成员
占用的内存
-
isa的类型 isa_t
查看objc4-781源码
,看到以下isa
指针的类型isa_t
的定义,从定义中可以看出是通过联合体(union)
定义的。
isa_t
的定义中可以看出:
- 提供了两个成员,
cls
和bits
,由联合体的定义所知,这两个成员是互斥
的 - 提供了一个结构体定义的
位域
,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD
,这是一个宏定义
,有两个版本__arm64__
(对应iOS移动端)和__x86_64__
(对应macOS),以下是它们的一些宏定义,如下图所示 -
nonpointer
:表示是否对isa
指针开启指针优化
。0
:纯isa指针;1
:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等 -
has_assoc
:关联对象标志位。0
:没有;1
:存在 -
has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器
,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 -
shiftcls
:存储类指针的值。开启指针优化的情况下,在arm64
架构中有33
位⽤来存储类指针;在x86_64
架构中有44
位⽤来存储类指针 -
magic
:⽤于调试器判断当前对象是真的对象
还是没有初始化的空间
-
weakly_referenced
:志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。 -
deallocating
:标志对象是否正在释放内存 -
has_sidetable_rc
:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位 -
extra_rc
:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1。例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的has_sidetable_rc
两种不同的平台isa
储存情况如图所示
原理探索
- 通过
alloc
-->_objc_rootAlloc
-->callAlloc
-->_objc_rootAllocWithZone
-->_class_createInstanceFromZone
方法路径,查找到initInstanceIsa
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
- 跳转至
initIsa
的源码,即isa指针的初始化
验证isa指针位域
- 首先通过main中的
HLPerson
断点 -->initInstanceIsa
-->initIsa
(由于项目是macOS
环境下,所以使用的是x86_64
),查看如果当前cls
为HLPerson
,往下运行代码至准备赋值bits
- 执行
lldb
命令:p newisa
,打印newisa
的详细信息 - 继续往下执行,走完
newisa.bits = ISA_MAGIC_VALUE;
这一行,表示为isa
的bits
成员赋值完成,重新执行lldb
命令p newisa
,打应结果如下
通过赋值前后对比,我们发现newisa
的值产生了变化。nonpointer
变为了1
,magic
变为了59
,59
转换为2进制
为111011
。根据规则,ISA_MAGIC_VALUE
的第0
位应为1
,52-47
位应为111011
,其它位应为0
。查看ISA_MAGIC_VALUE
定义,该值等于0x001d800000000001ULL
,将其转换为2进制
,如图
isa 与 类 的关联
cls
与isa
关联原理就是isa
指针中的shiftcls
位域中存储了类信息
,其中initInstanceIsa
的过程是将calloc指针
和当前的类cls
关联起来,可以通过以下几种方式来验证:
- 【方式一】通过
initIsa
方法中的newisa.shiftcls = (uintptr_t)cls >> 3;
验证 - 【方式二】通过
isa
指针地址和ISA_MSAK
的值进行&
运算来验证 - 【方式三】通过
runtime
的方法object_getClass
验证 - 【方式四】通过
位运算
验证
方式一:initIsa
- 运行至
newisa.shiftcls = (uintptr_t)cls >> 3;
,其中shiftcls
用于存储当前类的值信息 - 执行lldb命令
p (uintptr_t)cls
,结果为(uintptr_t) $10 = 4295000648
,再将结果右移三位,有以下两种方式(任选其一),将得到536875081
存储到newisa
的shiftcls
中p 4295000648 >> 3
- 通过上一步的结果
$10
,执行lldb命令p $10 >> 3
- 继续执行代码,将
newisa.shiftcls
赋值完成,让后打印newisa
- 查看结果得知,
cls
由默认值变成了HLPerson
,shiftcls
也由0
变成了536875081
,此时isa
与cls
关联已经完成
方式二:isa & ISA_MSAK
-
initInstanceIsa
进行完毕,继续执行,回到_class_createInstanceFromZone
方法,此时cls
与isa
已经关联完成,执行po obj
- 执行
x/4gx obj
,得到isa
指针的地址0x001d80010000824d
- 将
isa指针地址 & ISA_MASK
(处于macOS
环境,使用x86_64
中的宏定义
),即po 0x001d80010000824d & 0x00007ffffffffff8ULL
,得出HLPerson
方式三:object_getClass
-
main
中导入#import <objc/runtime.h>
- 通过
runtime
的api
,即object_getClass
函数获取类信息
object_getClass(<#id _Nullable obj#>)
- 查看
object_getClass
函数源码的实现 - 跳转至
object_getClass
源码 - 跳转至
getIsa
源码 - 可以看到,如果不是一个纯
isa指针
返回的是ISA()
的返回值,跳转至ISA
源码 - 在
else
流程中,拿到isa
的bits
,再& ISA_MASK
,这与方式二中的原理是一致的 - 至此,也可证明
isa
与cls
关联完成
方式四:位运算
- 回到
_class_createInstanceFromZone
方法。执行x/4gx obj
,得到isa
指针的地址0x001d80010000824d
,isa
中的shiftcls
此时占44
位(因为处于macOS
环境)。想要获取shiftcls
,将isa
的前3位
和后17位
抹零即可 - 将
isa
地址右移3位:p/x 0x001d80010000824d >> 3
,得到0x0003b00020001049
- 将
0x0003b00020001049
左移20位,再右移17位,得到0x0000000100008248
,这就是isa
中的shiftcls
,即cls
- 打印
p/x cls
,其结果为0x0000000100008248
,得证~
运算过程如图