OC底层原理08:isa和类关联探索
前言
本文的主要目的是理解 类与isa
是如何 关联
的
在介绍正文之前,首先需要理解一个概念:OC对象
的 本质
是什么?
在探索oc对象本质前,先了解一个编译器:clang
Clang
1、介绍
Clang
是⼀个由 Apple
主导编写,基于 LLVM的C/C++/Objective-C
编译器
主要是用于 底层编译
,将一些文件输出成 c++
文件,例如 main.m
输出成 main.cpp
,其目的是为了更好的观察 底层
的一些 结构
及 实现
的逻辑,方便理解底层原理。
2、 常用编译命令:
把⽬标⽂件编译成 c++
⽂件,终端输入
clang -rewrite-objc main.m -o main.cpp
3、 xcode安装的时候顺带安装了 xcrun
命令,xcrun
命令在 clang
的基础上进⾏了
⼀些封装,要更好⽤⼀些:
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 mainarm64.cpp (⼿机)
对象的本质
1、 第一步:自定义一个 ZJPerson
类,添加一个属性 name
2、 通过终端,利用 clang
将 main.m
编译成 main.cpp
,有以下几种编译命令,这里使用的是第一种:
1 | // 1、将 main.m 编译成 main.cpp |
3、打开编译好的 main.cpp
,找到 ZJPerson
的定义,发现 ZJPerson
在底层会被编译成 struct
结构体:
1 | // 👇NSObject的定义 |
如下图:
ZJPerson
继承自NSObject
,属于伪继承
,伪继承
的方式是直接将NSObject
结构体定义为ZJPerson
中的第一个属性,意味着ZJPerson
拥有NSObject
中的所有成员变量
。ZJPerson
中的第一个属性就是Class isa
。
4、总结
OC对象
的本质
其实就是结构体
ZJPerson
中的isa
是继承
自NSObject
中的isa
objc_setProperty
由上面可知 ZJPerson
被编译成 结构体
,属性 name
被编译成对应的 set
和 get
方法,其中 set
方法的实现是依赖 runtime API objc_setProperty
实现的。
1、我们可以通过查找 objc4
源码来查看 objc_setProperty
底层的进一步实现:
1)全局搜索 objc_setProperty
,查找所在位置
2)点击进入 objc_setProperty
内部查看源码实现
3)点击 reallySetProperty
查看内部源码实现
2、总结
通过对 objc_setProperty
底层源码探索,我们可得出以下几个结论:
所有
的set
方法最终都将会找到 objc_setProperty LLVM中间隔离层
函数去调用
objc_setProperty
用于关联上层
的set
方法 和下层
的reallySetProperty
的一个接口隔离层
函数这么设计的原因是,如果上层有
很多set
方法,如果你直接调用下层
的LLVM
方法,会产生很多中间层变量
,非常恶心,很难
去处理(很难找
)那么如何区分呢,系统根据
cmd
去查找,就是说无论你上层
怎么变化
,我下层reallySetProperty
都不用变化
,你下层
怎么变化
,上层
都不会被影响
3、上层、隔离层、底层之间的关系图:
联合体和结构体
在 iOS-OC底层原理03:alloc&init&new探索 和 iOS-OC底层原理07:malloc源码探索 中我们分
别探索了 alloc
核心源码中的前两个,分别是 cls->instanceSize
和 calloc
,今天我们来探索最后一个 obj->initInstanceIsa
在此之前我们需要先了解什么是 联合体
,它和 结构体
的区别?
1、构造数据类型的方式有以下两种:
结构体
(struct)联合体
(union,也称为共用体)
2、 结构体
和 联合体
写法:
1 | // 结构体:s表示结构体类型名 |
在 main.m 中打印一下
1 | int main(int argc, const char * argv[]) { |
由结果可知:
结构体
的大小为12
,最大成员变量的倍数4*3 = 12
联合体
的大小为4
,最大成员变量4
3、结构体(struct)
结构体
:各成员各自拥有 自己的内存
,各自使用 互不干涉
,同时存在的,遵循 内存对齐
原则。一个 struct
变量的 总长度
等于 所有成员
的 长度之和
。
缺点:所有属性都分配内存,比较浪费内存,假设有
4个int
成员,一共分配了4*4=16
字节的内存,但是在使用时,你只使用了4
字节,剩余的12
字节就是属于内存的浪费优点:存储容量较大,包容性强,且成员之间不会相互影响
4、联合体(union)
联合体
:各成员 共用
一块 内存空间
,并且同时只有一个成员可以得到这块内存的 使用权(对该内存的读写)
,各变量 共用一个内存首地址
。因而,联合体比结构体更 节约内存
。一个 union
变量的总长度至少能容纳 最大
的成员变量,而且要满足是所有 成员变量
类型大小的 整数倍
。
缺点:包容性弱
优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节约内存
5、两者的区别
1)内存占用
情况
结构体
的各个成员会占用不同
的内存
,互相之间没有影响共用体
的所有成员占用同一段
内存,修改一个成员会影响其余所有成员
2)内存分配
大小
结构体
内存>=
所有成员占用的内存总和(成员之间可能会有缝隙)共用体
占用的内存等于最大的成员占用的内存
isa的类型
1、我们知道 isa
的类型是 isa_t
的 联合体
,isa_t
类型使用 联合体
的 原因
也是基于 内存优化
的考虑;这里的内存优化是指在 isa
指针中通过 char + 位域
(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa
指针占用的 内存
大小是 8
字节(1字节=8位),即 8*8=64
位,已经足够存储很多的信息了,这样可以极大的 节省内存
,以 提高性能
。
isa_t源码:
1 | // 联合体 |
2、从 isa_t
的定义中可以看出:
提供了两个成员,cls
和 bits
,由联合体的定义所知,这两个成员是 互斥
的,也就意味着,当初始化 isa
指针时,有两种初始化方式:
- 如果不是
nonpointer
,通过isa_t(uintptr_t value) : bits(value) { }
实现初始化
1 | // 如果不是nonpointer,初始化isa,返回cls信息 |
- 如果是
nonpointer
,通过isa_t()
初始化
1 | // 如果是nonpointer的初始化方式 |
3、SUPPORT_INDEXED_ISA
:表示 isa_t
中存放的 Class
信息是 Class
的地址,还是一个 索引
(根据该 索引
可在 类信息表
中查找该类结构地址)。目前接触到的iOS的设备上 SUPPORT_INDEXED_ISA
为 0
,如下代码:
1 | // 如果不是64位 |
4、isa_t
还提供了一个结构体定义的 位域
,用于存储类信息及其他信息,结构体的成员 ISA_BITFIELD
,这是一个宏定义,有两个 架构__arm64__(真机)
和 __x86_64__(macOS/模拟器)
,以下是它们的一些宏定义,如下图所示:
nonpointer
:表示是否对isa
指针开启指针优化
0
:如果没有nonpointer
就是纯isa指针
1
:如果有nonpointer
,不⽌有类对象地址
,isa
中包含了类信息、对象的引⽤计数
等
has_assoc
:关联对象标志位
0没有
1存在
has_cxx_dtor
:该对象是否有C++
或者Objc
的析构器
,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象shiftcls
:存储类指针的值
。开启指针优化的情况下:- 在
__arm64__
架构中有33
位⽤来存储类指针
、dealloc
函数 - 在
__x86_64__
架构中有44
位用来存储类指针。
- 在
magic
:⽤于调试器判断当前对象是真的对象
还是没有初始化
的空间
weakly_referenced
:指对象是否被指向
或者曾经指向
⼀个ARC
的弱变量- 如果
有
,则在调用delloc
进行释放
- 如果
没有
,弱引⽤的对象可以更快释放
。
- 如果
deallocating
:标志对象是否正在释放
内存has_sidetable_rc
:当对象引⽤计数⼤于 10
时,则需要借⽤该变量存储进位
extra_rc
:当表示该对象的引⽤计数值
,实际上是引⽤计数值减 1
- 如果对象的引⽤计数为
10
,那么extra_rc
为9
。 - 如果引⽤计数⼤于
10
,则需要使⽤到上⾯
的has_sidetable_rc
。
- 如果对象的引⽤计数为
针对两种不同平台,其 isa
的 存储情况
如图所示:
由上面的分析得出,大部分 自定义的类
都是属于 nonpointer_isa
,不光存储了isa
,还存储了其他一些 信息
。
dealloc释放 重点
我们通过 dealloc释
查看 通过
中 dealloc
释放流程:
1、dealloc
调用 _objc_rootDealloc
:
1 | // Replaced by NSZombies |
2、_objc_rootDealloc
调用 rootDealloc
:
1 | void |
3、rootDealloc
内部进行一些判断,释放 isa
相关信息:
1 | inline void |
4、object_dispose
内部调用 objc_destructInstance
1 | id |
5、objc_destructInstance
内部调用 clearDeallocating
清除
1 | void *objc_destructInstance(id obj) |
具体细节待完善…
isa 和 类关联
isa
与 cls
关联 原理
就是 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
验证
1、首先通过 main
中的 ZJPerson
断点 –> initInstanceIsa
–> initIsa
–> 走到 else
中的 newisa.bits = ISA_MAGIC_VALUE
这一行:
2、 通过lldb调试打印 p newisa
初始化的信息:
这时 isa
通过 newisa
进行了初始化,但是还未被赋值
3、往下走一步,走到下一行 newisa.has_cxx_dtor = hasCxxDtor
:
继续断点调试,在上一行 newisa.bits = ISA_MAGIC_VALUE
已经为 isa
的 bits
成员赋值,执行lldb命令 p newisa
,得到的结果如下:
通过与前一个 newisa
对比我们发现,isa
指针中有一些变化,如下图所示:
赋值
bits
会对cls
进行追加0x001d800000000001
默认值赋值
bits
里面的ISA_BITFIELD
宏中所有的信息nonpointer = 1
,代表是nonpointer
magic = 59
,为什么呢?往下看
4、打开 计算器
我们查看一下 16
进制 0x001d800000000001
转化为 二进制
,和 10
进制 59
转化为 二进制
,进行对比一下:
由对比可知,0x001d800000000001
转化为 二进制
,从 47(因为前面有4个位域,共占用46位)
位开始往后数6位是 110111
; 和 10
进制 magic = 59
转换成 二进制
,从 0
开始 往后数 6
位的 值相等;
5、继续往下执行断点到 newisa.shiftcls = (uintptr_t)cls >> 3
:
shiftcls
存的是类的信息
cls
是ZJPerson
进行编码
之后往右移3位
1 | (lldb) p (uintptr_t)cls |
6、继续往下走一到 执行断点到 isa = newisa
:
我们通过 lldb
调试一下 newisa
:
我们看到 cls = ZJPerson
,而 shiftcls = 536871965
与上面的 $3 = 536871965
刚好吻合
7、最后用一张图来表示一下具体流程:
8、为什么在 shiftcls
赋值时需要类型强转?
因为内存的存储不能存储字符串,机器码
只能识别 0 、1
这两种数字,所以需要将其转换为 uintptr_t
数据类型,这样 shiftcls
中存储的类信息才能被 机器码理解
, 其中 uintptr_t
是long
9、为什么需要右移3位?
主要是由于 shiftcls
处于 isa
指针地址的 中间
部分,前面还有 3
个位域,为了不影响前面的3个位域的数据,需要 右移
将其 抹零
。
【方法二】:通过 isa
指针地址与 ISA_MSAK
的值 & 来验证
1、在方法一执行完后,回到 _class_createInstanceFromZone
中,此时 isa
与 cls
已经关联完成,我们走到下面这个方法,打上断点:
2、执行 po obj
,输出对应的值,再执行 x/4gx 0x10181ba70
得到 isa
指针的地址 0x001d8001000020e9
,将 isa
指针地址 & ISA_MASK
,即 po 0x001d8001000020e9 & 0x0000000ffffffff8ULL
或 po 0x001d8001000020e9 & 0x00007ffffffffff8ULL
,得出 ZJPerson
__arm64__
中,ISA_MASK
宏定义的值为0x0000000ffffffff8ULL
__x86_64__
中,ISA_MASK
宏定义的值为0x00007ffffffffff8ULL
【方法三】:通过 object_getClass
通过查看 object_getClass
的源码实现,同样可以验证 isa
与 类
关联的原理,有以下几步:
1、main
中导入 #import <objc/runtime.h>
2、通过 runtime
的 api
,即 object_getClass
函数获取类信息
1 | object_getClass(<#id _Nullable obj#>) |
3、查看 object_getClass
函数 源码的实现
4、点击进入 object_getClass
底层实现
5、点击 getIsa
进入查看底层实现:
6、点击 ISA
进入查看底层实现,可以看到如果是 SUPPORT_INDEXED_ISA
类型,执行 if
流程,反之 执行的是 else
流程,我们这里走的是 else
:
在else流程中,拿到
isa
的bits
这个位,再& ISA_MASK
,这与方式二中的原理是一致的,获得当前的类信息从这里也可以得出
isa
与cls
已经完美关联
【方法四】:通过位运算
1、回到 _class_createInstanceFromZone
方法。通过 x/4gx obj
得到 obj
的 内存分布情况
,当前类的信息存储在 isa
指针中,且 isa
中的 shiftcls
此时占 44
位(因为处于macOS
环境)
2、想要读取中间的 44
位 类信息(shiftcls)
,就需要经过 位运算
,将 右边3位
,和 左边17
位 除去
,44位以外
的部分都 抹零
,其 相对位置是不变的
。其位运算过程如图所示,其中shiftcls
即为需要读取的类信息:
3、将isa地址右移3位:p/x 0x001d8001000020e9 >> 3
,得到 0x0003b0002000041d
在将得到的
0x0003b0002000041d
左移20位:p/x 0x0003b0002000041d << 20
,得到0x0002000041d00000
为什么是左移20位?因为先右移了3位,相当于向右偏移了3位,而左边需要抹零的位数有17位,所以一共需要移动20位
将得到的
0x0002000041d00000
再右移17位:p/x 0x0002000041d00000 >> 17
得到新的0x00000001000020e8
获取cls的地址 与 上面的进行验证 :
p/x cls
也得出0x00000001000020e8
,所以由此可以证明isa
与cls
是关联的
4、为什么右移3位,左移20位,再右移17位?
因为 initIsa(初始化isa)
传入的是 cls(即ZJPerson)
,只有中间 shiftcls
才 存储着类的信息
,在 __x86_64__
架构是 44
位,在 __arm64__
架构是 33
位,只有在 33位/44位
才存 储着类信息
,当右移3位,左移20位,再右移17位,相当于复位,不足64位 系统
自动帮我们 用0补全
。
isa返回Class类型的验证
1、我们在前面探索了 对象
的 本质
是 结构体
,它的第一个 属性
是 继承
自 NSObject
的 isa
是 Class
类型的,而后面我们通过【方法三】object_getClass
验证 isa
与 cls
关联流程得知, isa
是 isa_t
类型的:
2、那么为什么我们在 外层
得到的是 Class
的 isa
呢?
我们其实在上面的【方法三】,已经验证了其原因,看源码 return (Class)(isa.bits & ISA_MASK);
这一句代码将其 强转
为 Class
类型提供给外层使用。
- Post title:OC底层原理08:isa和类关联探索
- Post author:张建
- Create time:2020-09-18 15:04:23
- Post link:https://redefine.ohevan.com/2020/09/18/OC底层原理/OC底层原理08:isa和类关联探索/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.