这是我参与更文挑战的第5天,活动详情查看: 更文挑战
引言
- 我们已经知道iOS对象的
+alloc
方法底层进行了instanceSize(内存分配)
,initInstanceIsa(绑定类型)
等操作,内存分配和计算相关信息可以通过iOS 底层探索01——alloc初探和 iOS 底层探索02——结构体对齐原则进行回顾; - 本文将继续对alloc方法的核心方法
initInstanceIsa(绑定类型)
进行探索;
一、对象的本质
1.1 LLVM与GCC
- 我们都知道OC是C语言的超集,为了探索OC对象的本质,最好的办法是看到OC对象在底层语言的实现;早期的OC编译器使用的是
GCC编译器
,但GCC编译器
编译器的前端和后端过度耦合,当需要扩展语言支持的CPU架构
时不仅要新增编译器的后端功能,前端代码也需要增加,非常麻烦; - 为了解决
GCC编译器
前后端耦合不利于扩展的问题,苹果引入了LLVM编译器
,由于LLVM编译器
的前端和后端是隔离开来的,当扩展语言支持的架构时只需要修改编译器后端,扩展性更高,性能更好,现阶段Xcode
内置的编译器已经替换为LLVM; - 这里我们使用
LLVM编译器
框架中的前端编译器clang
把OC
代码还原成c++
;
1.2 Clang重写OC
有两种还原方式,分别是:
- Clang直接重写
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk main.m
复制代码
- 使用xcrun中的Clang重写
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk main.m
复制代码
上述两种方式都可以将main.m
重写为main.cpp
,重写完成之后我们进一步查看底层C++
文件;
重写前
重写后
1.3 阅读重写后的OC代码
- 通过关键字搜索发现
OC对象GCPerson
被转换成了GCPerson结构体
; GCPerson结构体
内部持有一个NSObject_IMPL结构体
类型的成员变量NSObject_IVARS
和NSString
类型的gcName;- 最终可以确定
OC对象GCPerson
底层主要内容是一个只有一个成员变量isa
的NSObject_IMPL结构体
,并且所有继承于NSObject
的OC对象
底层实现都具有的结构体NSObject_IMPL
struct GCPerson_IMPL {//转换后的结构体GCPerson
struct NSObject_IMPL NSObject_IVARS;
NSString *__strong _gcName;
};
struct NSObject_IMPL {//所有对象底层实现都具有的结构体NSObject_IMPL
__unsafe_unretained Class isa;
};
复制代码
- 同时我们还发现和
id
和Class
的一些底层实现信息
typedef struct objc_object GCPerson;//把objc_object 起别名为GCPerson
typedef struct objc_class *Class;//objc_class结构体的指针起别名为Class
typedef struct objc_object *id;// objc_object结构体指针起别名位id
复制代码
因为*Class
和*id
的别名中已经包含了指针*
所以我们上层在使用id
和Class
类型的时候可以直接不用加*
了;
- 另外可以发现编译器帮我们把
OC对象GCPerson
的属性gcName
添加了set
方法和get
方法;方法中增加了GCPerson
类型的self
和SEL
类型的cmd
这两个参数;
//set方法
static void _I_GCPerson_setGcName_(GCPerson * self, SEL _cmd, NSString *gcName) {
(*(NSString *__strong *)((char *)self + OBJC_IVAR_$_GCPerson$_gcName)) = gcName;
}
//get方法
static NSString * _I_GCPerson_gcName(GCPerson * self, SEL _cmd) {
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_GCPerson$_gcName));
}
复制代码
- 通过
gcName
的get
方法可以看出:获取gcName
成员的方法是self
的地址 +OBJC_IVAR
的地址,然后强转成OC
的NSString
即可;为什么会这样?self
地址是当前对象的首地址,将ivar
跳过之后的下一个位置就是成员变量gcName
的地址;如下图所示;
7. 继续阅读源码我们发现底层中还存在其他信息,如_protocol_t
,ivar
,methodlist
,,category_t
,class_t
等相关信息;
二、nonPointerIsa
2.1 结构体、联合体、位域
在探索nonPointerIsa
方法之前我们先了解下列几个知识点
2.1.1 结构体
结构体(struct)
是一种存储结构,特点是所有成员变量是“共存”的,优点是“有容乃⼤”,信息更全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全部分配,不够精细;
2.1.2 联合体
联合体(union)
也是一种存储结构,特点是各变量是“互斥”的,优点是内存使⽤更为精细灵活,也节省了内存空间;缺点就是不够“包容”,使用覆盖技术导致内部的成员变量互相覆盖,同时只能存储一个成员变量值;如下图所示age
会覆盖掉name
设置联合体的成员变量name
设置联合体的成员变量age
2.1.3 位域
位域
是一种常用于结构体中的存储技术,一般情况下结构体信息的存取以字节为单位,实际上有时存储信息只用一个字节中的几个byte位
即可,这种情况下可以使用位域
存储技术;如图所示
- 例如:一个
BOOL
占用一个字节8位
,4个BOOL
共占用32位,但实际上BOOL
用0
和1
表示只需要一个byte
位即可存储,4个BOOL
用4个byte位
半个字节就够了; 位域
虽然可以节省存储空间,但由于需要读写时需要对特定byte位进行操作,不是很方便,在当前硬件条件下,日常开发中使用位域并不常见;- 位域的读写需要借助于
掩码
来进行,所谓掩码
就是一串特定的byte位
信息,通过将原数据
与掩码
进行按位与&
或者按位或|
运算,即可对位域中的值进行读写;
2.1.4 位域和联合体结合使用
一般情况下位域
和联合体
一起使用,可以实现节省存储空间的作用;
2.2 initInstanceIsa
2.2.1 isa_t
之前我们分析alloc过程中核心方法instanceSize
和calloc
,现在继续分析另一个核心方法initInstanceIsa
,这里可以看到一个关键信息isa_t
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer()); //判断是否TaggedPointer
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
isa = newisa;
}
复制代码
isa_t
的简略结构如下,为了不影响分析我删除了一些无关紧要的信息;
union isa_t {
isa_t() { }//构造方法
isa_t(uintptr_t value) : bits(value) { }//构造方法
uintptr_t bits;
private:
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // 关键信息 defined in isa.h
};
...无关内容
};
复制代码
从isa_t
结构中可以看到ISA_BITFIELD
这个成员变量,进入ISA_BITFIELD
定义界面发现了真相;
2.2.2 ISA_BITFIELD
ISA_BITFIELD
是一个使用了位域存储技术的结构体,结构体内部不同的byte位
存储的信息如下
//arm64真机真机下各个byte位的存储信息
# if __arm64__
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19
# endif
//_x86_64架构下各个byte位的存储信息
# elif __x86_64__
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
复制代码
ISA_BITFIELD
在byte
位上的分布如下图,每一种颜色代表存储某个成员变量所需要占用的byte位
个数
成员变量占用byte位
的位置和成员变量的用途如下表所示
成员变量 | arm下位置 | x86_64下位置 | 用途 |
---|---|---|---|
nonpointer |
63 | 63 | 表示是否对 isa 指针开启指针优化0:纯isa 指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等 |
has_assoc |
62 | 62 | 关联对象标志位,0没有,1存在 |
has_cxx_dtor |
61 | 61 | 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 |
shiftcls |
28-60 | 17-60 | 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针;在X86_64 架构下有44位用来存储类指针 |
magic |
22-27 | 11-16 | ⽤于调试器判断当前对象是真的对象还是没有初始化的空间 |
weakly_referenced |
21 | 10 | 标志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放 |
unused |
20 | 9 | 标志对象是否正在释放内存 |
has_sidetable_rc |
19 | 8 | 当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位 |
extra_rc |
0-18 | 0-7 | 当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc 。 |
2.3 shiftcls的读取方式1
- 我们已经知道
ISA_BITFIELD
为位域存储的,shiftcls
作为位域结构体中的成员变量,读取可以使用ISA_BITFIELD
&ISA_MASK
的方式; - 在
person
对象初始化完毕后,通过x/4gx person
查看person
对象的ISA_BITFIELD
信息; - 根据当前的设备架构,选择合适的
ISA_MASK
与ISA_BITFIELD
进行&(按位与)
操作获取当前对象的Class对象
指向的地址:p/x 0x000001a10405d685 & 0x0000000ffffffff8ULL
; - 通过
p/x person.class
操作查看person
对象指向的Class对象
地址; - 发现步骤3和步骤4获取的
Class对象
地址一致,证明读取方式1是没问题的;
2.4 shiftcls的读取方式2
我们分析系统的initIsa
方法时发现绑定Class
的实际方法是newisa.setClass(cls, this)
,具体实现如下
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
uintptr_t signedCls = (uintptr_t)newCls;
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
uintptr_t signedCls = (uintptr_t)newCls;
if (newCls->isSwiftStable())
signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
# else
# error Unknown isa signing mode.
# endif
shiftcls_and_sig = signedCls >> 3;
#elif SUPPORT_INDEXED_ISA
cls = newCls;
#else
shiftcls = (uintptr_t)newCls >> 3;
#endif
}
复制代码
这段方法本质上是对ISA_BITFIELD
进行移位操作;下面我们就以arm64架构下的真机来演示上述代码所进行的移位操作;
- 因为
ISA_BITFIELD
中的固定位是shiftcls
,所以我们需要通过位移操作,把不相关的byte位
信息给移到64位以外,这样留下来的就是shiftcls
位的信息了; - 首先把
shiftcls
右移3位将shiftcls
右边所有byte位
清空; - 再把
shiftcls
左移3+28位将shiftcls
做边所有byte位
清空; - 经历过步骤3和步骤4的操作后此时
ISA_BITFIELD
中已经只剩下shiftcls
信息了,但是经过移位后shiftcls
的位置发生了改变需要右移28
位还原到原来的位置; - 发现步骤3和步骤4获取的
Class对象
地址一致,证明读取方式2是没问题的;
三、init、new分析
- 对象的
init
方法主要作用:提供一个工厂设计模式的构造函数,用来给子类便于重写初始化进行扩展; new
方法内部也会调用init
方法,new 本质上就是alloc 和 new 结合
3. 汇编查看alloc
会调用objc_alloc_init
,但是new
调用的是 objc_opt_new
, 经过查看objc
源码发现这两个方法的实现是一样的;素以alloc、init
的初始化方式和new
的初始化方法可以看作是一致的;