前言
通过分析 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__
内核下是0x00007ffffffffff8
arm64e
内核和模拟器下是0x007ffffffffffff8
除
arm64e
以外的其他arm64
内核下是0x0000000ffffffff8
方式三:原生方法 setClass
shiftcls = (uintptr_t)newCls >> 3
补充:为什么类的isa和元类的地址是一样的?
原理: 从
isa
的结构来看,对于对象来说,没有是否释放
、引用计数
等字段,存储的只有元类
本身,所以类的isa
和元类的地址是一样。
网友评论