alloc底层原理这篇文章主要介绍了,当我们创建一个
NSObject
的子类的时候,调用alloc
方法的流程、类创建的对象实际占用的内存大小分析以及系统分配的内存大小分析、影响对象内存大小的因素等问题。结构体内存对齐这篇文章主要介绍了,当我们创建结构体的时候对于相同的成员数以及成员类型,顺序不同导致占用内存大小不一致的原因分析、系统对于
alloc
方法的防Hook
操作、alloc
函数内存开辟过程。那么今天这篇文章一起探索下,当我们创建一个
NSObject
的子类的时候,调用alloc
方法的时候,系统是如何把创建的类对象和isa进行绑定的。
initIsa函数
通过alloc底层原理这篇文章我们可以发现,类cls
和isa
进行绑定是在initIsa
函数中实现的,那么我们就看下initIsa
函数的实现,代码如下:
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
然后继续进入initIsa(cls, false, false);
的实现函数中,发现函数实现中有条件分支,先看下条件分支的判断条件,然后进行代码简化,SUPPORT_INDEXED_ISA
的定义如下:
// 在将类存储在isa字段中的平台上定义SUPPORT_INDEXED_ISA=1作为类表的索引。注意,要与任何定义它的.s文件保持同步。确保也编辑objc-abi.h。
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
可以看到默认走的是SUPPORT_INDEXED_ISA=1
也就是说分支里面走的是true
的判断。所以剔除多余代码后,精简代码如下:
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic是ISA_MAGIC_VALUE的一部分
// isa.nonpointer是ISA_MAGIC_VALUE的一部分
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
newisa.extra_rc = 1;
}
// 在某些情况下,这个写入必须在单个存储中执行(例如,当实现一个类时,因为其他线程可能同时尝试使用这个类)。Fixme使用原子来保证单存储和保证内存顺序。但不要太原子,因为我们不想破坏实例化
isa = newisa;
}
当简化完成函数实现后,我们开始分析函数中的代码,首先就是创建了一个联合体(又称共用体)对象newisa
,那么先去看下这个联合体isa_t
的结构,代码如下:
union isa_t {
// 析构函数,无参数
isa_t() { }
// 析构函数,有参数,设置位域信息
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// 访问这个类需要自定义的ptrauth操作,所以强制客户端通过私有来访问setClass/getClass。
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // 在isa.h文件中进行了定义,isa位域中每个位置锁代表的含义进行了定义
};
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);
};
在分析函数中代码前,我们先做下扩展知识,也就是俗话说的备战,了解相关知识点,才能更快更高效的读懂源码。
补充:
-
uintptr_t
:在很多地方都能看见使用这个关键字去定义变量,那么它到底是什么呢?带着好奇心,经过一顿操作,找到这么一段定义,而且是在Linux
平台的usr/include/stdint.h
头文件中进行的定义。具体代码如下:#if __WORDSIZE == 64 # ifndef __intptr_t_defined typedef long int intptr_t; # define __intptr_t_defined # endif typedef unsigned long int uintptr_t; #else # ifndef __intptr_t_defined typedef int intptr_t; # define __intptr_t_defined # endif typedef unsigned int uintptr_t; #endif
解读下来就以下几点:
在
64
位系统下long int
类型起个别名叫intptr_t
。unsigned long int
类型起个别名叫uintptr_t
。在
32
位系统下int
类型起个别名叫intptr_t
。unsigned int
类型起个别名叫uintptr_t
。至于这么做的原因,应该就是为了提高程序在不同的系统环境下的可移植性。
-
位域
:在C语言中允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单元的成员称为位域。
结构体和联合体(共用体)的异同
- 同:
结构体
和联合体
都是拥有一个或多个成员的结构型数据类型,而且可以相互嵌套包含。 - 异:
结构体
中所有成员之间是可以共存
的,能够同时赋值取值。联合体
中所有成员之间是互斥
的,某一时刻只能有一个值是真实有效的,当给一个成员赋值时其他成员立马就会变成垃圾数据,不是有效值。
MASK(面具)
在iOS开发中经常能够看见一些&(与)
运算,~(取反)
运算等等,在位域运算中经常会遇到&(与)
运算或者位移
运算,从而达到访问指定位置的值。运行objc
源码的时候都是在mac
运行,所以我们主要看下x86
架构下的ISA_BITFIELD
,如下所示:
# define ISA_BITFIELD
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
在x86
架构下的ISA_BITFIELD
中每个位置代表的信息如图所示:
x86
架构下示意图:
arm64
架构下示意图:
nonpointer
在0位,表示是否对isa
指针开启指针优化。0:纯isa
指针,1:不止是类对象地址,isa
包含了类信息、对象的引用计数等。
has_assoc
在1位,表示关联对象标志位,0:没有,1:有。
has_cxx_dtor
在2位,表示该对象是否有C++
或者Objc
的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。
shiftcls
在x86
架构中占用3~46
位,表示存储类指针的值。开启指针优化的情况下,在arm64
架构中占用3~35
位。
magic
在x86
架构中占用47~52
位,在arm64
架构中占用36~41
位,用于调式器判断当前对象是真的对象还是没有初始化的空间。
weakly_referenced
在x86
架构中占用53
位,在arm64
架构中占用42
位,标志对象是否被指向或者曾经指向一个ARC
的弱变量,没有弱引用的对象可以更快释放。
unused
在x86
架构中占用54
位,在arm64
架构中占用43
位,标志对象是否正在释放内存。
has_sidetable_rc
在x86
架构中占用55
位,在arm64
架构中占用44
位,表示当对象引用计数大于10
时,则需要借用该变量存储进位。
extra_rc
在x86
架构中占用56~63
位,在arm64
架构中占用45~63
位,当表示该对象的引用计数值时,实际上是引用计数值减1
,例如:如果对象的引用计数为10
,那么extra_rc
为9
,如果引用计数大于10
,则需要使用到has_sidetable_rc
。
initIsa函数实现
当了解这些扩展知识后,回到开始的isa_t
结构体,主要是setClass
函数,函数有一段注释,大概意思就是在isa中设置类字段。接受要设置的类和指向最终将使用isa的对象的指针。这是使指针正确签名所必需的。注意:此方法不支持设置索引的isa。当使用索引isa时,它只能用于设置原始isa的类。
然后这个函数中重点代码为这一句uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
,因为我们要将结果指针作为函数指针调用,因此需要对其进行签名,也就是对传入的newCls
进行签名。
然后回到开始的initIsa
函数,当创建一个联合体
对象newisa
后,先进行了判断传入的nonpointer
是否为false
,从函数执行流程跟进来的时候,就能看见调用initIsa
的时候写死的传入false
,所以条件分支走进newisa.setClass(cls, this);
,然后继续来到函数setClass
的实现中,因为条件分支比较多,最笨的方法那就是挨个添加断点,看走的那个,如下图:
断点走的是最后的shiftcls = (uintptr_t)newCls >> 3;
这句代码,上面补充中说了,这个值是用来存储类指针的值的。