本文的目的主要是理解类与 isa 是如何关联的
在介绍正文之前,首先要理解一个概念:oc 对象的本质是什么
OC 对象的本质
在探索本质之前,先了解一个编译器 clang
Clang
- 是有 Apple 主导编写,基于 LLVM 和 c/c++/oc 的编译器
- 主要是用于底层编译,将一些文件输出成 c++文件,例如 main.m 输出成 main.cpp,其目的是为了更好的观察底层实现逻辑.
探索对象本质
- 在 main 中自定义一个 LGPerson 类,写一个 name 属性
@interface LGPerson : NSObject
@property(nonatomic,copy)NSString * name;
@end
@implementation LGPerson
@end
- 通过终端,将 main.m 编译成 main.cpp 文件,可以通过以下几种方式
//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
- 打开 cpp 文件,找到 LGPerson 的定义,发现被定义成了一个结构体
- LGPerson_IMPL 中的第一个属性其实就是 isa,是继承自 NSObject,属于伪继承,伪继承的方式就是把NSObject_IMPL定义为 LGPerson_IMPL 的第一个属性,意味着 LGPerson 就有了 NSObject 的所有属性
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
struct NSObject_IMPL {
Class isa;
};
通过以上分析,理解了 oc 对象的本质,但是有一个疑问,为什么 isa 的类型是 Class???
- 在底层 2中分析过,alloc 核心方法之一initInstanceIsa,通过这个方法的实现,isa 是一个 isa_t 类型
- 而在 NSObject 中定义的 isa 是 class 类型,根本原因在于,isa 对外反馈的是类信息,为了让开发人员更加清晰明确,在 isa 返回时做了一个强制类型转换,源码中的强转如下
inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
if (nonpointer) {
return classForIndex(indexcls);
}
return (Class)cls;
#else
return getClass(authenticated);
#endif
}
inline Class
objc_object::ISA(bool authenticated)
{
ASSERT(!isTaggedPointer());
return isa.getDecodedClass(authenticated);
}
总结
- oc 对象的本质是结构体
- LGPerson 中 isa 继承自 NSObject 中的 isa
objc_setProperty 源码探索
除了 LGPerson 的底层定义,我们还发现了 name 的 set 和 get 方法定义,其中 set 方法依赖objc_setProperty
下面就来探索 objc_setProperty 源码
- 在源码中搜索 objc_setProperty,找到源码实现
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
-
进入reallySetProperty源码实现,其方法主要是新值 retain,旧值 release
总结
通过 objc_setProperty 源码探索,有几下几点说明
- object_setProperty 主要作用就是关联上层 set 方法以及底层reallySetProperty方法,作为一个中间层
- 设计的原因.上层的 set 方法有很多,如果直接调用底层方法,会产生很多的临时变量,当你想查找一个 sel 的时候,会非常麻烦
-
所以苹果采用了适配器设计模式(将底层接口适配为客户端需要的接口),对外提供一个接口,供上层使用,对内调用底层的 set 方法,使其相互不受影响,无论上层怎么变,下层都不变,主要达到一个上下层隔离的目的
下图代表,上层,隔离层,底层的关系
cls 与类的关联原理
探索出发点就是initInstanceIsa函数, 探究 isa 与类是如何关联到一起的
在这之前需要了解一个联合体, 为什么 isa 的类型 isa_t 是联合体类型
联合体union
构造数据类型的方式有两种
- 结构体(struct)
- 联合体(union,也叫共用体)
结构体
结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存
- 缺点:所有变量都分配内存,比较浪费内存,假设有 4 个 int 成员,一共分配了 16 字节的空间,但在使用的时候,你只用了 4 个, 就会有 12 字节浪费掉了
- 优点:存储量大,包容性强, 互相之间不影响
联合体
联合体也是由不同的数据类型组成,但其变量是互斥的,所有成员共占同一块内存,而且共用体采用了内存覆盖覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉
- 缺点:包容性弱
- 优点:所有成员共用一段内存,节省了内存空间
两者的区别
- 内存占用情况
- 结构体的各个成员占用不同的内存,互相不影响
- 共用体各个成员占用同一段内存,修改其中一个成员,会影响其他所有成员 - 内存分配大小
- 结构体的内存 >= 内部所有成员内存相加(中间会有间隙)
- 共用体占用的内存 == 其内部最大成员占用的内存
isa 的类型 isa_t
以下是 isa 指针的类型 isa_t 的定义,从定义可以看出是通过联合体(union)定义的
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
isa类型使用联合体的原因也是基于内存优化考虑的,这里的内存优化是指在 isa 指针中通过char+位域(即二进制中的每一位都可以代表不同的信息)的原理实现,通常来说 isa 指针占用的内存大小是 8 字节,也就是 64 位,足够存储很多信息了,这样可以极高的节省内存
从 isa 的定义中可以看出
- 提供了两个成员 cls 和 bits,由联合体的定义可知,这两个成员是互斥的,也就意味着,初始化 isa 指针,有两种方式
- 通过 cls 初始化,bits 无值
- 通过 bits 初始化,cls 无值
还提供了一个结构体定义的位域,用于存储类信息和其他信息,结构体的成员ISA_BITFIELD这是一个宏定义,有两个版本,arm64(对应 ios 移动端)和x86_64(macos)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; //是否对 isa 指针开启了指针优化
uintptr_t has_assoc : 1; //是否有关联对象
uintptr_t has_cxx_dtor : 1; //是否有 c++实现
uintptr_t shiftcls : 33; //存储类信息
uintptr_t magic : 6; //调试器判断对象是真对象还是未初始化空间
uintptr_t weakly_referenced : 1; //对象是否被指向或者曾经指向一个ARC 弱变量
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; //是否有外挂的散列表
uintptr_t extra_rc : 19//额外的引用计数
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
- nonpointer有两个值,标识自定义的类等,占 1 位
- 0:纯 isa 指针
- 1:不只是类对象地址,还包含了类信息,对象的引用计数等
- has_assoc:标识关联对象标志,占一位
- 0:没有关联对象
- 1:有关联对象
- has_cxx_dtor:表示该对象是否有 c++/oc 的析构器(dealloc),占 1 位
- 如果有,做析构逻辑
- 如果没有,可以更快的释放对象
- shiftcls:表示存储类的指针值,即类信息
- arm64 占 33 位,开启指针优化的情况下,在 arm64 架构下有 33 位存储类指针
- x86_64 占 44 位,
- magic:用于调试器判断当前对象是真对象还是未初始化空间,占 6 位
- weakly_referenced:代表对象是否被指向或者曾经指向一个ARC 的弱变量
- 没有弱引用的变量可以更快的释放
- has_sidetable_rc:表示当对象的引用计数大于 10(举例而已,不一定是 10),则需要借用该变量存储进位
- extra_rc:额外的引用计数,表示该对象的引用计数值,实际是引用计数值减 1,
-
如果对象的引用计数为 10,那么 extra_rc 的值为 9(举例而已),实际上 iphone 真机上的exra_rc 是使用 19 位来存储引用计数的
针对 arm64 平台 isa存储情况如下
-
原理探索
- 通过alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone->initInstanceIsa进入查看实现
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
-
进入 initIsa 实现,主要是初始化 isa 指针
该方法的逻辑主要分为两部分
- 1.通过 cls 初始化 isa
- 2.通过 bits 初始化 isa
验证 isa 指针 位域(0-64)
根据前面提到的位域信息,可以在这里验证一下位域是真的存在的,在newisa.bits 处打一个断点
在执行到这句代码是,通过 lldb 打印p newisa, 然后走到下一行在打印一次 newisa,得到的信息如下图
通过与前一个newisa 相比,后一个的nonpointer变成了 1, magic变成了 59,
-
其中的 59 是十进制的体现, 把 isa 指针从 47 位(x86 下,前面的位域占47 位)开始读取 6 位,在转成 10 进制,就是 59
isa 与类的关联
cls 与 isa 关联的原理,就是 isa 中的 shiftcls 位域存储了类信息,其中initInstanceIsa的过程是将calloc 指针与当前类关联起来,有以下几种验证方式
- 1.通过 initIsa 中的newisa.shiftcls = (uintptr_t)cls >> 3验证
- 2.通过 isa 指针地址与 ISA_MASK 值 & 验证
- 3.通过 runtime 的方法 object_getClass 验证
- 4.通过位运算验证
1.通过 initIsa
- 运行到shiftcls = (uintptr_t)newCls >> 3,其中 shiftcls 存储的是当前类的值信息
- 查看 newCls 信息是 SATest类
-
shiftCls 赋值的逻辑是将编码后的 SATest 数据右移3 位
-
执行 lldb 指令, 打印p (uintptr_t)newCls >> 3得到值存储到 shiftCls 中
-
继续执行到isa = newisa; 打印 p newisa
与bits 赋值结果对比,bits 位域中有两处变化
- cls 由默认值变成了 SATest, isa 与类完美关联
- shiftCls 从 0 变成了有值
为什么在shiftcls 赋值时需要强转
因为内存存储时,不能存储字符串,机器码只能识别 0 和 1 这两种数字,所以需要将其转换为uintptr_t数据,这样 shiftcls 中的数据才能被机器识别,其中uintptr_t为 long
为什么需要右移 3 位
因为 shiftcls 处于 isa 中间部分,前面还有 3 个位域,为了不影响前面 3 个位域,需要右移将其抹零
方式 2:通过 isa & ISA_MASK
- 在 main 中断点到 SATest 创建时,按照下图方式进行打印
arm64 中 ISA_MASK 为0x0000000ffffffff8ULL
x86 中 ISA_MASK 为0x00007ffffffffff8ULL
方式 3 通过 runtime中的函数 object_getClass
-
查找 object_getClass源码实现
-
进入getIsa实现
-
进入 ISA()实现
-
进入getDecodedClass实现
-
进入getClass实现
方式 4:通过位运算
-
在 main 中 SATest 创建处加一个断点,通过x/4gx test打印test 存储信息,当前类的信息存储在 isa 指针中,切此时的 shiftcls 占 44 位(因为在 macos 环境下)
-
想要读取中间的 44 位信息,就需要经过位运算,将 shiftcls 右边的 3 位和左边的 17 位都要抹零,相对位置不能变,分为如下几步
- 1.先将 isa >> 3: 将前三位抹零
- 2. 然后用第一步的结果 << 20 (本身左边是 17 位,但是经过第一步以后,左边变成了 20 位)
- 3.第二步的结果 >> 17(回到最初 shiftcls 在 isa 中的初始位置,此时左右已经全部抹零)