在探讨这个问题前,我们首先要弄清楚对象的本质什么
编译器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的定义我们发现image.pngHLPerson在底层被编译成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的源码实现image.png - 跳转至
reallySetProperty,其方法的原理就是新值retain,旧值releaseimage.png
总结
所有外层属性的set方法。都会来到objc_setProperty方法,调用了reallySetProperty实现set功能。

image.png
这是一种
适配器设计模式(即将底层接口适配为客户端需要的接口),对外提供一个接口,供上层的set方法使用,对内调用底层的set方法,使其相互不受影响,即无论上层怎么变,下层都是不变的,或者下层的变化也无法影响上层,主要是达到上下层接口隔离的目的
构造数据类型
构造数据类型的方式有以下两种:
-
结构体(struct) -
联合体(union,也称为共用体)
结构体struct
结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。 - 缺点:所有属性都分配内存,比较
浪费内存,假设有4个int成员,一共分配了16字节的内存,但是在使用时,你只使用了4字节,剩余的12字节依旧会分配,这就属于内存的浪费 - 优点:存储
容量较大,包容性强,且成员之间不会相互影响(占用不同内存)
联合体 union
联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉 - 缺点:每个变量是
互斥的,且包容性差 - 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
两者的区别
- 内存占用情况
-
结构体的各个成员会占用不同的内存,互相之间没有影响 -
联合体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
-
- 内存分配大小
-
结构体内存>=所有成员占用的内存总和(成员之间可能会有缝隙) -
共用体占用的内存等于最大的成员占用的内存
-
isa的类型 isa_t
查看objc4-781源码,看到以下isa指针的类型isa_t的定义,从定义中可以看出是通过联合体(union)定义的。

image.png
isa_t的定义中可以看出:
- 提供了两个成员,
cls和bits,由联合体的定义所知,这两个成员是互斥的 - 提供了一个结构体定义的
位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本__arm64__(对应iOS移动端)和__x86_64__(对应macOS),以下是它们的一些宏定义,如下图所示image.png -
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储存情况如图所示

image.png
原理探索
- 通过
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指针的初始化image.png
验证isa指针位域
- 首先通过main中的
HLPerson断点 -->initInstanceIsa-->initIsa(由于项目是macOS环境下,所以使用的是x86_64),查看如果当前cls为HLPerson,往下运行代码至准备赋值bits
image.png - 执行
lldb命令:p newisa,打印newisa的详细信息image.png - 继续往下执行,走完
newisa.bits = ISA_MAGIC_VALUE;这一行,表示为isa的bits成员赋值完成,重新执行lldb命令p newisa,打应结果如下image.png
通过赋值前后对比,我们发现newisa的值产生了变化。nonpointer变为了1,magic变为了59,59转换为2进制为111011。根据规则,ISA_MAGIC_VALUE的第0位应为1,52-47位应为111011,其它位应为0。查看ISA_MAGIC_VALUE定义,该值等于0x001d800000000001ULL,将其转换为2进制,如图完美印证~image.png
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
image.png
- 继续执行代码,将
newisa.shiftcls赋值完成,让后打印newisaimage.png - 查看结果得知,
cls由默认值变成了HLPerson,shiftcls也由0变成了536875081,此时isa与cls关联已经完成
方式二:isa & ISA_MSAK
-
initInstanceIsa进行完毕,继续执行,回到_class_createInstanceFromZone方法,此时cls与isa已经关联完成,执行po objimage.png - 执行
x/4gx obj,得到isa指针的地址0x001d80010000824d - 将
isa指针地址 & ISA_MASK(处于macOS环境,使用x86_64中的宏定义),即po 0x001d80010000824d & 0x00007ffffffffff8ULL,得出HLPersonimage.png
方式三:object_getClass
-
main中导入#import <objc/runtime.h> - 通过
runtime的api,即object_getClass函数获取类信息
object_getClass(<#id _Nullable obj#>)
- 查看
object_getClass函数源码的实现image.png - 跳转至
object_getClass源码image.png - 跳转至
getIsa源码image.png - 可以看到,如果不是一个纯
isa指针返回的是ISA()的返回值,跳转至ISA源码image.png - 在
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,得证~
运算过程如图image.png


















