前言
通过分析 alloc原理 和 内存对齐原理,只是了解了如何创建 对象,alloc流程 及 内存对齐 原则,却对 对象 的本质及 isa 知之甚少。下面具体理解一下对象的本质及isa的原理。
基本知识
位域
-
产生:有些信息在存储时,并不需要占用一个
完整的字节,而只需占一个或几个二进制位。例如在存放一个只有0和1两种状态时, 用一个二进位即可。基于这种原理,C语言提供了一种叫做位域的数据结构。 -
定义:在定义结构体或联合体时,成员变量后面加
: 数字,用来限定成员变量占用的位数,这就是位域。 -
目的:节省存储空间,处理简便。
下面通过代码理解位域的原理。
创建两个结构体,定义属性如下,一个使用位域前结构体,一个使用位域后结构体。
打印其内存结果,如下:
总结:
没有位域的结构体s1占用的内存空间大小为4字节;使用位域的结构的大小为1字节。
$ p s3可得s3的值:s3 = (a = 255, b = 255, c = NO, d = 255)
$ x/1bt &s3可得内存中二进制数据为0x00001011。低4位(从低到高)分别对应的是a、b、c、d的值;共占4个二进制位,再进行结构体的内存对齐,总大小为最大成员变量的整数倍,为1字节;
联合体(union)
联合体的语法和结构体非常类似,但是它们占用内存的情况却不同。
联合体(union)和结构体(struct)的差异:
- 结构体的成员之间是
共存的:各个成员占用不同的内存,它们互相之间没有影响。 - 联合体的成员之间是
互斥的:所有成员共用同一段内存,修改一个成员的值,会影响其余所有成员。 - 结构体占用的内存:
大于等于所有成员占用内存的总和(需要内存对齐) - 联合体占用的内存:
等于最大的成员占用的内存,同一时刻只能保存一个成员的值
下面通过代码理解联合体(union)和结构体(struct)的差异。
创建一个联合体
对联合体进行赋值,并打印其内存情况:
1. 没有赋值的情况:
2. a 赋值的情况:
3. b 赋值的情况:
4. c 赋值的情况:
总结:
联合体可以定义多个不同类型的成员,联合体的内存大小由其中
最大的成员的大小决定。联合体中
修改其中的某个变量会覆盖其他变量的值。联合体所有的变量
公用一块内存,变量之间互斥。优缺点:
优点:节省内存。
缺点:不够包容,同一时刻
只能保存一个成员的值。
对象的本质
如何对对象底层进行探究?首先了解一波 编译器
准备工作
- 新建一个
Project - 创建如下文件,并添加属性
编译器 Clang
Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。
把⽬标⽂件编译成c++⽂件
$ clang -rewrite-objc main.m -o main.cpp
如果遇到如下 UIKit 未找到的问题
执行如下指令
$ clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.4 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk为本地sdk路径
-runtime=ios-14.4:14.4为版本号,可在下面的路径拿到。
结果如下:
编译器 xcrun
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 os-mainarm64.cpp
模拟器编译如下:
结果如下:
真机编译如下:
结果如下:
main.cpp文件分析
1. 分析对象属性
从源码分析可得:
NSObject的底层实现都是objc_object结构体类型。
struct ZLObject_IMPL:对象的底层是结构体。嵌套
NSObject_IMPL结构体。即对象继承于NSObject。属性以
_开头的,代表成员属性。
其中 NSObject_IVARS 是什么?并不知道。
2. 分析 NSObject_IMPL
NSObject_IMPL里面只有一个成员变量是Class isa。
3. 分析 Class
在 main.cpp 文件中全局搜索 *Class。代码如下:
从源码分析可得:
Class的底层是objc_class类型的结构体指针。
objc_object里面也是只有一个成员变量是Class isa。泛型指针
id:常用的id是一个objc_object结构体指针,这就为什么平时在使用id修饰变量时为什么不加*。
SEL:SEL也是结构体指针。
4. 分析 get 和 set 方法
从源码分析可得:
属性的
get和set方法中通过获取当前对象的首地址+变量的偏移值来得到当前变量,进而实现get和set对当前变量的修改和获取。定义的属性,底层自动添加了
get和set方法。get方法名为_I_类名_属性名,set方法名为_I_类名_set属性名_,例如:_I_ZLObject_name和_I_ZLObject_setName_。隐含参数:当前对象
self,方法_cmd。
isa的本质
在分析 alloc原理 时,跳过了对 isa 的分析,下面具体分析 isa 的原理。
回忆 alloc 流程,其流程图如下:
其中 obj->initInstanceIsa 方法如下:
最终都会执行 objc_object::initIsa 方法:
这也印证了对象的底层就是
objc_object结构体的观点,即对象在调用initIsa方法时,也是底层objc_object的结构体调用initIsa方法。
initIsa 的流程分析
1. isa_t 分析
总结:
isa_t是一个联合体,联合体的成员变量存储是互斥的。有两个成员变量
bits和cls,使用同一块内存。
2. ISA_BITFIELD 分析
其中 __has_feature(ptrauth_calls) 是什么呢?
__has_feature:此函数的功能是判断编译器是否支持某个功能ptrauth_calls:指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构- 参考链接:developer.apple.com
- 验证流程请参考 jr大神。
针对这三种类型,其 64 位存储分布图如下:
遍历分析如下:
nonpointer:是否对isa指针开启指针优化。0纯isa指针;1不⽌是类对象地址,isa中包含了类信息、对象的引⽤计数等。has_assoc:关联对象标志位,0没有,1存在has_cxx_dtor:是否有 C++ 或者 Objc 的析构器,1有析构函数,需要做析构逻辑;0没有,则可以更快的释放对象。shiftcls: 存储类指针的值。magic:⽤于调试器判断当前对象是真的对象,还是没有初始化的空间。weakly_referenced:是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。deallocating:是否正在释放内存。has_sidetable_rc:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位。extra_rc:对象的引⽤计数值,实际上是引⽤计数值减 1。例如,如果对象的引⽤计数为 10,那么extra_rc为 9。如果引⽤计数⼤于 10,则需要使⽤has_sidetable_rc存储进位。
3. 关联类的方式
方式一:位运算 (此处以 __86_64__ 为例)
类信息是存储在
isa指针中,其中shiftcls是对应的对象信息。按位偏移是目的是只保留shiftcls信息,其它位的信息清0。最终shiftcls的相对位置要保持不变。如图:
方式二:与运算 isa & ISA_MASK
其中
ISA_MASK为掩码,用于与isa指针地址与运算。值也是区分内核的:
__x86_64__内核下是0x00007ffffffffff8ULL
arm64e内核和模拟器下是0x007ffffffffffff8ULL除
arm64e以外的其他arm64内核下是0x0000000ffffffff8ULL
方式三:原生方法 setClass
shiftcls = (uintptr_t)newCls >> 3
补充:为什么类的isa和元类的地址是一样的?
原理: 从
isa的结构来看,对于对象来说,没有是否释放、引用计数等字段,存储的只有元类本身,所以类的isa和元类的地址是一样。