runtime 结构模型
本文完整版共三篇:
- 阿里、字节:一套高效的iOS面试题(一 - runtime 结构模型 - 上)
- 阿里、字节:一套高效的iOS面试题(一 - runtime 结构模型 - 中)
- 阿里、字节:一套高效的iOS面试题(一 - runtime 结构模型 - 下)
题目来自 阿里、字节:一套高效的iOS面试题
1 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
概括
NSObject 内存仅有一个 Class isa 的成员变量。Class 是一个 objc_class,它继承自 objc_object,内部存储着 superclass、缓存 cache、数据 bits。重点是 bits,其内部存储一个 class_rw_t,rw 指向 class_rw_ext_t 或 class_ro_t。class_ro_t 保存着编译器就确定的成员变量,属性,方法,协议,成员变量布局,weak 成员变量布局。class_rw_rxt_t 内部存有有一个指向 class_ro_t 的指针,还有一些运行时加入的属性,方法,协议等。
isa:是一个联合体。内存存储着 non-pointer(1)、has_assoc(1)、has_cxx_dtor(1)、shiftcls(33 superclass)、magic(6)、weakly_referenced(1)、has_sidetable_rc(1)、extra_rc(19)
描述
@interface NSObject <NSObject> {
Class isa;
}
@end
typedef objc_class * Class;
typedef objc_object * id;
struct objc_object {
char isa_storage[sizeof(isa_t)];
}
struct objc_class : objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
struct class_data_bits_t {
uintptr_t bits;
}
union isa_t {
uintptr_t bits;
private:
Class cls;
public:
struct {
/// 0 代表普通的 isa 指针,存储 Class、MetaClass
/// 1 代表优化过的指针,可存储更多信息:如引用计数
/// 64 位机型下均是 1
uintptr_t nonpointer : 1;
/// 是否存在关联对象,若没有可以更快释放
uintptr_t has_assoc : 1;
/// 是否存在 C++ 析构器,若没有可以更快释放
uintptr_t has_cxx_dtor : 1;
/// 存储类型
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/
/// 在 DEBUG 环境下是否完成初始化
uintptr_t magic : 6;
/// 是否被弱引用过,若没有可以更快释放
uintptr_t weakly_referenced : 1;
/// 是否正在释放
uintptr_t deallocating : 1;
/// 是否存在额外的引用计数记录
/// 当引用记录过大无法存储在 isa.extra_rc 时,引用计数则会存储在一个 sidetable 中
uintptr_t has_sidetable_rc : 1;
/// 额外的引用计数值
uintptr_t extra_rc : 19
};
};
复制代码
2 为什么要设计metaclass
为了保持类的一致性。inst.isa = class, class.isa = metaclass, metaclass.isas = rootclass, rootclass.isa = rootclass
- 调用实例方法时在 inst.isa,也就是类的方法列表中查找
- 调用类方法时在 cls.isa,也就是在元类的方法列表中查找
3 class_copyIvarList & class_copyPropertyList区别
- class_copyIvarList 拷贝所有的成员变量,包括属性对应的成员变量
- clsss_copyPropertyList 仅拷贝属性,不过包含分类中的属性
4 class_rw_t 和 class_ro_t 的区别
- class_ro_t 只读,代表在编译期间就已确定的成员变量,属性,方法,符合的协议等
- class_rw_t 可读可写,包含该类包含在分类中的所有成员变量,属性,方法,协议。内部存在一个 class_ro_t 指针。(新版本内是一个指向 class_rw_ext_t 或 class_ro_t 的指针)
- class_rw_ext_t 可读可写,同老版 class_rw_t
5 category 如何被加载的,两个 category 的 load 方法的加载顺序,两个 category 的同名方法的加载顺序
- category 在
load_images
将didInitialAttachCategories
设置为 true 时加载- 若目标类已实现,则直接附加到目标类中
- 若目标类未实现,则添加至 unattachedCategories 中
- category 的 load 方法在所有非延时类实现之后,如果目标类还未实现则实现
- 不同的 category 的 load 方法加载顺序取决于其在 Compile Sources 中的顺序
- category 中的方法是添加在方法列表的头部的。所以,如果目标类是同一个,后加载的 category 中的方法会“覆盖” 先加载的同名方法的(不止能“覆盖” category 中的同名方法,也能“覆盖”类本身的同名方法)
6 category & extension 区别,能给 NSObject添加 Extension 吗,结果如何
- category 可以在任意地方,其添加的属性是没有实例变量的
- extension 是隐藏在类本身 .m 文件中的。人如其名,是扩展,其添加的属性是有成员变量的
对于系统类来说,我们是拿不到 .m 文件的,所以给系统类添加的 extension 相当于 category,没有意义
7 消息转发机制,消息转发机制和其他语言的消息机制优劣对比
- objc_msgSend
- 方法调用
- receiver 的 NilOrTaggedTest
- 接受者的 nil tagged 监测
- CacheLookUp
- 缓存查找
- MethodTableLookUp
- 方法列表查找
- lookUpImpOrForward
- 查找方法的 IMP
- resolveMethod:
- 动态派发
- forwardingTargetForSelector
- 消息转发
- forwardInvocation
- 消息转发
- 优点
- 灵活:编译期间并不确定真正的函数调用地址,只是把方法调用重写为
objc_msgSend
。在运行期间才会通过上述操作确定函数地址
- 灵活:编译期间并不确定真正的函数调用地址,只是把方法调用重写为
- 缺点
- 效率略低:需要查找,需要更多的时间。方法 cache 就是为了提高效率而设计的,但依然比不上直接调用来得快
8 在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么
方法调用时,方法查询之前。所以跟编译期间没有任何关系。所以这里的答案是:
- 检测 receiver 情况:nil 或 tagged
- 在缓存中查找实现
9 IMP、SEL、Method 的区别和使用场景
struct method_t {
SEL name;
const char *types;
IMP imp;
}
复制代码
是什么 | 使用场景 | |
---|---|---|
IMP | 方法的具体实现,相当于 C 函数,可以调用 | 为类添加方法,替换方法实现 |
SEL | 选择子,只是个消息名称,可以通过 SEL 找到 IMP 或 Method | 日常方法调用,及相关判断 |
Method | 一个保存了方法完整信息的结构体:方法名 name、方法类型 types、具体实现 IMP | 方法交换 method Swizzling |
10 load、initialize 方法的区别什么?在继承关系中他们有什么区别
load | initialize | |
---|---|---|
调用时机 | main 函数之前 | 第一次接收到消息时 |
父类的 load 会先于子类被调用 | 父类的 +initialize 会先于子类被调用 | |
是否需要手动 super | 不需要(看上一条) | 不需要(看上一条) |
加载顺序 | 取决于在 Compile Sources 中的顺序 | 取决于在 Compile Sources 中的顺序 |
继承关系 | 相互独立,也会独立效用 | 相互独立,但后添加的 initialize 会 “覆盖” 先添加的 |
11 说说消息转发机制的优劣
看第 8 个问题
深入一下~
源码来自 objc-866.9
runtime
,运行时,是 Objective-C 的核心。正是由于 runtime
的存在,OC 才能称为一门动态语言。
先来看看 官方说明:
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language.
Objective-C 运行时是一个为 Objective-C 语言提供动态属性的运行时库。
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically.
Objective-C 语言尽可能多地将决策从编译时和链接时推迟到运行时。只要有可能,它都动态地执行操作。
This means that the language requires not just a compiler, but also a runtime system to execute the compiled code.
这意味着 Objective-C 不止需要一个编译器,还需要一个运行时系统来执行编译后的代码。
The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.
这个运行时系统就相当于 Objective 语言的操作系统,这也正是 Objective-C 语言动态的原理。
对于静态语言来说,编译期就能确定每个函数的真正调用地址。而对于动态语言来说,方法调用时一个动态的过程,编译期只能确定方法的接受者 receiver
和消息本身 _cmd
。
在使用 OC 编程的时候,可以给任意对象发消息, receiver
针对这条消息的响应跟处理操作都是在运行时才决定的。
与 runtime
进行交互的方式有三种,就是网上流传甚广的这张图:
- OC 源码(
Objective-C Code
)
只要我们是使用 OC 在写代码,就是在与 runtime 系统交互了。就比如这个:
NSObject *obj = [[NSObject alloc] init];
复制代码
- NSObject 定义方法(
Framework & Service
)
在 OC 的世界里,其实只有两个类:NSObject
与 NSProxy
,两者都实现了 NSObject 协议。前者便是我们日常使用的;后者是是一个虚基类,专门用于实现消息代理,我们可以子类化并实现 forwardInvocation:
与 methodSignatureForSelector:
来处理自己没有实现的消息。
在 NSObject 协议中,声明了所有 OC 对象的公共方法。其中,存在五个可以从 runtime 获取信息的 对象自省方法:
/// 返回当前对象的类
- (Class)class;
/// 是否是特定类(继承链检查)
- (BOOL)isKindOfClass:(Class)aClass;
/// 是否是特定类的实例
- (BOOL)isMemberOfClass:(Class)aClass;
/// 是否符合指定协议
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
/// 是否能响应指定消息
- (BOOL)respondsToSelector:(SEL)aSelector;
复制代码
- 直接调用 API(
Runtime API
)
首先可以在 这里 查看 runtime API 的详细文档。
不过,从 Xcode 5 之后,Apple 不建议我们手动调用 runtime API,所以默认关闭了 runtime API 的代码提示,我们可以在这里进行设置:
默认值为 YES,改为 NO 即可。
1、一切从 NSObject
开始
先看一下定义:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
复制代码
1.1 创建一个 NSObject
我们先来看一下 NSObject
的创建过程,先简单地看一下 alloc
:
对象创建 - alloc
打上断点:
运行程序,进入断点后打开汇编(勾选 “Xcode 菜单中的【Debug > Debug Workflow > Always Show Disassembly】”)
我们到 NSObject.mm
找到 objc_alloc
:
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
复制代码
继续,看看 callAlloc
:
/// 调用时传入 checkNil = true, allocWithZone = false
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
#if __OBJC2__
/// 判断 Class cls 是否存在,不重要
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
/// 消息转发流程,调用 alloc
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
复制代码
slowpath
与fastpath
不用在意,这是为了告诉编译器这个判断逻辑的大概率倾向,当做不存在即可
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
首先,这里判断 !cls->ISA()->hasCustomAWZ())
,我们一个个点进去看一下 hasCustomAWZ()
到底是何方神圣?
bool hasCustomAWZ() const {
return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
bool getBit(uint16_t flags) const {
return _flags & flags;
}
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
///+ 类或其父类存在默认的 alloc/allocWithZone: 实现,存储在元类中
#define FAST_CACHE_HAS_DEFAULT_AWZ (1<<14)
复制代码
翻译一下:类或者父类是否存在默认的 alloc / allocWithZone:
实现。存储元类 metaclass
中(这个先不着急)
先继续往下看
- 如果 默认的
alloc / allocWithZone:
实现 存在:
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
///+ allocWithZone 在 __OBJC2__ 环境下忽略 zone 参数
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
复制代码
这个分支暂时就先看到这里,_class_createInstanceFromZone()
一会儿再说!!!
- 如果 默认的
alloc / allocWithZone:
实现 不存在 :
此时进入下边的分支:
/// 调用时传入 checkNil = true, allocWithZone = false
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
///... 这里此时不重要,因为调用时 allocWithZone 为 false
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
/// 消息转发流程,调用 alloc
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
复制代码
此时应该执行这一句 ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))
,毕竟 allocWithZone = false
。也就相当于 [cls alloc]
:
+ (id)alloc {
return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
复制代码
又到了 callAlloc
, 只不过这次传入的参数变成了 checkNil: false, allocWithZone: true :
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
复制代码
好,那就看看 +[cls allocWithZone:]
:
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
///+ allocWithZone 在 __OBJC2__ 环境下忽略 zone 参数
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
复制代码
有没有一点眼熟???还是 _class_createInstanceFromZone()
!!!梅开二度,那就盘它:
虽然在调用
_objc_rootAllocWithZone()
时的第二个参数一个是nil
,一个是zone
,但是请注意_objc_rootAllocWithZone()
的内部实现中,有这样一句备注// allocWithZone under OBJC2 ignores the zone parameter
在 Objc2 中 zone 是被忽略的,所以两者完全相同,
实现上也是如此,第三个参数传入的 nil
/// cls = cls
/// extraBytes = 0
/// zone = nil
/// construct_flags = OBJECT_CONSTRUCT_CALL_BADALLOC = 2
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
/// 是否存在 C++ 构造器
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
/// 是否存在 C++ 析构器
bool hasCxxDtor = cls->hasCxxDtor();
/// 是否可以快速 alloc()
bool fast = cls->canAllocNonpointer();
size_t size;
/// 获取类实例所占空间
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
/// 申请空间
if (zone) {
/// 这个分支暂时不用看,因为调用时没有传入 zone
/// 其实是用于 copy 方法
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
/// 空间申请失败
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
/// 初始化 ISA
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
/// 根据继承链构造 C++ 成员
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
复制代码
从注释可以看出来流程,重点看一下 初始化 isa,先看一下 initInstanceIsa
:
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
复制代码
也调用到了 initIsa
方法,这个先按下不表,一会儿再说。
看一下 object_cxxConstructFromClass
方法:
id
object_cxxConstructFromClass(id obj, Class cls, int flags)
{
id (*ctor)(id);
Class supercls;
supercls = cls->superclass;
/// 如果父类存在 C++ 构造方法,递归调用
/// 若在某一层构建失败,则放弃构建,alloc 就失败了
if (supercls && supercls->hasCxxCtor()) {
bool ok = object_cxxConstructFromClass(obj, supercls, flags);
if (slowpath(!ok)) return nil; // some superclass's ctor failed - give up
}
// Find this class's ctor, if any.
/// 在当前类的方法列表中查找 C++ 构造方法
/// 只在当前类的方法列表中查找,若找不到,则缓存并返回 objc_*msgForward(* _objc_*msgForward_impcache)*
ctor = (id(*)(id))lookupMethodInClassAndLoadCache(cls, SEL_cxx_construct);
if (ctor == (id(*)(id))_objc_msgForward_impcache) return obj; // no ctor - ok
/// 执行找到的 C++ 构建方法
if (fastpath((*ctor)(obj))) return obj; // ctor called and succeeded - ok
supercls = cls->superclass; // this reload avoids a spill on the stack
/// C++ 构建失败了,执行清理操作
// This class's ctor was called and failed.
// Call superclasses's dtors to clean up.
if (supercls) object_cxxDestructFromClass(obj, supercls);
if (flags & OBJECT_CONSTRUCT_FREE_ONFAILURE) free(obj);
if (flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
复制代码
到这里,alloc 就结束了。(原本的英文注释留着,可以对照着看。。。)
对象创建 - init
我们直接打开 NSObject.mm
,找到 -init
方法:
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
return obj;
}
复制代码
就这么点,完了!!! init
啥也没做。也即是说,默认情况下,init
啥也没做。那为何还要保留 init
这个操作呢?我个人针对这有两个理解:
- 继承。一个类在初始化的时候,必须要考虑父类在创建时候的操作,所以我们需要在
init
方法里调用[super init]
。 - 自定义。我们可以在
init
方法内来添加一些额外的操作,以达到自定义的目的。
对象创建 - new
除了 [[NSObject alloc] init]
。我们也可以使用 [NSObject new]
来创建一个实例。来看看源码:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
复制代码
因为 new
是个关键字,所以没办法 Cmd + click。想看的话再 NSObject.mm
中查找 + (id)new {
。
一看就明白了: alloc + init。
总结一下创建对象流程(抄来的图):
这个图结合源码基本就很明白了。
1.2 isa
- (Class)class
这个方法应该是众所周知的,其返回当前对象的具体类型。我们来看看实现:
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA(/*authenticated*/true);
/// 剩下的部分代码是从 TaggedPointer 获取到这个指针的类型
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
/// 去地址后 4 位和 0xf 做 & 操作得到类型标记
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
/// 从所有 tag 类型中查找类型
cls = objc_tag_classes[slot];
/// 如果没找到,则通过相同的操作在 ext tag 类型查找
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
复制代码
看一下 isa_t
的结构:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
Class cls;
public:
struct {
ISA_BITFIELD; // defined in isa.h
};
};
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
/// 0 代表普通的 isa 指针,存储 Class、MetaClass
/// 1 代表优化过的指针,可存储更多信息:如引用计数
/// 64 位机型下均是 1
uintptr_t nonpointer : 1; \
/// 是否存在关联对象,若没有可以更快释放
uintptr_t has_assoc : 1; \
/// 是否存在 C++ 析构器,若没有可以更快释放
uintptr_t has_cxx_dtor : 1; \
/// 存储类型
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
/// 在 DEBUG 环境下是否完成初始化
uintptr_t magic : 6; \
/// 是否被弱引用过,若没有可以更快释放
uintptr_t weakly_referenced : 1; \
/// 是否正在释放
uintptr_t deallocating : 1; \
/// 是否存在额外的引用计数记录
/// 当引用记录过大无法存储在 isa.extra_rc 时,引用计数则会存储在一个 sidetable 中
uintptr_t has_sidetable_rc : 1; \
/// 额外的引用计数值
uintptr_t extra_rc : 19
# define ISA_HAS_INLINE_RC 1
# define RC_HAS_SIDETABLE_BIT 44
# define RC_ONE_BIT (RC_HAS_SIDETABLE_BIT+1)
# define RC_ONE (1ULL<<RC_ONE_BIT)
# define RC_HALF (1ULL<<18)
复制代码
先来验证下以上注释,这里使用真机验证(模拟器与真机需要计算对应位时偏移不同),先编写一段可以将 isa 信息拆出来的代码:
struct isa_struct {
uintptr_t isa;
};
uintptr_t p = ((__bridge struct isa_struct *)obj)->isa;
/// iOS 使用小端模式
uintptr_t nonpointer = p & 0x1;
uintptr_t has_assoc = p & 0x2;
uintptr_t has_cxx_dtor = p & 0x4;
// 该值输出没有意义,稍后使用 Xcode 调试命令查看
// shiftcls = p & 0x0000000ffffffff8UL; // ISA_MASK
uintptr_t magic = (p & 0x000003f000000001UL) >> 39;
uintptr_t weakly_referenced = (p & 0x0000040000000001UL) >> 42;
uintptr_t deallocating = (p & 0x0000080000000001UL) >> 43;
uintptr_t has_sidetable_rc = (p & 0x0000100000000001UL) >> 44;
uintptr_t extra_rc = (p & 0xffff800000000001UL) >> 45;
复制代码
如图所示,第一行 log 为初始化状态:
- 红框:添加关联对象后,
has_assoc
位由 0 变为 1 - 篮框:添加弱引用后,
weakly_referenced
位由 0 变为 1 - 绿框:在添加强引用后,
extra_rc
的值会增加对应的数字 - 黄框:在
extra_rc
达到存储最大值(524287,无符号 19 位最大值)后,再增加引用计数时,runtime 会将其一半的引用计数存储至散列表SideTable
中,具体可查看 objc-object.h 中的rootRetain
方法 - 红色箭头:po 输出计算得到的
shiftcls
与obj.class
一致
关于 shiftcls 也可以采用位移的方式将除 shiftcls 的其他位归零再使用 Xcode 调试命令输出即可:
/// 移除 nonpointer, has_assoc, has_cxx_dtor
pointer >> 3 << 3;
/// 移除 magic, weakly_refrenced, deallocating, has_sidetable_rc, _extra_rc
pointer << (19 + 1 + 1 + 1 + 6) >> (19 + 1 + 1 + 1 + 6)
复制代码
如图:
如果还想要验证
has_cxx_dtor
,只需给这个类添加一个 C++ 类型的成员变量即可,例如 string
现在来阅读一下在创建 NSObject 没说的 initIsa
:
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
/// 其实也就以前的 newisa.shiftcls = (uintptr_t)cls >> 3;
newisa.setClass(cls, this);
newisa.extra_rc = 1;
}
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
// 这里返回的是 isa.isa_storage,也就相当于直接赋值
isa() = newisa;
}
复制代码
1.3 retain
& release
既然说到 isa.extra_rc
中存储这部分引用计数,那么就来看一下引用计数到底是变动的
retatin
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
/// 获取 isa
oldisa = LoadExclusive(&isa().bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_retain()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClassfalse)->hasCustomRR())) {
ClearExclusive(&isa().bits);
if (oldisa.getDecodedClassfalse)->canCallSwiftRR()) {
return swiftRetain.load(memory_order_relaxed)((id)this);
}
return ((i(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
}
/// 未优化的 isa,使用 sidetable_retain()
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa().bits);
return (id)this;
}
}
do {
transcribeToSideTable = false;
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa().bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// don't check newisa.fast_rr; we already called any RR overrides
///Junes 正在析构...
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa().bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil
} else {
return (id)this;
}
}
uintptr_t carry;
/// 引用计数 extra_rc++
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
/// 溢出
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (variant != RRVariant::Full) {
ClearExclusive(&isa().bits);
/// 重新调用该函数,第二个参数 handleOverflow 为 true
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
/// 保留一半引用计数
/// 准备将另一半复制到 side table 中
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa().bits, &oldisa.bits, newisa.bits)));
if (variant == RRVariant::Full) {
/// 将另一半引用计数复制到 side table
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
} else { }
return (id)this;
}
复制代码
简单总结一下:
- 判断是否为 tagged pointer,是则直接 return;
- 判断该对象的 isa 是否优化过,若未优化,则使用
sidetable_retain
存储引用计数; - 若正在释放,则不进行 retain 操作;
- 引用计数 +1;
- 若引用计数 +1 后溢出了(extra_rc 仅能存储 524287),则将一半的引用计数存储到 sidetable 中。
当然,在 retain 过程中,需要对 sidetable 进行枷锁操作。
release
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
/// 如果是 tagged pointer 直接 return
if (slowpath(isTaggedPointer())) return false;
bool sideTableLocked = false;
isa_t newisa, oldisa;
oldisa = LoadExclusive(&isa().bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_release()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa().bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
swiftRelease.load(memory_order_relaxed)((id)this);
return true;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
return true;
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa().bits);
return false;
}
}
retry:
do {
newisa = oldisa;
/// 未优化的 isa
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa().bits);
return sidetable_release(sideTableLocked, performDealloc);
}
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa().bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
return false;
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
/// 引用计数 extra_rc--
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
/// 是否下溢出,溢出则意味着存储在 isa.extra_rc 本来就已经为 0 了,需要从 sidetable 中拿到引用计数再做 -1 操作
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits))); /// 更新 isa
if (slowpath(newisa.isDeallocating()))
goto deallocate;
if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else { }
return false;
/// 下溢出了,意味着需要减少该对象在 sidetable 中的引用计数
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
if (variant != RRVariant::Full) {
/// 有借位保存,重新进入该函数并传入 performDealloc
ClearExclusive(&isa().bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
ClearExclusive(&isa().bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
oldisa = LoadExclusive(&isa().bits);
goto retry;
}
// Try to remove some retain counts from the side table.
/// 从 sidetable 中获取借位引用计数,获取最大值为 RC_HALF
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
if (borrow.borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
bool didTransitionToDeallocating = false;
/// 已从 sidetable 中获取到引用计数,做 -1 操作
newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too
newisa.has_sidetable_rc = !emptySideTable;
bool stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
if (!stored && oldisa.nonpointer) {
/// 保存失败,再试一次
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
uintptr_t overflow;
newisa.bits =
addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
newisa.has_sidetable_rc = !emptySideTable;
if (!overflow) {
stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
if (stored) {
didTransitionToDeallocating = newisa.isDeallocating();
}
}
}
if (!stored) {
/// 再试一次还没成功,则将引用计数放入 sidetable 中再试一次
// Inline update failed.
// Put the retains back in the side table.
ClearExclusive(&isa().bits);
sidetable_addExtraRC_nolock(borrow.borrowed);
oldisa = LoadExclusive(&isa().bits);
goto retry;
}
// Decrement successful after borrowing from side table.
if (emptySideTable)
sidetable_clearExtraRC_nolock();
if (!didTransitionToDeallocating) {
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
}
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
/// 没有借位保存的情况
deallocate:
// Really deallocate.
ASSERT(newisa.isDeallocating());
ASSERT(isa().isDeallocating());
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
/// 调用 dealloc 方法
if (performDealloc) {
this->performDealloc();
}
return true;
}
复制代码
简单总结一下:
- 判断是否为 tagged pointer,是则直接 return;
- 判断该对象的 isa 是否优化过,若未优化,则直接调用
sidetable_release
进行操作然后 return; - 尝试将
isa.extra_rc
-1,若未下溢出则执行第 5 步; - 若下溢出了,从 sidetable 中取出 RC_HALF 并将该值 -1 存储到
isa.extra_rc
中(期间有重试流程); - 执行 dealloc 操作 。
dealloc
反正都看到这里,就再看一下对象时如何释放的:
- (void)dealloc {
_objc_rootDealloc(self);
}
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
/// 若可以快速释放,则直接释放
if (fastpath(isa().nonpointer &&
!isa().weakly_referenced &&
!isa().has_assoc &&
!isa().has_cxx_dtor &&
!isa().has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
/// 销毁该实例对象
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
/// 顺序很重要
/// 根据继承链销毁 C++ 成员变量
if (cxx) object_cxxDestruct(obj);
/// 移除关联对象
if (assoc) _object_remove_associations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
inline void
objc_object::clearDeallocating()
{
/// 清理所有 weak 引用
/// 清空 sidetable 中的自己
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
}
复制代码
总结起来就是:
- 根据继承链销毁 C++ 成员变量,执行其析构方法;
- 移除关联对象;
- 清空自身的 weak 引用(全部置为 nil);
- 清理 sidetable 中的自己,
- 调用 free 释放空间。
2. 类是如何被加载的
类要加载的话,那么必须得有类加载的环境,我们先看一下环境是如何准备的
2.1 _objc_init
先来看一下 objc-os.mm 中的这段代码:
/*
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
*/
void _objc_initvoid)
{
/// 仅初始化一次
static bool initialized = false;
if (initialized) return;
initialized = true;
/// 读取会影响 runtime 的环境变量。如有需要,也可打印出来
environ_init();
/// 执行 C++ 静态构造方法
static_init();
/// runtime 初始化
runtime_init();
/// libobjc 异常处理系统初始化
exception_init();
/// 缓存初始化
cache_t::init();
/// 回调机制初始化,通常不做任何事情,毕竟所有事情都是懒加载的。但对于某些特定进程,需要尽早加载回调机制。
_imp_implementationWithBlock_init();
_dyld_objc_callbacks_v1 callbacks = {
1, // version
&map_images, /// dyld 映射镜像完成时回调:加载由 dyld 映射提供的镜像
load_images, /// dyld 镜像加载完成时回调:加载所有 category,执行 +load 方法,包括类与 category
unmap_image, /// dyld 镜像接触映射完成时回调:反映射
_objc_patch_root_of_class /// 补全 dyld 给定的类:包括 initIsa, setSuperclass, cache.initializeToEmpty
};
/// 注册刚构建的回调
_dyld_objc_register_callbacks((_dyld_objc_callbacks*)&callbacks);
didCallDyldNotifyRegister = true;
}
复制代码
先看一眼函数注释:引导程序初始化,通过 dyld 注册镜像通知器。由 libSystem 在库初始化之前调用。
environ_init
源码就不贴了,不过可以看一下到底有哪些可以影响 runtime 的环境变量,在 target 的环境变量中加入 OBJC_HELP
再运行即可:
其中第三行与第五行输出 “XXX is set” 是因为我在环境变量中添加了 OBJC_HELP
与 OBJC_PRINT_OPTION
。
static_init
static void static_init()
{
size_t count1
///+ 从 __**DATA,** __DATA_CONST, __**DATA_DIRTY 获取所有以** __objc_init_func 开头的方法,并执行
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count1);
for (size_t i = 0; i < count1; i++) {
inits[i]();
}
size_t count2;
///+ 从 __**TEXT 区获取所有以** __objc_init_func 开头的方法,并执行
auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count2);
for (size_t i = 0; i < count2; i++) {
UnsignedInitializer init(offsets[i]);
init();
}
}
复制代码
函数注释:libc 在 dyld 调用我们的静态构造函数之前会先调用 _objc_init
,所以我们不得不自己来做这个调用操作。
getLibobjcInitializers
其实是由宏定义来的:
#define GETSECT(name, type, sectname)
type *name(const headerType *mhdr, size_t *outCount) {
return getDataSection<type>(mhdr, sectname, nil, outCount);
}
type *name(const header_info *hi, size_t *outCount) {
return getDataSection<type>(hi->mhdr(), sectname, nil, outCount);
}
GETSECT(getLibobjcInitializers, UnsignedInitializer, "__objc_init_func");
复制代码
所以展开一下:
UnsignedInitializer *getLibobjcInitializers(const headerType *mhdr, size_t *outCount) {
return getDataSection<type>(mhdr, "__objc_init_func", nil, outCount);
}
UnsignedInitializer *getLibobjcInitializers(const header_info *hi, size_t *outCount)
{
return getDataSection<type>(hi->mhdr(), "__objc_init_func", nil, outCount);
}
复制代码
而 getDataSection
长这样:
template <typename T>
T* getDataSection(const headerType *mhdr, const char *sectname,
size_t *outBytes, size_t *outCount)
{
unsigned long byteCount = 0;
T* data = (T*)getsectiondata(mhdr, "__DATA", sectname, &byteCount);
if (!data) {
data = (T*)getsectiondata(mhdr, "__DATA_CONST", sectname, &byteCount);
}
if (!data) {
data = (T*)getsectiondata(mhdr, "__DATA_DIRTY", sectname, &byteCount);
}
if (outBytes) *outBytes = byteCount;
if (outCount) *outCount = byteCount / sizeof(T);
return data;
}
复制代码
至于 UnsignedInitializer
,长这样:
struct UnsignedInitializer {
private:
uintptr_t storage;
public:
UnsignedInitializer(uint32_t offset) {
storage = (uintptr_t)&_mh_dylib_header + offset;
}
void operator () () const {
using Initializer = void(*)();
Initializer init =
ptrauth_sign_unauthenticated((Initializer)storage,
ptrauth_key_function_pointer, 0);
init();
}
};
复制代码
将以上几段代码串联起来,static_init
中第一个循环其实就是尝试先后从 __DATA、__DATA_CONST、__DATA_DIRTY 区获取以 __objc_init_func
字符串开头的方法,并执行。而第二个循环则是从 _TEXT 区获取以 __objc_init_func
字符串开头的方法,并执行。
有关于 _DATA 等区的概念搜索 Mach-O 文件,也可直接至 Mach-O 文件格式探索 查看
runtime_init
这里其实比较简单,它仅仅只是构造了 unattachedCategories
、allocatedClasses
两张表以便后续加载类使用。
void runtime_init(void)
{
/// disable class_rx_t pointer signing enforcement
/// 禁用 class_rx_t 指针签名的执行
objc::disableEnforceClassRXPtrAuth = DisableClassRXSigningEnforcement;
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
复制代码
exception_init
初始化异常处理系统,由 map_images 调用。
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
if (PrintExceptions) {
_objc_inform("EXCEPTIONS: terminating");
}
if (! __cxa_current_exception_type()) {
// No current exception.
/// 未发生异常,继续监测
(*old_terminate)();
}
else {
// There is a current exception. Check if it's an objc exception.
/// 发生了异常,检查一下是否是 objc 异常
@try {
/// 尝试抛出并继续运行
__cxa_rethrow();
} @catch (id e) {
// It's an objc object. Call Foundation's handler, if any.
/// 当前异常是 objc 对象,尝试执行 Foundation 处理程序
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
// It's not an objc object. Continue to C++ terminate.
/// 当前异常不是 objc 对象,执行 C++ 异常处理
(*old_terminate)();
}
}
}
复制代码
其流程是:
- 持续检查是否存在异常;
- 发生异常时,先尝试抛出异常并尝试运行,
- 若该异常未被处理,检查异常是否由 objc 对象产生;
- 如果当前异常是由 objc 对象产生,则利用该对象调用已注册的异常回调
uncaught_handler
; - 如果当前异常不是有 objc 对象产生,调用原本的终止程序
再来查看一下 uncaught_handler
:
static void _objc_default_uncaught_exception_handler(id exception)
{
}
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
///设置 异常回调
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn;
return result;
}
复制代码
可以看到,默认的 uncaught_handler
为空实现,开发者可通过 objc_setUncaughtExceptionHandler
自行设置异常处理程序。可如下操作:
#import <objc/objc-exception.h>
void handleException(id exception) {
NSLog(@"发生异常:%@", exception);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
objc_setUncaughtExceptionHandler(handleException);
SomeObject *obj = [SomeObject new];
/// 这是一个不存在方法,会抛出异常
[obj performSelector:@selector(test)];
}
return 0;
}
复制代码
运行一下:
上边的 _objc_terminate
中在监测到异常时,会抛出异常交于开发者处理,那么我们只要捕获到这个异常并处理掉就可以继续运行了,所以我们将产生异常的地方修改一下:
@try {
/// 这是一个不存在方法,会抛出异常
[obj performSelector:@selector(test)];
} @catch (id e) {
NSLog(@"捕获到了异常");
}
复制代码
再运行一下,看看结果:
cache_t::init
void cache_t::init()
...
...
mach_msg_type_number_t count = 0;
kern_return_t kr;
/// 通过统计已注册的 task_restartable_range_t 计算得到需要新建用到的下标
while (objc_restartableRanges[count].location) {
count++;
}
/// 在系统当前的 task(其实就是这个进程)注册这个新的 task_restartable_range_t
/// 这里就牵扯到苹果系系统的进程资源分配,有兴趣的自行查阅
kr = task_restartable_ranges_register(mach_task_self(),
objc_restartableRanges, count);
}
复制代码
这个 objc_restartableRanes 一看就是个数组,其内部到底是什么呢?
extern "C" task_restartable_range_t objc_restartableRanges[];
/// 用户空间中可回收使用的一个区域 section
typedef struct {
/// 在系统的起始地址
mach_vm_address_t location;
/// 这个区域的总长度
unsigned short length;
/// 基于起始位置已使用的偏移
unsigned short recovery_offs;
/// 当前并未使用的字段
unsigned int flags;
} task_restartable_range_t;
复制代码
2.2 _dyld_objc_register_callbacks
这句非常重要:通过 dyld 注册一些回调(其源码在 dyld 中):
- map_images
- load_images
- unmap_image
- _objc_patch_root_of_class
一个一个来看都干了些什么?
map_images
dyld 完成镜像映射时回调该方法,在这里加载由 dyld 映射提供的镜像。
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
bool takeEnforcementDisableFault;
{
/// 通过作用于加锁解锁
mutex_locker_t lock(runtimeLock);
/// 加载由 dyld 映射完成之后提供的镜像
map_images_nolock(count, paths, mhdrs, &takeEnforcementDisableFault);
}
...
...
}
复制代码
可以发现,其主要逻辑均在 map_images_nolock
中,阅读一下:
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[],
bool *disabledClassROEnforcement)
{
static bool firstTime = YES;
static bool executableHasClassROSigning = false;
static bool executableIsARM64e = false;
header_info *hList[mhCount];
uint32_t hCount;
size_t selrefCount = 0;
*disabledClassROEnforcement = false;
// Perform first-time initialization if necessary.
// This function is called before ordinary library initializers.
// fixme defer initialization until an objc-using image is found?
///+ 第一次初始化时调用
///+ 在二进制库初始化之前调用
///+ 决定是否禁用共享缓存优化
if (firstTime) {
preopt_init();
}
// Find all images with Objective-C metadata.
///+ 通过 OC 元数据查找所有的镜像
hCount = 0;
// Count classes. Size various table based on the total.
///+ 所有类的数目
int totalClasses = 0;
///+ 未优化类的数目
int unoptimizedTotalClasses = 0;
{
uint32_t i = mhCount;
///+ 遍历所有的 mach_header
while (i--) {
const headerType *mhdr = (const headerType *)mhdrs[i];
///+ 从这个 mach_header 中获取 header_info 并添加到链表尾部
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
// no objc data in this entry
continue;
}
if (mhdr->filetype == MH_EXECUTE) {
///+ 这个 mach_header 的文件类型为可执行文件
// Size some data structures based on main executable's size
///+ 根据主可执行文件调整一些数据结构的 size
// If dyld3 optimized the main executable, then there shouldn't
// be any selrefs needed in the dynamic map so we can just init
// to a 0 sized map
///+ 如果 dyld3 优化了这个主可执行文件,那么在这个动态映射中不需要任何的 选择器引用
///+ 因此我们可以只初始化一个 size 为 0 的映射。
if ( !hi->hasPreoptimizedSelectors() ) {
size_t count;
///+ 从 “__objc_selrefs” 区中获取 selector 引用**
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
///+ 从 “__objc_msgrefs” 区中获取 消息引用**
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
}
#if SUPPORT_GC_COMPAT
// Halt if this is a GC app.
///+ 如果这是一个 GC app,终止
if (shouldRejectGCApp(hi)) {
_objc_fatal_with_reason(...);
}
#endif
//+ 检测该 header_info 是否存在经过签名的 class_ro_t 指针
if (hasSignedClassROPointers(hi)) {
executableHasClassROSigning = true;
}
}
///+ 将该 header_info 放入数组中
hList[hCount++] = hi;
}
}
// Perform one-time runtime initialization that must be deferred until
// the executable itself is found. This needs to be done before
// further initialization.
// (The executable may not be present in this infoList if the
// executable does not contain Objective-C code but Objective-C
// is dynamically loaded later.
///+ 执行所有必须延后执行的 runtime 初始化流程,直到找到可执行文件。这部分操作需要再未来的初始化流程之前完成
///+(如果可执行文件不包含 OC 代码但 OC 在之后动态加载,那么这个可执行文件可能不会表现在这个 infoList 中)
if (firstTime) {
///+ 初始化 selector 表,注册内部(libobjc)使用的 selector
sel_init(selrefCount);
///+ 初始化 sideTableMap,并初始化关联对象管理器 AssociationsManager,并在开启 DebugScanWeakTables 的情况下开始扫描所引用表扫描
arr_init();
#if SUPPORT_GC_COMPAT
// Reject any GC images linked to the main executable.
// We already rejected the app itself above.
// Images loaded after launch will be rejected by dyld.
///+ 拒绝任何链接到主可执行程序的 GC 镜像
///+ 刚才已经拒绝了所有的 GC app
///+ 在启动后才加载的镜像将被 dyld 拒绝
for (uint32_t i = 0; i < hCount; i++) {
auto hi = hList[i];
auto mh = hi->mhdr();
if (mh->filetype != MH_EXECUTE && shouldRejectGCImage(mh)) {
_objc_fatal_with_reason(...);
}
}
}
#endif
// If the main executable is ARM64e, make sure every image that is loaded
// has pointer signing turned on.
///+ 如果主可执行文件是 ARM64e,确保每个加载的镜像都打开了指针签名
if (executableIsARM64e) {
bool shouldWarn = (executableHasClassROSigning
&& DebugClassRXSigning);
for (uint32_t i = 0; i < hCount; ++i) {
auto hi = hList[i];
if (!hasSignedClassROPointers(hi)) {
if (!objc::disableEnforceClassRXPtrAuth) {
*disabledClassROEnforcement = true;
objc::disableEnforceClassRXPtrAuth = 1;
}
}
}
}
if (hCount > 0) {
///+ 读取镜像
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
// Call image load funcs after everything is set up.
///+ 在所有东西都设置完成呢之后调用镜像的 load 方法
for (auto func : loadImageFuncs) {
for (uint32_t i = 0; i < mhCount; i++) {
func(mhdrs[i]);
}
}
复制代码
总结一下流程:
- 如果是第一次进入,根据需要决定是否禁用共享缓存优化
- 从所有 mach_header 中获取所有的 header_info 并添加到链表中
- 如果该 mach_header 的文件类型为可执行文件且未被 dyld3 优化过,则读取 selector 引用与 message 引用
- 如果是第一次进入,
- 初始化 selector 表,注册内部(libobjc)使用的 selector
- 初始化 sideTableMap
- 初始化关联对象管理器 AssociationsManager
- 开启 DebugScanWeakTables 的情况下开启所引用表扫描
- 拒绝任何想要链接到主可执行程序的 GC 镜像
- 读取上边获取到的所有 header_info
- 执行镜像的 +load 方法
接下来看看 map_images
内部比较重要的几个子流程
preopt_init
void preopt_init(void)
{
// Get the memory region occupied by the shared cache.
///+ 获取共享缓存占用的内存区域(start 与 length)
size_t length;
const uintptr_t start = (uintptr_t)_dyld_get_shared_cache_range(&length);
///+ 将刚获取到的贡献个缓存区域设置给 dataSegmentsRanges.sharedCacheRange
if (start) {
objc::dataSegmentsRanges.setSharedCacheRange(start, start + length);
}
///+ “设置了 OBJC_DISABLE_PREOPTIMIZATION” 或 “表缺失”,则标记为失败
if (DisablePreopt) {
// OBJC_DISABLE_PREOPTIMIZATION is set
// If opt->version != VERSION then you continue at your own risk.
failure = "(by OBJC_DISABLE_PREOPTIMIZATION)";
}
else if (opt->version != 16) {
// This shouldn't happen. You probably forgot to edit objc-sel-table.s.
// If dyld really did write the wrong optimization version,
// then we must halt because we don't know what bits dyld twiddled.
///+ 这是不应该发生的。若发生了则直接终止
_objc_fatal(...);
}
else if (!opt->headeropt_ro()) {
// One of the tables is missing.
///+ 表缺失
failure = "(dyld shared cache is absent or out of date)";
}
///+ 根据 failure 标记决定是否禁用共享缓存优化
if (failure) {
// All preoptimized selector references are invalid.
///+ 所有预优化的 selector 引用均无效
preoptimized = NO;
opt = nil;
///+ 禁用共享缓存优化
disableSharedCacheOptimizations();
}
else {
// Valid optimization data written by dyld shared cache
///+ dyld 共享缓存写入的优化有效
preoptimized = YES;
}
}
复制代码
所以,preopt_init
用于决定是否禁用共享缓存优化。
addHeader
///+ 从 mach_header 中获取 header_info
static header_info * addHeader(const headerType *mhdr, const char *path, int &totalClasses, int &unoptimizedTotalClasses)
{
header_info *hi;
if (bad_magic(mhdr)) return NULL;
bool inSharedCache = false;
// Look for hinfo from the dyld shared cache.
///+ 从 dyld 共享缓存中查找 header_info
hi = preoptimizedHinfoForHeader(mhdr);
if (hi) {
// Found an hinfo in the dyld shared cache.
///+ 找到了目标 header_info
// Weed out duplicates.
///+ 过滤掉已添加到链表中的
if (hi->isLoaded()) {
return NULL;
}
inSharedCache = true;
// Initialize fields not set by the shared cache
// hi->next is set by appendHeader
///+ 将当前 header_info.loaded 标记为 true
hi->setLoaded(true);
///+ DEBUG 模式下会验证 image_info
}
else
{
// Didn't find an hinfo in the dyld shared cache.
///+ 未找到 header_info,封装 header_info
// Locate the __OBJC segment
///+ 通过 "__**objc_imageinfo" 定位** __OJBC 段
size_t info_size = 0;
unsigned long seg_size;
const objc_image_info *image_info = _getObjcImageInfo(mhdr,&info_size);
const uint8_t *objc_segment = getsegmentdata(mhdr,SEG_OBJC,&seg_size);
if (!objc_segment && !image_info) return NULL;
// Allocate a header_info entry.
// Note we also allocate space for a single header_info_rw in the
// rw_data[] inside header_info.
///+ 开辟 header_info 入口
///+ 此外,还在 header_info 内部的 header_info_rw[] 中开辟了一个 header_info_rw 的空间
hi = (header_info *)calloc(sizeof(header_info) + sizeof(header_info_rw), 1);
// Set up the new header_info entry.
///+ 设置这个新的 header_info 入口
hi->setmhdr(mhdr);
// Install a placeholder image_info if absent to simplify code elsewhere
///+ 如果没有,设置 image_info 占位符 {0, 0} 以简化其余代码
static const objc_image_info emptyInfo = {0, 0};
hi->setinfo(image_info ?: &emptyInfo);
///+ 将当前 header_info.loaded 标记为 true
hi->setLoaded(true);
///+ 将当前 header_info.allClassesRealized 标记为 false
hi->setAllClassesRealized(NO);
}
{
size_t count = 0;
///+ 通过 “__**objc_classlist” 在这个 header_info 中获取类列表**
if (_getObjc2ClassList(hi, &count)) {
totalClasses += (int)count;
if (!inSharedCache) unoptimizedTotalClasses += count;
}
}
///+ 将这个 header_info 添加到链表尾部
///+ 并将该 header_info 添加至数据段区域数字 dataSegmentsRanges
appendHeader(hi);
return hi;
}
复制代码
所以 addHeader
就是读取指定 mach_header 并将信息封装为 header_info
然后添加到链表中。
什么是 mach_header
呢?
struct mach_header_64 {
///+ 标志符
uint32_t magic; /* mach magic number identifier */
///+ cpu 类型标志符
int32_t cputype; /* cpu specifier */
///+ cpu 类型标志符
int32_t cpusubtype; /* machine specifier */
///+ 文件类型,Mach-O 支持多种文件格式
uint32_t filetype; /* type of file */
///+ 加载命令的树木
uint32_t ncmds; /* number of load commands */
///+ 所有加载命令的总大小
uint32_t sizeofcmds; /* the size of all the load commands */
///+ dyld 的标志
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
复制代码
更多信息可至 Mach-O 文件格式探索
而 header_info
是什么呢?其实也没必要太究根究底,知道它
typedef struct header_info {
private:
intptr_t mhdr_offset;
///+ mhdr 偏移
intptr_t mhdr_offset;
///+ info 偏移
intptr_t info_offset;
intptr_t info_offset;
// Offset from this location to the non-lazy class list
///+ 非懒加载类列表偏移
intptr_t nlclslist_offset;
uintptr_t nlclslist_count;
// Offset from this location to the non-lazy category list
///+ 非懒加载 category 列表偏移
intptr_t nlcatlist_offset;
uintptr_t nlcatlist_count;
// Offset from this location to the category list
///+ category 列表偏移
intptr_t catlist_offset;
uintptr_t catlist_count;
// Offset from this location to the category list 2
///+ 第二个 category 列表偏移
intptr_t catlist2_offset;
uintptr_t catlist2_count;
...
当然还有一些通过这些变量设置或获取信息的函数
...
private:
header_info_rw rw_data[];
} header_info;
typedef struct header_info_rw {
...
一些方法
...
private:
#ifdef __LP64__
uintptr_t isLoaded : 1;
uintptr_t allClassesRealized : 1;
uintptr_t next : 62;
#else
///+ 是否已加载
uintptr_t isLoaded : 1;
///+ 类是否已识别
uintptr_t allClassesRealized : 1;
///+ 下一个 node
uintptr_t next : 30;
#endif
} header_info_rw;
复制代码
sel_init
void sel_init(size_t selrefCount)
{
///+ 根据给定数目初始化无需集合
namedSelectors.init((unsigned)selrefCount);
// Register selectors used by libobjc
///+ 注册内部(libobjc)使用的 selector
mutex_locker_t lock(selLock);
///+ 注册 C++ 构造与析构方法
SEL_cxx_construct = sel_registerNameNoLock(".cxx_construct", NO);
SEL_cxx_destruct = sel_registerNameNoLock(".cxx_destruct", NO);
}
SEL sel_registerNameNoLock(const char *name, bool copy) {
return __sel_registerName(name, 0, copy); // NO lock, maybe copy
}
static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
{
SEL result = 0;
if (shouldLock) lockdebug::assert_unlocked(&selLock);
else lockdebug::assert_locked(&selLock);
if (!name) return (SEL)0;
result = search_builtins(name);
if (result) return result;
conditional_mutex_locker_t lock(selLock, shouldLock);
///+ 通过 name 获取。格式为 <address, name>
auto it = namedSelectors.get().insert(name);
if (it.second) {
// No match. Insert.
///+ 若不存在,分配空间再插入
*it.first = (const char *)sel_alloc(name, copy);
}
return (SEL)*it.first;
}
复制代码
- 根据给定数目初始化无序 selectors 集合
- 注册内部(libobjc)使用的方法
- 注册 C++ 构造方法
- 注册 C++ 析构方法
arr_init
void arr_init(void)
{
///+ 初始化散列表 sideTableMap
SideTablesMap.init();
///+ 初始化关联对象管理器 AssociationsManager
_objc_associations_init();
///+ 创建专用线程,开启弱引用表扫描
if (DebugScanWeakTables)
startWeakTableScan();
}
复制代码
_read_images
void _read_images(header_info **hList, uint32_t hCount, int totalCl__**objc_selrefs** asses, int unoptimizedTotalClasses)
加载镜像。该方法 400 多行,所以分开查看
第一部分:doneOnce
这部分代码只有第一次进入时会执行:
static bool doneOnce;
if (!doneOnce) {
doneOnce = YES;
///+ 根据需要禁用 tagged pointer
if (DisableTaggedPointers) {
disableTaggedPointers();
}
///+ 根据生成随机数,初始化 tagged pointer 混淆器
initializeTaggedPointerObfuscator();
///+ 根据类总数扩容
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
///+ 创建存储类的哈希表
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
}
复制代码
第二部分:修正 Sel
// Note this has to be before anyone uses a method list, as relative method
// lists point to selRefs, and assume they are already fixed up (uniqued).
///+ 在任何地方使用方法列表之前,因为方法列表指向 selector 引用
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
///+ 若该 header_info 中的方法已预优化,略过
if (hi->hasPreoptimizedSelectors()) continue;
bool isBundle = hi->isBundle();
///+ 从 “__objc_selrefs” 区获取 selector 引用
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
///+ 获取方法地址
const char *name = sel_cname(sels[i]);
///+ 注册该方法
SEL sel = sel_registerNameNoLock(name, isBundle);
///+ 修正 selector 指向:不同类可能存在相同的方法,他们的指向是不同的
if (sels[i] != sel) {
sels[i] = sel;
}
}
}
}
复制代码
方法名是可以为所欲为的,但是在程序执行时可不能为所欲为。不同的类可能会有相同名字的方法,他们指向本该不同,因此需要修复。