阿里、字节:一套高效的iOS面试题(一 - runtime 结构模型 - 上)

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_imagesdidInitialAttachCategories 设置为 true 时加载
    • 若目标类已实现,则直接附加到目标类中
    • 若目标类未实现,则添加至 unattachedCategories 中
  • category 的 load 方法在所有非延时类实现之后,如果目标类还未实现则实现
  • 不同的 category 的 load 方法加载顺序取决于其在 Compile Sources 中的顺序
  • category 中的方法是添加在方法列表的头部的。所以,如果目标类是同一个,后加载的 category 中的方法会“覆盖” 先加载的同名方法的(不止能“覆盖” category 中的同名方法,也能“覆盖”类本身的同名方法)

6 category & extension 区别,能给 NSObject添加 Extension 吗,结果如何

  • category 可以在任意地方,其添加的属性是没有实例变量的
  • extension 是隐藏在类本身 .m 文件中的。人如其名,是扩展,其添加的属性是有成员变量的

对于系统类来说,我们是拿不到 .m 文件的,所以给系统类添加的 extension 相当于 category,没有意义

7 消息转发机制,消息转发机制和其他语言的消息机制优劣对比

  1. objc_msgSend
    • 方法调用
  2. receiver 的 NilOrTaggedTest
    • 接受者的 nil tagged 监测
  3. CacheLookUp
    • 缓存查找
  4. MethodTableLookUp
    • 方法列表查找
  5. lookUpImpOrForward
    • 查找方法的 IMP
  6. resolveMethod:
    • 动态派发
  7. forwardingTargetForSelector
    • 消息转发
  8. forwardInvocation
    • 消息转发
  • 优点
    • 灵活:编译期间并不确定真正的函数调用地址,只是把方法调用重写为 objc_msgSend。在运行期间才会通过上述操作确定函数地址
  • 缺点
    • 效率略低:需要查找,需要更多的时间。方法 cache 就是为了提高效率而设计的,但依然比不上直接调用来得快

8 在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么

方法调用时,方法查询之前。所以跟编译期间没有任何关系。所以这里的答案是:

  1. 检测 receiver 情况:nil 或 tagged
  2. 在缓存中查找实现

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 的世界里,其实只有两个类:NSObjectNSProxy,两者都实现了 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

打上断点:

image.png

运行程序,进入断点后打开汇编(勾选 “Xcode 菜单中的【Debug > Debug Workflow > Always Show Disassembly】”)

image.png

我们到 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));
}
复制代码

slowpathfastpath 不用在意,这是为了告诉编译器这个判断逻辑的大概率倾向,当做不存在即可

#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 这个操作呢?我个人针对这有两个理解:

  1. 继承。一个类在初始化的时候,必须要考虑父类在创建时候的操作,所以我们需要在 init 方法里调用 [super init]
  2. 自定义。我们可以在 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;
复制代码

isa_test.png 如图所示,第一行 log 为初始化状态:

  • 红框:添加关联对象后,has_assoc 位由 0 变为 1
  • 篮框:添加弱引用后,weakly_referenced 位由 0 变为 1
  • 绿框:在添加强引用后,extra_rc 的值会增加对应的数字
  • 黄框:在 extra_rc 达到存储最大值(524287,无符号 19 位最大值)后,再增加引用计数时,runtime 会将其一半的引用计数存储至散列表 SideTable 中,具体可查看 objc-object.h 中的 rootRetain 方法
  • 红色箭头:po 输出计算得到的 shiftclsobj.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)
复制代码

如图: image.png

如果还想要验证 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;
}
复制代码

简单总结一下:

  1. 判断是否为 tagged pointer,是则直接 return;
  2. 判断该对象的 isa 是否优化过,若未优化,则使用 sidetable_retain 存储引用计数;
  3. 若正在释放,则不进行 retain 操作;
  4. 引用计数 +1;
  5. 若引用计数 +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;
}
复制代码

简单总结一下:

  1. 判断是否为 tagged pointer,是则直接 return;
  2. 判断该对象的 isa 是否优化过,若未优化,则直接调用 sidetable_release 进行操作然后 return;
  3. 尝试将 isa.extra_rc -1,若未下溢出则执行第 5 步;
  4. 若下溢出了,从 sidetable 中取出 RC_HALF 并将该值 -1 存储到 isa.extra_rc 中(期间有重试流程);
  5. 执行 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();
    }
}
复制代码

总结起来就是:

  1. 根据继承链销毁 C++ 成员变量,执行其析构方法;
  2. 移除关联对象;
  3. 清空自身的 weak 引用(全部置为 nil);
  4. 清理 sidetable 中的自己,
  5. 调用 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 再运行即可:

image.png

其中第三行与第五行输出 “XXX is set” 是因为我在环境变量中添加了 OBJC_HELPOBJC_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

这里其实比较简单,它仅仅只是构造了 unattachedCategoriesallocatedClasses 两张表以便后续加载类使用。

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)();
        }
    }
}
复制代码

其流程是:

  1. 持续检查是否存在异常;
  2. 发生异常时,先尝试抛出异常并尝试运行,
  3. 若该异常未被处理,检查异常是否由 objc 对象产生;
  4. 如果当前异常是由 objc 对象产生,则利用该对象调用已注册的异常回调 uncaught_handler
  5. 如果当前异常不是有 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;
}
复制代码

运行一下: image.png

上边的 _objc_terminate 中在监测到异常时,会抛出异常交于开发者处理,那么我们只要捕获到这个异常并处理掉就可以继续运行了,所以我们将产生异常的地方修改一下:

@try {
    /// 这是一个不存在方法,会抛出异常
    [obj performSelector:@selector(test)];
} @catch (id e) {
    NSLog(@"捕获到了异常");
}
复制代码

再运行一下,看看结果: image.png

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]);
        }
    }
复制代码

总结一下流程:

  1. 如果是第一次进入,根据需要决定是否禁用共享缓存优化
  2. 从所有 mach_header 中获取所有的 header_info 并添加到链表中
    • 如果该 mach_header 的文件类型为可执行文件且未被 dyld3 优化过,则读取 selector 引用与 message 引用
  3. 如果是第一次进入,
    • 初始化 selector 表,注册内部(libobjc)使用的 selector
    • 初始化 sideTableMap
    • 初始化关联对象管理器 AssociationsManager
    • 开启 DebugScanWeakTables 的情况下开启所引用表扫描
    • 拒绝任何想要链接到主可执行程序的 GC 镜像
  4. 读取上边获取到的所有 header_info
  5. 执行镜像的 +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;
}
复制代码
  1. 根据给定数目初始化无序 selectors 集合
  2. 注册内部(libobjc)使用的方法
  3. 注册 C++ 构造方法
  4. 注册 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;
            }
        }
    }
}
复制代码

方法名是可以为所欲为的,但是在程序执行时可不能为所欲为。不同的类可能会有相同名字的方法,他们指向本该不同,因此需要修复。

查看更多

猜你喜欢

转载自juejin.im/post/7218915344119234616