0%

初探懒加载类

上一章我们探索了 iOS 中类的加载,让我们简单回顾一下大概的流程。

类的加载回顾

  • libObjcdyld 注册了回调 _dyld_objc_notify_register,当 dyldApp 以及 App 所依赖的一系列 Mach-O 镜像加载到当前 App 被分配的内存空间之后,dyld 会通过 _dyld_objc_notify_mapped 也就是 map_images 来通知 libObjc 来完成具体的加载工作,map_images 被调用之后会来到 _read_images
  • _read_images
    • 主要会进行类的加载工作,会插入 所有的类gdb_objc_realized_classes 哈希表中(插入方式为 类名为 key,类对象为value, 不包括通过 共享缓存 里面的类),同时还会把类插入到 allocatedClasses 这个集合里面,注意,allocatedClasses 的类型为 NXHashTable,可以类比为 NSSet,而 gdb_objc_realized_classes 的类型为 NXMapTable,可以类比为 NSDictionary
    • 对所有的类进行重映射
    • 将所有的 SEL 插入到 namedSelectors 哈希表中(插入方式为:SEL 名称为 keySELvalue)
    • 修复函数指针遗留
    • 将所有的 Protocol 插入到 readProtocol 哈希表中(插入方式为:Protocol 名称为 keyProtocolvalue)
    • 对所有的 Protocol 做重映射
    • 初始化所有的非懒加载类,包括 rwro 的初始化操作
    • 处理所有的分类(包括类的分类和元类的分类)

懒加载类的发现

我们这个时候观察 _read_images 源码这部分的注释:

Realize non-lazy classes (for +load methods and static instances)

实现非懒加载类(实现了 +load 方法和静态实例)

什么意思呢,我们这里其实打印的都是所谓的非懒加载类,这里除了我们自己实现了 +load 方法的两个类之外,其他的内容都是系统内置的类,包括我们十分熟悉的 NSObject 类。通过这里其实反过来推论,我们没有实现 +load 方法就是所谓的懒加载类,这种类并不会在 _read_images环节被加载,那么应该是在哪里加载呢?我们稍微思考一下,我们一般第一次操作一个类是不是在初始化这个类的时候,而初始化类不就是发送alloc消息吗,而根据我们前面探索消息查找的知识,在第一次发送某个消息的时候,是没有缓存的,所以会来到一个非常重要的方法叫lookUpImpOrForward,我们在main.mLGPerson类初始化的地方和lookUpImpOrForward 入口处打上断点:

Tips: 这里有个小技巧,我们先打开 main.m 文件中的断点,等断点来到了我们想要探索的 LGPerson 初始化的位置的时候,我们再打开 lookUpImpOrForward 处的断点,这样才能确保当前执行 lookUpImpOrForward 的是我们的研究对象 LGPerson

因为我们断点的位置是 LGPerson 类发送 alloc 消息,而显然 alloc 作为类方法是存储在元类上的,也就是说 lookUpImpOrForwardcls 其实是 LGPerson 元类。那么 inst 就应该是真正的对象.

我们总结一下,如果类没有实现 load 方法,那么这个类就是懒加载类,反之、这个类如果实现了 load 方法,那么这个类就是非懒加载类

懒加载类的流程

关于非懒加载类的加载流程我们已经很熟悉了,我们总结下懒加载类的流程:

  • 类第一次发送消息的时候是没有缓存的,所以会来到 _class_lookupMethodAndLoadCache3 ,关于这个方法我们在前面的消息查找章节已经介绍过了,不熟悉的同学可以去查阅一下。
  • _class_lookupMethodAndLoadCache3 会调用 lookUpImpOrForward ,这个方法的重要性在我们学习 Runtime 的过程中不言而喻
  • lookUpImpOrForward 内部会进行一下判断,如果 cls 没有被实现,会调用 realizeClassMaybeSwiftAndLeaveLocked 方法
  • realizeClassMaybeSwiftAndLeaveLocked 方法又会调用 realizeClassMaybeSwiftMaybeRelock 方法
  • realizeClassMaybeSwiftMaybeRelock 方法内部会进行一下是否是 Swift 的判断,如果不是 Swift 环境的话,就会来到 realizeClassWithoutSwift ,也就是最终的类的加载的地方

分类的底层实现

分类作为 Objective-C 中常见的特性,相信大家都不会陌生,不过在底层它是怎么实现的呢?

分类的定义

我们根据 _category_t 来到 libObjc 源码中进行查找,不过我们需要去掉一下 _category_t 的下划线,然后不难找到分类真正的定义所在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};

根据刚才 clang 重写之后的内容,我们不难看出

  • name : 是分类所关联的类,也就是类的名字,而不是分类的名字

  • cls : 我们在前面可以看到 clang 重写后这个值为 0,但是后面有注释为 &OBJC_CLASS_$_LGTeacher ,也就是我们的类对象的定义,所以这里其实就是我们要扩展的类对象,只是在编译期这个值并不存在

  • instanceMethods : 分类上存储的实例方法

  • classMethods :分类上存储的类方法

  • protocols :分类所实现的协议

  • instanceProperties :分类所定义的实例属性,不过我们一般在分类中添加属性都是通过关联对象来实现的

  • _classProperties:分类所定义的类属性。这里有一行注释:

    Fields below this point are not always present on disk.
    下面的内容并不是一直在磁盘上保存

也就是说 _classProperties 其实是一个私有属性,但并不是一直都存在的。

分类的加载

我们现在知道了类分为了 懒加载类非懒加载类 ,它们的加载时机是不一样的,那么分类的加载又是怎么样的呢?我们还是同样的先分析没有实现 load 方法的分类的情况:

但是我们在分析前,还要搞清楚一点,分类必须依附于类而存在,如果只有分类,没有类,那么从逻辑上是说不通的,就算实现了,编译器也会忽略掉。而关于类是懒加载还是非懒加载的,所以这里我们还要再细分一次。

  • 懒加载分类与懒加载类
  • 懒加载分类和非懒加载类

没有实现 load 的分类

与懒加载类配合加载

我们先分析第一种情况,也就是类和分类都不实现 load 方法的情况。
首先,懒加载类的流程上面我们已经探索过了,在向类第一次发送消息的时候,懒加载类才会开始加载,而根据我们上一章类的加载探索内容,在 realizeClassWithoutSwift 方法的最后有一个 methodizeClass 方法,在这个方法里面会有一个 Attach categories 的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Attach categories.
if (previously) {
if (isMeta) {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_METACLASS);
} else {
// When a class relocates, categories with class methods
// may be registered on the class itself rather than on
// the metaclass. Tell attachToClass to look for those.
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_CLASS_AND_METACLASS);
}
}
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

我们进入unattachedCategoriesattachToClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void attachToClass(Class cls, Class previously, int flags)
{
runtimeLock.assertLocked();
ASSERT((flags & ATTACH_CLASS) ||
(flags & ATTACH_METACLASS) ||
(flags & ATTACH_CLASS_AND_METACLASS));
auto &map = get();
auto it = map.find(previously);
if (it != map.end()) {
category_list &list = it->second;
if (flags & ATTACH_CLASS_AND_METACLASS) {
int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
} else {
attachCategories(cls, list.array(), list.count(), flags);
}
map.erase(it);
}
}

但是我们断点之后发现这个时候通过 unattachedCategoriesForClass 方法并没有取到分类,我们此时不妨通过 LLDB 打印一下当前类里面是否已经把分类的内容附加上了。
前面的流程大家都很熟悉了,我们直接看 clsrw 中的 methods 是否有内容:

这样进一步证明了,如果是懒加载类,并且分类也是懒加载,那么分类的加载并不会来到 unattachedCategoriesForClass ,而是直接在编译时加载到了类的 ro 里面,然后在运行时被拷贝到了类的 rw 里面。这一点可以通过下面的 LLDB 打印来证明。

与非懒加载类配合加载

同样的道理,当类为非懒加载类的时候,走的是 _read_images 里面的流程,这个时候我们的懒加载分类是在哪加载的呢?

分类还是不在这,同时通过 LLDB 打印,发现分类的方法已经在类的 ro 里面了,所以说分类的加载其实跟类的懒加载与否并没有关系,也就是说懒加载的分类都是在编译时期被加载的。

实现了 load 的分类

我们再接着分下下面两种情况:

  • 非懒加载分类与懒加载类
  • 非懒加载分类和非懒加载类
与懒加载类配合加载

其实懒加载和非懒加载的最大区别就是加载是否提前,而实现了 +load 方法的分类,面对的是懒加载的类,
而懒加载的类我们前面已经知道了,是在第一次发送消息的时候才会被加载的,那我们直接在
lookupImpOrForward => realizeClassMaybeSwiftAndLeaveLocked => realizeClassMaybeSwiftMaybeRelock => realizeClassWithoutSwift => methodizeClass 流程中的 methodizeClass 打上断点,看下在这里分类会不会被加载:

这一次通过 unattachedCategoriesForClass 取出来值了,并且在这之前 clsro 中并没有分类的 initialize 方法:

为什么走的不是发送消息的流程,而走的是 load_images 里面的 prepare_load_methods 方法呢?我们来到 prepare_load_methods 方法处:

可以看到,其实是在这里调用了 realizeClassWithoutSwift 方法来加载类的。而上面的 _getObjc2NonlazyCategoryList 方法显示就是获取的所有的非懒加载分类,然后遍历这些非懒加载分类,然后去加载这些分类所依赖的类。这个逻辑很好理解,非懒加载分类让我们的懒加载类实现提前了,所以说懒加载类并不一定只会在第一次消息发送的时候加载,还要取决于有没有非懒加载的分类,如果有非懒加载的分类,那么就走的是 load_images 里面的 prepare_load_methodsrealizeClassWithoutSwift

与非懒加载类配合加载

非懒加载类的流程我们也十分熟悉了,在 _read_images 里面进行加载,而此时,分类也是非懒加载。我们还是在 methodizeClass 里面进行断点:

结果如上图所示,这次从 unattachedCategoriesForClass 方法取出来的是 NULL 值,显然分类不是在这个地方被加载的,我们回到 _read_images 方法,还记得那个 Discover categories 流程吗,我们打开里面的断点:

其实 attachCategories 这个方法只会在实现了 load 方法的分类下才会被调用,而来到 attachCategories 之前又取决于类是否为懒加载,如果是懒加载,那么就在 load_images 里面去处理,如果是非懒加载,那么就在 map_images 里面去处理。

总结

我们今天探索的内容可能会有点绕,不过其实探索下来,我们只需要保持研究重点就很简单。分类的加载其实可以笼统的分为实现 load 方法和没有实现 load 方法:

  • 没有实现 load 方法的分类由编译时确定
  • 实现了 load 方法的分类由运行时去确定

这也说明分类的加载和类的加载是不一样的,而结合着类的懒加载与否,我们有以下的结论:

  • 懒加载分类 + 懒加载类

类的加载在第一次消息发送的时候,而分类的加载则在编译时

  • 懒加载分类 + 非懒加载类

类的加载在 _read_images 处,分类的加载还是在编译时

  • 非懒加载分类 + 懒加载类

类的加载在 load_images 内部,分类的加载在类加载之后的 methodizeClass

  • 非懒加载分类 + 非懒加载类

类的加载在 _read_images 处,分类的加载在类加载之后的 reMethodizeClass

应用的加载回顾

上一章我们对应用的加载有了初步的认识,我们知道了

  • 系统调用 exec() 会我们的应用映射到新的地址空间
  • 然后通过 dyld 进行加载、链接、初始化主程序和主程序所依赖的各种动态库
  • 最后在 initializeMainExecutable 方法中经过一系列初始化调用 notifySingle 函数,该函数会执行一个 load_images 的回调
  • 然后在 doModinitFuntions 函数内部会调用 __attribute__((constructor))c 函数
  • 然后 dyld 返回主程序的入口函数,开始进入主程序的 main 函数 在 main 函数执行执行,其实 dyld 还会在流程中初始化 libSystem,而 libSystem 又会去初始化 libDispatch,在 libDispatch 初始化方法里面又会有一步 _os_object_init,在 _os_object_init 内部就会调起 _objc_init。而对于 _objc_init 我们还需要继续探索,因为这里面会进行类的加载等一系列重要的工作。

探索_objc_init

首先来到 libObjc 源码的 _objc_init 方法处,你可以直接添加一个符号断点 _objc_init 或者全局搜索关键字来到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
//如果已经初始化直接返回
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}

environ_init

我们直接看调用的第一个方法environ_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/***********************************************************************
* environ_init
* Read environment variables that affect the runtime.
* Also print environment variable help, if requested.
**********************************************************************/
void environ_init(void)
{
if (issetugid()) {
// All environment variables are silently ignored when setuid or setgid
// This includes OBJC_HELP and OBJC_PRINT_OPTIONS themselves.
return;
}
// Turn off autorelease LRU coalescing by default for apps linked against
// older SDKs. LRU coalescing can reorder releases and certain older apps
// are accidentally relying on the ordering.
// rdar://problem/63886091
// if (!dyld_program_sdk_at_least(dyld_fall_2020_os_versions))
// DisableAutoreleaseCoalescingLRU = true;
bool PrintHelp = false;
bool PrintOptions = false;
bool maybeMallocDebugging = false;
// Scan environ[] directly instead of calling getenv() a lot.
// This optimizes the case where none are set.
for (char **p = *_NSGetEnviron(); *p != nil; p++) {
if (0 == strncmp(*p, "Malloc", 6) || 0 == strncmp(*p, "DYLD", 4) ||
0 == strncmp(*p, "NSZombiesEnabled", 16))
{
maybeMallocDebugging = true;
}
if (0 != strncmp(*p, "OBJC_", 5)) continue;

if (0 == strncmp(*p, "OBJC_HELP=", 10)) {
PrintHelp = true;
continue;
}
if (0 == strncmp(*p, "OBJC_PRINT_OPTIONS=", 19)) {
PrintOptions = true;
continue;
}
if (0 == strncmp(*p, "OBJC_DEBUG_POOL_DEPTH=", 22)) {
SetPageCountWarning(*p + 22);
continue;
}
const char *value = strchr(*p, '=');
if (!*value) continue;
value++;
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
if ((size_t)(value - *p) == 1+opt->envlen &&
0 == strncmp(*p, opt->env, opt->envlen))
{
*opt->var = (0 == strcmp(value, "YES"));
break;
}
}
}

// Special case: enable some autorelease pool debugging
// when some malloc debugging is enabled
// and OBJC_DEBUG_POOL_ALLOCATION is not set to something other than NO.
if (maybeMallocDebugging) {
const char *insert = getenv("DYLD_INSERT_LIBRARIES");
const char *zombie = getenv("NSZombiesEnabled");
const char *pooldebug = getenv("OBJC_DEBUG_POOL_ALLOCATION");
if ((getenv("MallocStackLogging")
|| getenv("MallocStackLoggingNoCompact")
|| (zombie && (*zombie == 'Y' || *zombie == 'y'))
|| (insert && strstr(insert, "libgmalloc")))
&&
(!pooldebug || 0 == strcmp(pooldebug, "YES")))
{
DebugPoolAllocation = true;
}
}
// if (!os_feature_enabled_simple(objc4, preoptimizedCaches, true)) {
// DisablePreoptCaches = true;
// }
// Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
if (PrintHelp || PrintOptions) {
if (PrintHelp) {
_objc_inform("Objective-C runtime debugging. Set variable=YES to enable.");
_objc_inform("OBJC_HELP: describe available environment variables");
if (PrintOptions) {
_objc_inform("OBJC_HELP is set");
}
_objc_inform("OBJC_PRINT_OPTIONS: list which options are set");
}
if (PrintOptions) {
_objc_inform("OBJC_PRINT_OPTIONS is set");
}
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
}
}
}

我们可以看到,这里主要是读取影响 Runtime 的一些环境变量,如果需要,还可以打印环境变量帮助提示。

我们可以在终端测试一下,直接输入export OBJC-HELP=1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
jason@192 ~ % export OBJC_HELP=1
jason@192 ~ % ls
objc[6097]: Objective-C runtime debugging. Set variable=YES to enable.
objc[6097]: OBJC_HELP: describe available environment variables
objc[6097]: OBJC_PRINT_OPTIONS: list which options are set
objc[6097]: OBJC_PRINT_IMAGES: log image and library names as they are loaded
objc[6097]: OBJC_PRINT_IMAGE_TIMES: measure duration of image loading steps
objc[6097]: OBJC_PRINT_LOAD_METHODS: log calls to class and category +load methods
objc[6097]: OBJC_PRINT_INITIALIZE_METHODS: log calls to class +initialize methods
objc[6097]: OBJC_PRINT_RESOLVED_METHODS: log methods created by +resolveClassMethod: and +resolveInstanceMethod:
objc[6097]: OBJC_PRINT_CLASS_SETUP: log progress of class and category setup
objc[6097]: OBJC_PRINT_PROTOCOL_SETUP: log progress of protocol setup
objc[6097]: OBJC_PRINT_IVAR_SETUP: log processing of non-fragile ivars
objc[6097]: OBJC_PRINT_VTABLE_SETUP: log processing of class vtables
objc[6097]: OBJC_PRINT_VTABLE_IMAGES: print vtable images showing overridden methods
objc[6097]: OBJC_PRINT_CACHE_SETUP: log processing of method caches
objc[6097]: OBJC_PRINT_FUTURE_CLASSES: log use of future classes for toll-free bridging
objc[6097]: OBJC_PRINT_PREOPTIMIZATION: log preoptimization courtesy of dyld shared cache
objc[6097]: OBJC_PRINT_CXX_CTORS: log calls to C++ ctors and dtors for instance variables
objc[6097]: OBJC_PRINT_EXCEPTIONS: log exception handling
objc[6097]: OBJC_PRINT_EXCEPTION_THROW: log backtrace of every objc_exception_throw()
objc[6097]: OBJC_PRINT_ALT_HANDLERS: log processing of exception alt handlers
objc[6097]: OBJC_PRINT_REPLACED_METHODS: log methods replaced by category implementations
objc[6097]: OBJC_PRINT_DEPRECATION_WARNINGS: warn about calls to deprecated runtime functions
objc[6097]: OBJC_PRINT_POOL_HIGHWATER: log high-water marks for autorelease pools
objc[6097]: OBJC_PRINT_CUSTOM_CORE: log classes with custom core methods
objc[6097]: OBJC_PRINT_CUSTOM_RR: log classes with custom retain/release methods
objc[6097]: OBJC_PRINT_CUSTOM_AWZ: log classes with custom allocWithZone methods
objc[6097]: OBJC_PRINT_RAW_ISA: log classes that require raw pointer isa fields
objc[6097]: OBJC_DEBUG_UNLOAD: warn about poorly-behaving bundles when unloaded
objc[6097]: OBJC_DEBUG_FRAGILE_SUPERCLASSES: warn about subclasses that may have been broken by subsequent changes to superclasses
objc[6097]: OBJC_DEBUG_NIL_SYNC: warn about @synchronized(nil), which does no synchronization
objc[6097]: OBJC_DEBUG_NONFRAGILE_IVARS: capriciously rearrange non-fragile ivars
objc[6097]: OBJC_DEBUG_ALT_HANDLERS: record more info about bad alt handler use
objc[6097]: OBJC_DEBUG_MISSING_POOLS: warn about autorelease with no pool in place, which may be a leak
objc[6097]: OBJC_DEBUG_POOL_ALLOCATION: halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
objc[6097]: OBJC_DEBUG_DUPLICATE_CLASSES: halt when multiple classes with the same name are present
objc[6097]: OBJC_DEBUG_DONT_CRASH: halt the process by exiting instead of crashing
objc[6097]: OBJC_DISABLE_VTABLES: disable vtable dispatch
objc[6097]: OBJC_DISABLE_PREOPTIMIZATION: disable preoptimization courtesy of dyld shared cache
objc[6097]: OBJC_DISABLE_TAGGED_POINTERS: disable tagged pointer optimization of NSNumber et al.
objc[6097]: OBJC_DISABLE_TAG_OBFUSCATION: disable obfuscation of tagged pointers
objc[6097]: OBJC_DISABLE_NONPOINTER_ISA: disable non-pointer isa fields
objc[6097]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY: disable safety checks for +initialize after fork

可以看到不同的环境变量对应的内容都被打印出来了。

tls_init

接着看tls_init方法内部:

1
2
3
4
5
6
7
8
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

这里执行的是关于线程 key 的绑定,比如每个线程数据的析构函数。

static_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors,
* so we have to do it ourselves.
**********************************************************************/
static void static_init()
{
size_t count;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
inits[i]();
}
auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
UnsignedInitializer init(offsets[i]);
init();
}
}

这里会运行 C++ 的静态构造函数,在 dyld 调用我们的静态构造函数之前,libc 会调用 _objc_init,所以这里我们必须自己来做,并且这里只会初始化系统内置的 C++ 静态构造函数,我们自己代码里面写的并不会在这里初始化。

runtime_init

1
2
3
4
5
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}

Runtime环境初始化,unattachedCategories和allocatedClasses两张表的初始化工作

exception_init

1
2
3
4
5
6
7
8
9
/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}

这里是初始化 libobjc 的异常处理系统,我们程序触发的异常都会来到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/***********************************************************************
* _objc_terminate
* Custom std::terminate handler.
*
* The uncaught exception callback is implemented as a std::terminate handler.
* 1. Check if there's an active exception
* 2. If so, check if it's an Objective-C exception
* 3. If so, call our registered callback with the object.
* 4. Finally, call the previous terminate handler.
**********************************************************************/
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.
@try {
__cxa_rethrow();
} @catch (id e) {
// It's an objc object. Call Foundation's handler, if any.
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
// It's not an objc object. Continue to C++ terminate.
(*old_terminate)();
}
}
}

我们可以看到 _objc_terminate 是未处理异常的回调函数,其内部逻辑如下:

  • 检查是否是一个活跃的异常
  • 如果是活跃的异常,检查是否是 OC 抛出的异常
  • 如果是 OC 抛出的异常,调用 uncaught_handeler 回调函数指针
  • 如果不是 OC 抛出的异常,则继续 C++ 终止操作

cache_t::init()

看字面量很容易猜到,cache_t属性的初始化

_imp_implementationWithBlock_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// Initialize the trampoline machinery. Normally this does nothing, as
/// everything is initialized lazily, but for certain processes we eagerly load
/// the trampolines dylib.
void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
// Eagerly load libobjc-trampolines.dylib in certain processes. Some
// programs (most notably QtWebEngineProcess used by older versions of
// embedded Chromium) enable a highly restrictive sandbox profile which
// blocks access to that dylib. If anything calls
// imp_implementationWithBlock (as AppKit has started doing) then we'll
// crash trying to load it. Loading it here sets it up before the sandbox
// profile is enabled and blocks it.
//
// This fixes EA Origin (rdar://problem/50813789)
// and Steam (rdar://problem/55286131)
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
#endif
}

启动回调机制。通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib。

_dyld_objc_notify_register

接下来是我们今天探索的重点了: _dyld_objc_notify_register ,我们先看下它的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);

注意:仅供 objc 运行时使用
objc 镜像被映射(mapped)卸载(unmapped)初始化(initialized)的时候,注册的回调函数就会被调用。
这个方法是 dlyd 中声明的,一旦调用该方法,调用结果会作为该函数的参数回传回来。比如,当所有的 images 以及 sectionobjc-image-info 被加载之后会回调 mapped 方法。
load 方法也将在这个方法中被调用。

_dyld_objc_notify_register 方法的三个参数 map_imagesload_imagesunmap_image 其实都是函数指针:

1
2
3
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);

这三个函数指针是在 dyld 中回调的,我们打开 dyld 的源码即可一探究竟,我们直接搜索 _dyld_objc_notify_register :

1
2
3
4
5
6
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}

接着来到 dyldregisterObjCNotifiers 方法内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;//map_images
sNotifyObjCInit = init;//load_images
sNotifyObjCUnmapped = unmapped;//unmap_images

// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}

// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}
}
1
2
3
static _dyld_objc_notify_mapped		sNotifyObjCMapped;
static _dyld_objc_notify_init sNotifyObjCInit;
static _dyld_objc_notify_unmapped sNotifyObjCUnmapped;

通过上面的代码的内容说明在registerObjCNotifiers 内部, libObjc 传过来的这三个函数指针被 dyld 保存在了本地静态变量中。换句话来说,最终函数指针是否能被调用,取决于这三个静态变量:

  • sNotifyObjCMapped
  • sNotifyObjCInit
  • sNotifyObjCUnmapped

我们注意到 registerObjCNotifierstry-catch 语句中的 try 分支注释如下:

call ‘mapped’ function with all images mapped so far
调用 mapped 函数来映射所有的镜像

那么也就是说 notifyBatchPartial 里面会进行真正的函数指针的调用,我们进入这个方法内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static void notifyBatchPartial(dyld_image_states state, bool orLater, dyld_image_state_change_handler onlyHandler, bool preflightOnly, bool onlyObjCMappedNotification)
{
//省略代码
// tell objc about new images
if ( (onlyHandler == NULL) && ((state == dyld_image_state_bound) || (orLater && (dyld_image_state_bound > state))) && (sNotifyObjCMapped != NULL) ) {
const char* paths[imageCount];
const mach_header* mhs[imageCount];
unsigned objcImageCount = 0;
for (int i=0; i < imageCount; ++i) {
ImageLoader* image = findImageByMachHeader(infos[i].imageLoadAddress);
bool hasObjC = false;
if ( image != NULL ) {
if ( image->objCMappedNotified() )
continue;
hasObjC = image->notifyObjC();
}
#if SUPPORT_ACCELERATE_TABLES
else if ( sAllCacheImagesProxy != NULL ) {
const mach_header* mh;
const char* path;
unsigned index;
if ( sAllCacheImagesProxy->addressInCache(infos[i].imageLoadAddress, &mh, &path, &index) ) {
hasObjC = (mh->flags & MH_HAS_OBJC);
}
}
#endif
if ( hasObjC ) {
paths[objcImageCount] = infos[i].imageFilePath;
mhs[objcImageCount] = infos[i].imageLoadAddress;
++objcImageCount;
if ( image != NULL )
image->setObjCMappedNotified();
}
}
if ( objcImageCount != 0 ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_MAP, 0, 0, 0);
uint64_t t0 = mach_absolute_time();
(*sNotifyObjCMapped)(objcImageCount, paths, mhs);//函数指针真正调用的地方
uint64_t t1 = mach_absolute_time();
ImageLoader::fgTotalObjCSetupTime += (t1-t0);
}
}
}
//省略代码
}

我们可以看到,在 notifyBatchPartial 方法内部,这里的注释:

tell objc about new images 告诉 objc 镜像已经映射完成了

弄清楚了三个函数指针是怎么调用的还不够,接下来我们要深入各个函数的内部看里面究竟做了什么样的事情。

探索 map_images

首先是 map_images ,我们来到它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}

Process the given images which are being mapped in by dyld.
Calls ABI-agnostic code after taking ABI-specific locks.

处理由 dyld 映射的给定镜像
取得特定于 ABI 的锁后,调用与 ABI 无关的代码。

这里会继续往下走到 map_images_nolock

map_images_nolock 内部代码十分冗长,我们经过分析之后,前面的工作基本上都是进行镜像文件信息的提取与统计,所以可以定位到最后的 _read_images

1
2
3
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}

这里进入 _read_images 的条件是 hCount 大于 0, hCount 表示的是 Mach-Oheader 的数量

OK,我们的主角登场了, _read_imageslookupImpOrForward 可以说是我们学习 RuntimeiOS 底层里面非常重要的两个概念了, lookUpImpOrForward 已经探索过了,剩下的 _read_images 我们也不能落下。

_read_images定义

Perform initial processing of the headers in the linked list beginning with headerList.
headerList 开始,对已经链接了的 Mach-O 镜像表中的头部进行初始化处理

我们可以看到,整个 _read_images 有接近 400 行代码。

通过查看代码,以及日志打印提示信息,我们大致可以将 _read_images 分为下面几个流程:

_read_images具体流程

doneOnce 流程

我们从第一个分支 doneOnce 开始,这个名词顾名思义,只会执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
if (!doneOnce) {
doneOnce = YES;
launchTime = YES;
#if SUPPORT_NONPOINTER_ISA
// Disable non-pointer isa under some conditions.

# if SUPPORT_INDEXED_ISA
// Disable nonpointer isa if any image contains old Swift code
for (EACH_HEADER) {
if (hi->info()->containsSwift() &&
hi->info()->swiftUnstableVersion() < objc_image_info::SwiftVersion3)
{
DisableNonpointerIsa = true;
if (PrintRawIsa) {
_objc_inform("RAW ISA: disabling non-pointer isa because "
"the app or a framework contains Swift code "
"older than Swift 3.0");
}
break;
}
}
# endif
  • 通过宏 SUPPORT_NONPOINTER_ISA 判断当前是否支持开启内存优化的 isa
    • 如果支持,则在某些条件下需要禁用这个优化
  • 通过宏 SUPPORT_INDEXED_ISA 判断当前是否是将类存储在 isa 作为类表索引
    • 如果是的话,再递归遍历所有的 Mach-O 的头部,并且判断如果是 Swift 3.0 之前的代码,就需要禁用对 isa 的内存优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# if TARGET_OS_OSX
// Disable non-pointer isa if the app is too old
// (linked before OS X 10.11)
// if (!dyld_program_sdk_at_least(dyld_platform_version_macOS_10_11)) {
// DisableNonpointerIsa = true;
// if (PrintRawIsa) {
// _objc_inform("RAW ISA: disabling non-pointer isa because "
// "the app is too old.");
// }
// }

// Disable non-pointer isa if the app has a __DATA,__objc_rawisa section
// New apps that load old extensions may need this.
for (EACH_HEADER) {
if (hi->mhdr()->filetype != MH_EXECUTE) continue;
unsigned long size;
if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) {
DisableNonpointerIsa = true;
if (PrintRawIsa) {
_objc_inform("RAW ISA: disabling non-pointer isa because "
"the app has a __DATA,__objc_rawisa section");
}
}
break; // assume only one MH_EXECUTE image
}
# endif
  • 通过宏 TARGET_OS_OSX 判断是否是 macOS 执行环境
  • 判断 macOS 的系统版本,如果小于 10.11 则说明 app 太陈旧了,需要禁用掉 non-pointer isa
  • 然后再遍历所有的 Mach-O 的头部,判断如果有 __DATA__,__objc_rawisa 段的存在,则禁用掉 non-pointer isa ,因为很多新的 app 加载老的扩展的时候会需要这样的判断操作。
1
2
3
4
5
6
7
// namedClasses
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);

预先优化过的类不会加入到 gdb_objc_realized_classes 这个哈希表中来, gdb_objc_realized_classes 哈希表的装载因子为 0.75,这是一个经过验证的效率很高的扩容临界值。

  • 加载所有类到类的 gdb_objc_realized_classes 表中来
1
2
3
4
5
// This is a misnomer: gdb_objc_realized_classes is actually a list of 
// named classes not in the dyld shared cache, whether realized or not.
// This list excludes lazily named classes, which have to be looked up
// using a getClass hook.
NXMapTable *gdb_objc_realized_classes; // exported for debuggers in objc-gdb.h

这是一个误称:gdb_objc_realized_classes 表实际上存储的是不在 dyld 共享缓存里面的命名类,无论这些类是否实现

除了 gdb_objc_realized_classes 表之外,还有一张表 allocatedClasses :

其实 gdb_objc_realized_classesallocatedClasses 是一种包含的关系,一张是类的总表,一张是已经开辟了内存的类表.

Discover classes 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
for (EACH_HEADER) {
if (! mustReadClasses(hi, hasDyldRoots)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
classref_t const *classlist = _getObjc2ClassList(hi, &count);

bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->hasPreoptimizedClasses();

for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

if (newCls != cls && newCls) {
// Class was moved but not deleted. Currently this occurs
// only when the new class resolved a future class.
// Non-lazily realize the class below.
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}

Discover classes. Fix up unresolved future classes. Mark bundle classes.
发现类。修正未解析的 future 类,标记 bundle 类。

  • 先通过 _getObjc2ClassList 来获取到所有的类
  • 接着还是遍历所有的 Mach-Oheader 部分,然后通过 mustReadClasses 来判断哪些条件可以跳过读取类这一步骤
  • 读取 header 是否是 Bundle
  • 读取 header 是否开启了 预优化
  • 遍历 _getObjc2ClassList 取出的所有的类
    • 通过 readClass 来读取类信息
    • 判断如果不相等并且 readClass 结果不为空,则需要重新为类开辟内存

Fix up remapped classes 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Fix up remapped classes
// Class list and nonlazy class list remain unremapped.
// Class refs and super refs are remapped for message dispatching.

if (!noClassesRemapped()) {
for (EACH_HEADER) {
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
// fixme why doesn't test future1 catch the absence of this?
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
}
}

修复 重映射类
类表和非懒加载类表没有被重映射 (也就是 _objc_classlist)
由于消息转发,类引用和父类引用会被重映射 (也就是 _objc_classrefs)

  • 通过 noClassesRemapped 方法判断是否有类引用(_objc_classrefs)需要进行重映射
    • 如果需要,则遍历 EACH_HEADER
    • 通过 _getObjc2ClassRefs_getObjc2SuperRefs 取出当前遍历到的 Mach-O 的类引用和父类引用,然后调用 remapClassRef 进行重映射

Fix up @selector references 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // Fix up @selector references
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->hasPreoptimizedSelectors()) continue;

bool isBundle = hi->isBundle();
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);
if (sels[i] != sel) {
sels[i] = sel;
}
}
}
}
SEL sel_registerNameNoLock(const char *name, bool copy) {
return __sel_registerName(name, 0, copy); // NO lock, maybe copy
}

修正 SEL 引用

  • 操作前先加一个 selLock
  • 然后遍历EACH_HEADER
    • 如果开启了预优化,contiue 到下一个 Mach-O
    • 通过 _getObjc2SelectorRefs 拿到所有的 SEL 引用
    • 然后对所有的 SEL 引用调用 sel_registerNameNoLock 进行注册

也就是说这一流程最主要的目的就是注册 SEL ,我们注册真正发生的地方: __sel_registerName ,这个函数如果大家经常玩 Runtime 肯定不会陌生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
SEL result = 0;

if (shouldLock) selLock.assertUnlocked();
else selLock.assertLocked();

if (!name) return (SEL)0;

result = search_builtins(name);
if (result) return result;

conditional_mutex_locker_t lock(selLock, shouldLock);
auto it = namedSelectors.get().insert(name);
if (it.second) {
// No match. Insert.
*it.first = (const char *)sel_alloc(name, copy);
}
return (SEL)*it.first;
}

我们简单分析一下 __sel_registerName 方法的流程:

  • 判断是否要加锁
  • 如果 sel 为空,则返回一个空的 SEL
  • builtins 中搜索,看是否已经注册过,如果找到,直接返回结果
  • namedSelectors 哈希表中查询,找到了就返回结果
  • 如果 namedSelectors 未初始化,则创建一下这个哈希表
  • 如果上面的流程都没有找到,则需要调用 sel_alloc 来创建一下 SEL ,然后把新创建的 SEL 插入哈希表中进行缓存的填充
Fix up old objc_msgSend_fixup call sites 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if SUPPORT_FIXUP
// Fix up old objc_msgSend_fixup call sites
for (EACH_HEADER) {
message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
if (count == 0) continue;
if (PrintVtables) {
_objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
"call sites in %s", count, hi->fname());
}
for (i = 0; i < count; i++) {
fixupMessageRef(refs+i);
}
}
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif

修正旧的 objc_msgSend_fixup 调用

这个流程的执行前提是 FIXUP 被开启。

  • 还是老套路,遍历EACH_HEADER
    • 通过 _getObjc2MessageRefs 方法来获取当前遍历到的 Mach-O 镜像的所有消息引用
    • 然后遍历这些消息引用,然后调用 fixupMessageRef 进行修正
Discover protocols 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
ASSERT(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->hasPreoptimizedProtocols();

// Skip reading protocols if this is an image from the shared cache
// and we support roots
// Note, after launch we do need to walk the protocol as the protocol
// in the shared cache is marked with isCanonical() and that may not
// be true if some non-shared cache binary was chosen as the canonical
// definition
if (launchTime && isPreoptimized) {
if (PrintProtocols) {
_objc_inform("PROTOCOLS: Skipping reading protocols in image: %s",
hi->fname());
}
continue;
}
bool isBundle = hi->isBundle();
protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}

发现协议,并修正协议引用

Fix up @protocol references 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Fix up @protocol references
// Preoptimized images may have the right
// answer already but we don't know for sure.
for (EACH_HEADER) {
// At launch time, we know preoptimized image refs are pointing at the
// shared cache definition of a protocol. We can skip the check on
// launch, but have to visit @protocol refs for shared cache images
// loaded later.
if (launchTime && hi->isPreoptimized())
continue;
protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
for (i = 0; i < count; i++) {
remapProtocolRef(&protolist[i]);
}
}

对所有的协议做重映射

Realize non-lazy classes 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Realize non-lazy classes (for +load methods and static instances)
for (EACH_HEADER) {
classref_t const *classlist = hi->nlclslist(&count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;

addClassTableEntry(cls);

if (cls->isSwiftStable()) {
if (cls->swiftMetadataInitializer()) {
_objc_fatal("Swift class %s with a metadata initializer "
"is not allowed to be non-lazy",
cls->nameForLogging());
}
// fixme also disallow relocatable classes
// We can't disallow all Swift classes because of
// classes like Swift.__EmptyArrayStorage
}
realizeClassWithoutSwift(cls, nil);
}
}

初始化非懒加载类( +load 方法和静态实例)**

Realize newly-resolved future classes 流程
1
2
3
4
5
6
7
8
9
10
11
12
// Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) {
for (i = 0; i < resolvedFutureClassCount; i++) {
Class cls = resolvedFutureClasses[i];
if (cls->isSwiftStable()) {
_objc_fatal("Swift class is not allowed to be future");
}
realizeClassWithoutSwift(cls, nil);
cls->setInstancesRequireRawIsaRecursively(false/*inherited*/);
}
free(resolvedFutureClasses);
}

初始化新解析出来的 future

Discover categories 流程
1
2
3
4
5
6
7
8
9
// Discover categories. Only do this after the initial category
// attachment has been done. For categories present at startup,
// discovery is deferred until the first load_images call after
// the call to _dyld_objc_notify_register completes. rdar://problem/53119145
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}

处理所有的分类,包括类和元类

到这里, _read_images 的流程就分析完毕,我们可以新建一个文件来去掉一些干扰的信息,只保留核心的逻辑,这样从宏观的角度来分析更直观:

_read_image_schedule

Q & A 环节
Q: dyld 主要逻辑是加载库,也就是镜像文件,但是加载完是怎么读取的呢?
A: _read_images 是真正读取的地方

Q: SEL 方法编号何时加载?
A: _read_images

read_class 分析

我们探索了 _read_images 方法的流程,接下来让我们把目光放到本文的主题 - 类的加载
既然是类的加载,那么我们在前面所探索的类的结构中出现的内容都会一一重现。
所以我们不妨直接进行断点调试,让我们略过其它干扰信息,聚焦于类的加载。

  • 根据上一小节我们探索的结果, doneOnce 流程中会创建两个哈希表,并没有涉及到类的加载,所以我们跳过
  • 我们来到第二个流程 - 类处理

我们在_read_images函数,定位到下面这行代码:

1
2
Class cls = (Class)classlist[i];//断点
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

可以看到 cls 的属性、方法、协议以及类名都为空,说明这里类并没有被真正加载完成,我们接着聚焦到 read_class 函数上面,我们进入其内部实现,我们大致浏览之后会定位到如下图所示的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/***********************************************************************
* readClass
* Read a class and metaclass as written by a compiler.
* Returns the new class pointer. This could be:
* - cls
* - nil (cls has a missing weak-linked superclass)
* - something else (space for this class was reserved by a future class)
*
* Note that all work performed by this function is preflighted by
* mustReadClasses(). Do not change this function without updating that one.
*
* Locking: runtimeLock acquired by map_images or objc_readClassPair
**********************************************************************/
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->nonlazyMangledName();

if (missingWeakSuperclass(cls)) {
// No superclass (probably weak-linked).
// Disavow any knowledge of this subclass.
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING class '%s' with "
"missing weak-linked superclass",
cls->nameForLogging());
}
addRemappedClass(cls, nil);
cls->setSuperclass(nil);
return nil;
}

cls->fixupBackwardDeployingStableSwift();

Class replacing = nil;
if (mangledName != nullptr) {
if (Class newCls = popFutureNamedClass(mangledName)) {
// This name was previously allocated as a future class.
// Copy objc_class to future class's struct.
// Preserve future's rw data block.

if (newCls->isAnySwift()) {
_objc_fatal("Can't complete future class request for '%s' "
"because the real class is too big.",
cls->nameForLogging());
}

class_rw_t *rw = newCls->data();
const class_ro_t *old_ro = rw->ro();
memcpy(newCls, cls, sizeof(objc_class));

// Manually set address-discriminated ptrauthed fields
// so that newCls gets the correct signatures.
newCls->setSuperclass(cls->getSuperclass());
newCls->initIsa(cls->getIsa());

rw->set_ro((class_ro_t *)newCls->data());
newCls->setData(rw);
freeIfMutable((char *)old_ro->getName());
free((void *)old_ro);

addRemappedClass(cls, newCls);

replacing = cls;
cls = newCls;
}
}

if (headerIsPreoptimized && !replacing) {
// class list built in shared cache
// fixme strict assert doesn't work because of duplicates
// ASSERT(cls == getClass(name));
ASSERT(mangledName == nullptr || getClassExceptSomeSwift(mangledName));
} else {
if (mangledName) { //some Swift generic classes can lazily generate their names
addNamedClass(cls, mangledName, replacing);
} else {
Class meta = cls->ISA();
const class_ro_t *metaRO = meta->bits.safe_ro();
ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");
ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");
}
addClassTableEntry(cls);
}

// for future reference: shared cache never contains MH_BUNDLEs
if (headerIsBundle) {
cls->data()->flags |= RO_FROM_BUNDLE;
cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
}

return cls;
}

看起来类的信息在这里完成了加载,那么为了验证我们的猜想,直接断点调试一下但发现断点根本走不进来,原因在于这里的判断语句

1
if (Class newCls = popFutureNamedClass(mangledName))

判断当前传入的类的类名是否有 future 类的实现,但是我们刚才已经打印了,类名是空的,所以肯定不会执行这里。我们接着往下走:

  • addNamedClass 内部其实是将 cls 插入到 gdb_objc_realized_classes
  • addclassTableEntry 内部是将 cls 插入到 allocatedClasses

分析完 read_class ,我们回到 _read_images 方法

我们可以看到 read_class 返回的 newCls 会进行一个判断,判断与传入 read_class 之前的 cls 是否相等,而在 read_class 内部只有一个地方对类的内容进行了改动,但是我们刚才测试了是进不去的,所以这个 if 里面的内容我们可以略过,也就是说 resolvedFutureClasses 的内容我们都可以暂时略过。

总结一下 readClass

  • 判断是不是要后期处理的类
    • 如果是的话,就取出后期处理的类,读取这个类的 data() 类设置 ro/rw
  • addNamedClass 插入总表
  • addClassTableEntry 插入已开辟内存的类的表

realizeClassWithoutSwift 分析

通过分析 read_class ,我们可以得知,类已经被注册到两个哈希表中去了,那么现在一切时机都已经成熟了。但是我们还是要略过像 Fix up remapped classesFix up @selector referencesfix up old objc_msgSend_fixup call sitesDiscover protocols. Fix up protocol refsFix up @protocol references ,因为我们的重点是类的加载,我们最终来到了 Realize non-lazy classes (for +load methods and static instances) ,略去无关信息之后,我们可以看到我们的
主角 realizeClassWithoutSwift 闪亮登场了:

1
2
3
4
5
6
7
8
9
/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls, Class previously)

从方法的名称以及方法注释我们可以知道, realizeClassWithoutSwift 是进行类的第一次初始化操作,包括分配读写数据也就是我们常说的 rw ,但是并不会进行任何的 Swift 端初始化。我们直接聚焦下面的代码:

1
2
3
4
5
// Normal class. Allocate writeable class data.
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
  • 通过 zalloc 开辟内存空间,返回一个新的 rw
  • cls 取出来的 ro 赋值给这个 rw
  • rw 设置到 cls 身上

可以清楚地看到,此时 rw 还是为空,说明这里只是对 rw 进行了初始化,但是方法、属性、协议这些都没有被添加上。

我们接着往下走:

1
2
3
4
5
6
7
8
9
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
// This assumes that none of those classes have Swift contents,
// or that Swift's initializers have already been called.
// fixme that assumption will be wrong if we add support
// for ObjC subclasses of Swift classes.
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

这里可以看到父类和元类都会递归调用 realizeClassWithoutSwift 来初始化各自的 rw 。为什么在类的加载操作里面要去加载类和元类呢?回忆一下类的结构,答案很简单,要保证 superclassisa 的完整性,也就是保证类的完整性,

1
2
3
// Update superclass and metaclass in case of remapping
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);

上面的代码就是最好的证明,初始化完毕的父类和元类被赋值到了类的 superclassisa 上面。

接着往下走可以看到,不光要把父类关联到类上面,还要让父类知道子类的存在。

最后一行代码是 methodizeClass(cls) ,注释显示的是 attach categories ,附加分类到类?我们进入其内部实现一探究竟。

在探索 methodizeClass 前,我们先总结一下 realizeClassWithoutSwift :

  • 读取 classdata()
  • ro/rw 赋值
  • 父类和元类实现
    • supercls = realizeClassWithoutSwift(remapClass(cls->superclass))
    • metacls = realizeClassWithoutSwift(remapClass(cls->ISA()))
  • 父类和元类归属关系
    • cls->superclass = supercls
    • cls->initClassIsa(metacls)
  • 将当前类链接到其父类的子类列表 addSubclass(supercls, cls)

methodizeClass 分析

realizeClassWithoutSwift方法最后一行调用的是methodizeClass方法

1
2
3
4
5
6
7
/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void methodizeClass(Class cls, Class previously)

对类的方法列表、协议列表和属性列表进行修正
附加 category 到类上面来

我们直接往下面走:

1
2
3
4
5
6
// Install methods and properties that the class implements itself.
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
if (rwe) rwe->methods.attachLists(&list, 1);
}
  • ro 中取出方法列表附加到 rw
1
2
3
4
property_list_t *proplist = ro->baseProperties;
if (rwe && proplist) {
rwe->properties.attachLists(&proplist, 1);
}
  • ro 中取出属性列表附加到 rw
1
2
3
4
protocol_list_t *protolist = ro->baseProtocols;
if (rwe && protolist) {
rwe->protocols.attachLists(&protolist, 1);
}
  • ro 中取出协议列表附加到 rw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Attach categories.
if (previously) {
if (isMeta) {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_METACLASS);
} else {
// When a class relocates, categories with class methods
// may be registered on the class itself rather than on
// the metaclass. Tell attachToClass to look for those.
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_CLASS_AND_METACLASS);
}
}
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
  • cls 中取出未附加的分类进行附加操作

探索 load_images

我们接着探索 _dyld_objc_notify_register 的第二个参数 load_images ,这个函数指针是在什么时候调用的呢,同样的,我们接着在 dyld 源码中搜索对应的函数指针 sNotifyObjCInit :

1
2
3
4
5
6
7
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}

可以看到,在 notifySingle 方法内部, sNotifyObjCInit 函数指针被调用了。根据我们上一篇文章探索 dyld 底层可以知道, _load_images 应该是对于每一个加载进来的 Mach-O 镜像都会递归调用一次。

我们来到 libObjc 源码中 load_images 的定义处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock)
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}

处理由 dyld 映射的给定镜像中的 +load 方法

  • 判断是否有 load 方法,如果没有,直接返回
  • 搜索 load 方法,具体实现通过 prepare_load_methods
  • 调用 load 方法,具体实现通过 call_load_methods

prepare_load_methods 分析

从这个方法名称,我们猜测这里应该做的是 load 方法的一些预处理工作,让我们来到源码进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->getSuperclass());
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method

if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}

if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
  • 首先通过 _getObjc2NonlazyClassList 获取所有已经加载进去的类列表

  • 然后通过schedule_class_load 遍历这些类

    • 递归调用遍历父类的 load 方法,确保父类的 load 方法顺序排在子类的前面
    • 通过 add_class_to_loadable_list , 把类的 load 方法存在 loadable_classes 里面
  • 完成 schedule_class_load 之后,通过 _getObjc2NonlazyCategoryList 取出所有分类数据

  • 然后遍历这些分类

    • 通过 realizeClassWithoutSwift 来防止类没有初始化,如果已经初始化了则不影响
    • 通过 add_category_to_loadable_list ,加载分类中的 load 方法到 loadable_categories 里面

call_load_methods 分析

通过名称我们可以知道 call_load_methods 应该就是 load 方法被调用的地方了。我们直接看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* Class +load methods are called superclass-first.
* Category +load methods are not called until after the parent class's +load.
*
* This method must be RE-ENTRANT, because a +load could trigger
* more image mapping. In addition, the superclass-first ordering
* must be preserved in the face of re-entrant calls. Therefore,
* only the OUTERMOST call of this function will do anything, and
* that call will handle all loadable classes, even those generated
* while it was running.
*
* The sequence below preserves +load ordering in the face of
* image loading during a +load, and make sure that no
* +load method is forgotten because it was added during
* a +load call.
* Sequence:
* 1. Repeatedly call class +loads until there aren't any more
* 2. Call category +loads ONCE.
* 3. Run more +loads if:
* (a) there are more classes to load, OR
* (b) there are some potential category +loads that have
* still never been attempted.
* Category +loads are only run once to ensure "parent class first"
* ordering, even if a category +load triggers a new loadable class
* and a new loadable category attached to that class.
*
* Locking: loadMethodLock must be held by the caller
* All other locks must not be held.
**********************************************************************/
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}

call_load_methods
调用类和类别中所有未决的 +load 方法
类里面 +load 方法是父类优先调用的
而在父类的 +load 之后才会调用分类的 +load 方法

  • 通过 objc_autoreleasePoolPush 压栈一个自动释放池
  • do-while循环开始
    • 循环调用类的 +load 方法直到找不到为止
    • 调用一次分类中的 +load 方法
  • 通过 objc_autoreleasePoolPop 出栈一个自动释放池

总结

至此, _objc_init_dyld_objc_notify_register 我们就分析完了,我们对类的加载有了更细致的认知。 iOS 底层有时候探索起来确实很枯燥,但是如果能找到高效的方法以及明确自己的所探索的方向,会让自己从宏观上重新审视这门技术。是的,技术只是工具,我们不能被技术所绑架,我们要做到有的放矢的去探索,这样才能事半功倍。

App从点击图标就开启了生命周期,本文从App启动开始探索。

前导知识

Mach-O文件

Mach-O is a bunch of file types for different run time executables.
Mach-OiOS 系统不同运行时期可执行的文件的文件类型统称。

维基百科上关于 Mach-O 的描述:

Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品,Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。
大多数基于 Mach 内核的操作系统都使用 Mach-O。NeXTSTEP、OS X 和 iOS 是使用这种格式作为本地可执行文件、库和对象代码的例子。

Mach-O有三种文件类型:ExecutableDylibBundle

  • ExecutableL类型

    So the first executable, that’s the main binary in an app, it’s also the main binary in an app extension.
    Executableapp 的二进制主文件,同时也是 app extension 的二进制主文件

    我们一般可以在 Xcode 项目中的 Products 文件夹中找到它.

  • Dylib类型

    A dylib is a dynamic library, on other platforms meet, you may know those as DSOs or DLLs.
    dylib 是动态库,在其他平台也叫 DSO 或者 DLL

    对于接触 iOS 开发比较早的同学,可能知道我们在 Xcode 7 之前添加一些比如 sqlite 的库的时候,其后缀名为 dylib,而 Xcode 7 之后后缀名都改成了 tbd

    这里引用 StackoverFlow 上的一篇回答。

    So it appears that the .dylib file is the actual library of binary code that your project is using and is located in the /usr/lib/ directory on the user’s device. The .tbd file, on the other hand, is just a text file that is included in your project and serves as a link to the required .dylib binary. Since this text file is much smaller than the binary library, it makes the SDK’s download size smaller.

    刚刚我们简单介绍了动态库,还有一种库是静态库,他们的区别是什么呢。

    我们先看一下编译的过程:

    compile_process

    当然,这个过程中间其实还设计到编译器前端的 词法分析语法分析语义分析优化 等流程,我们在后面探索 LLVMClang 的时候会详细介绍。

    回到刚才的话题,静态库和动态库的区别:

    Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime.

    静态库和动态库都是编译好的二进制文件,只是用法不同。那为什么要分动态和静态库呢?

    static_link

    dyld_link

    通过上面两幅图我们可以知道:

    • 静态库表现为:在链接阶段会将汇编生成的目标文件和引用的库一起链接打包到可执行文件中。
    • 动态库的表现为:程序编译并不会链接到目标代码中,在程序可执行文件里面会保留对动态库的引用,其中动态库分为动态链接库和动态加载库。
      • 动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。在Linked Framework and Libraries设置的一些share libraries。【随着程序启动而启动】
      • 动态加载库:当需要的时候再使用dlopen等通过代码或者命令的方式来加载。【在程序启动之后】
  • Bundle类型

    Now a bundle’s a special kind of dylib that you cannot link against, all you can do is load it at run time by an dlopen and that’s used on a Mac OS for plug-ins.
    现阶段 Bundle 是一种特殊类型的 dylib,你是无法对其进行链接的。你所能做的是在 Runtime 运行时去通过 dlopen 来加载它,它可以在 macOS 上用于插件。

  • ImageFramework

    Image refers to any of these three types.
    镜像文件包含了上述的三种文件类型

    a framework is a dylib with a special directory structure around it to holds files needed by that dylib.
    有很多东西都叫做 Framework,但在本文中,Framework 指的是一个 dylib,它周围有一个特殊的目录结构来保存该 dylib 所需的文件。

Mach-O结构分析

segment

Mach-O

Mach-O镜像文件是由segments段组成的。

  • 段的名称为大写格式

    所有的段都是 page size 的倍数。

  • arm64 上段大小为 16 字节

  • 其它架构为 4 字节

这里再提及一下虚拟内存和内存页的知识:

具有 VM 机制的操作系统,会对每个运行的进程创建一个逻辑地址空间 logical address space 或者叫虚拟地址空间 virtual address space;该空间的大小由操作系统位数决定:32 位的操作系统,其逻辑地址空间的大小为 4GB,64位的操作系统为 2^34 GB(其计算方式是 2^32 || 2^64(理论上是64位,x86 Intel 是48位))。
vitual_memory

虚拟地址空间(或者逻辑地址空间)会被分为相同大小的块,这些块被称为内存页(page)。计算机处理器和它的内存管理单元(MMU - memory management uinit)维护着一张将程序的逻辑地址空间映射到物理地址上的分页表page table

masOS和早版本的iOS中,分页的大小为4kB。在之后的基于A7A8的系统中,虚拟内存(64位的地址空间)地址空间的分页大小变为了16KB,而物理RAM上的内存分页大小仍然维持在4KB;基于A9及之后的系统,虚拟内存和物理内存的分页都是16KB

section

Mach-O_section

在 segment 段内部还有许多的 section 区。section 名称为小写格式。

But sections are really just a subrange of a segment, they don’t have any of the constraints of being page size, but they are non-overlapping.
但是 sections 节实际上只是一个 segment 段的子范围,它们没有页面大小的任何限制,但是它们是不重叠的。

通过MachOView工具查看App的二进制可执行文件可以看到:

section_show

常见的segments

  • _TEXT:代码段,包括头文件、代码和常量。只读不可修改

    _TEXT_image

  • _DATA:数据段,包括全局变量,静态变量等。可读可写。

    _DATA_image

  • _LINKEDIT:如何加载程序的元数据, 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。

    _LINKEDIT_image

Mach-O Universal Files

Mach-o_UniversalFiles

Mach-O 通用文件,将多种架构的 Mach-O 文件合并而成。它通过 header 来记录不同架构在文件中的偏移量,segement 占多个分页,header占一页的空间。可能有人会觉得 header 单独占一页会浪费空间,但这有利于虚拟内存的实现。

虚拟内存

virtualMemory_image

虚拟内存是一层间接寻址

虚拟内存解决的是管理所有进程使用物理 RAM 的问题。通过添加间接层来让每个进程使用逻辑地址空间,它可以映射到 RAM 上的某个物理页上。这种映射不是一对一的,逻辑地址可能映射不到 RAM 上,也可能有多个逻辑地址映射到同一个物理 RAM 上。

  • 针对第一种情况,当进程要存储逻辑地址内容时会触发 page fault。
  • 而第二种情况就是多进程共享内存。
  • 对于文件可以不用一次性读入整个文件,可以使用分页映射 mmap() 的方式读取。也就是把文件某个片段映射到进程逻辑内存的某个页上。当某个想要读取的页没有在内存中,就会触发 page fault,内核只会读入那一页,实现文件的懒加载。也就是说 Mach-O 文件中的 __TEXT 段可以映射到多个进程,并可以懒加载,且进程之间共享内存。
  • __DATA 段是可读写的。这里使用到了 Copy-On-Write 技术,简称 COW。也就是多个进程共享一页内存空间时,一旦有进程要做写操作,它会先将这页内存内容复制一份出来,然后重新映射逻辑地址到新的 RAM 页上。也就是这个进程自己拥有了那页内存的拷贝。这就涉及到了 clean/dirty page 的概念。dirty page 含有进程自己的信息,而 clean page 可以被内核重新生成(重新读磁盘)。所以 dirty page 的代价大于 clean page。

多进程加载Mach-O镜像

Mach-O_image_loading

  • 所以在多个进程加载 Mach-O 镜像时 __TEXT 和 __LINKEDIT 因为只读,都是可以共享内存的,读取速度就会很快。
  • 而 __DATA 因为可读写,就有可能会产生 dirty page,如果检测到有 clean page 就可以直接使用,反之就需要重新读取 DATA page。一旦产生了 dirty page,当 dyld 执行结束后,__LINKEDIT 需要通知内核当前页面不再需要了,当别人需要的使用时候就可以重新 clean 这些页面。

ASLR

ASLR (Address Space Layout Randomization) 地址空间布局随机化,镜像会在随机的地址上加载。

Code Signing

可能我们认为 Xcode 会把整个文件都做加密 hash 并用做数字签名。其实为了在运行时验证 Mach-O 文件的签名,并不是每次重复读入整个文件,而是把每页内容都生成一个单独的加密散列值,并存储在 __LINKEDIT 中。这使得文件每页的内容都能及时被校验确并保不被篡改。

exec()

exec_image

Exec is a system call. When you trap into the kernel, you basically say I want to replace this process with this new program.

exec() 是一个系统调用。系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(因为使用 ASLR)。并将起始位置到 0x000000 这段范围的进程权限都标记为不可读写不可执行。如果是 32 位进程,这个范围至少是 4KB;对于 64 位进程则至少是 4GB 。NULL 指针引用和指针截断误差都是会被它捕获。这个范围也叫做 PAGEZERO。

dyld

dyld_image

Unix 的前二十年很安逸,因为那时还没有发明动态链接库。有了动态链接库后,一个用于加载链接库的帮助程序被创建。在苹果的平台里是 dyld,其他 Unix 系统也有 ld.so。 当内核完成映射进程的工作后会将名字为 dyld 的 Mach-O 文件映射到进程中的随机地址,它将 PC 寄存器设为 dyld 的地址并运行。dyld 在应用进程中运行的工作是加载应用依赖的所有动态链接库,准备好运行所需的一切,它拥有的权限跟应用一样。

dyld流程

dyld_process_image

  • Load dylibs
    从主执行文件的 header 获取到需要加载的所依赖动态库列表,而 header 早就被内核映射过。然后它需要找到每个 dylib,然后打开文件读取文件起始位置,确保它是 Mach-O 文件。接着会找到代码签名并将其注册到内核。然后在 dylib 文件的每个 segment 上调用 mmap()。应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以 dyld 所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载 100 到 400 个 dylib 文件,但大部分都是系统 dylib,它们会被预先计算和缓存起来,加载速度很快。

  • Fix-ups
    在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是 Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个 dylib 的调用另一个 dylib。这时需要加很多间接层。 现代 code-gen 被叫做动态 PIC(Position Independent Code),意味着代码可以被加载到间接的地址上。当调用发生时,code-gen 实际上会在 __DATA 段中创建一个指向被调用者的指针,然后加载指针并跳转过去。所以 dyld 做的事情就是修正(fix-up)指针和数据。Fix-up 有两种类型,rebasing 和 binding。

  • Rebasing 和 Binding

    Rebasing:在镜像内部调整指针的指向 Binding:将指针指向镜像外部的内容

dyld 的时间线由上图可知为:

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

dyld2 && dyld3

dyld3_image

iOS 13 之前,所有的第三方 App 都是通过 dyld 2 来启动 App 的,主要过程如下:

  • 解析 Mach-OHeaderLoad Commands,找到其依赖的库,并递归找到所有依赖的库
  • 加载 Mach-O 文件
  • 进行符号查找
  • 绑定和变基
  • 运行初始化程序

dyld3 被分为了三个组件:

  • 一个进程外的 MachO 解析器
    • 预先处理了所有可能影响启动速度的 search path、@rpaths 和环境变量
    • 然后分析 Mach-O 的 Header 和依赖,并完成了所有符号查找的工作
    • 最后将这些结果创建成了一个启动闭包
    • 这是一个普通的 daemon 进程,可以使用通常的测试架构
  • 一个进程内的引擎,用来运行启动闭包
    • 这部分在进程中处理
    • 验证启动闭包的安全性,然后映射到 dylib 之中,再跳转到 main 函数
    • 不需要解析 Mach-O 的 Header 和依赖,也不需要符号查找。
  • 一个启动闭包缓存服务
    • 系统 App 的启动闭包被构建在一个 Shared Cache 中, 我们甚至不需要打开一个单独的文件
    • 对于第三方的 App,我们会在 App 安装或者升级的时候构建这个启动闭包。
    • 在 iOS、tvOS、watchOS中,这这一切都是 App 启动之前完成的。在 macOS 上,由于有 Side Load App,进程内引擎会在首次启动的时候启动一个 daemon 进程,之后就可以使用启动闭包启动了。

dyld 3 把很多耗时的查找、计算和 I/O 的事前都预先处理好了,这使得启动速度有了很大的提升。

好了,先导知识就总结到这里,接下来让我们调整呼吸进入下一章~

App加载分析

我们在探索 iOS 底层的时候,对于对象、类、方法有了一定的认知哦,接下来我们就一起来探索一下应用是怎么加载的。

我们直接新建一个 Single View App 的项目,然后在 main.m 中打一个断点:

demoMain_image

可以看到堆栈信息为:

launch_stack_image

可以看到堆栈栈底是_dyld_start,说明App加载是从_dyld_start开始的

_dyld_start

我们在源码里全局搜索_dyld_start。源码下载地址,我这里下载的版本是832.7.3,我们可以来到 dyldStartup.s 这个汇编文件,然后我们聚焦于 arm64 架构下的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#if __arm64__ && !TARGET_OS_SIMULATOR
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp
and sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables
#if __LP64__
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add x2, x28, #16 // get argv into x2
#else
ldr w0, [x28] // get app's mh into x0
ldr w1, [x28, #4] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add w2, w28, #8 // get argv into x2
#endif
adrp x3,___dso_handle@page
add x3,x3,___dso_handle@pageoff // get dyld's mh in to x4
mov x4,sp // x5 has &startGlue

// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
mov x16,x0 // save entry point address in x16
#if __LP64__
ldr x1, [sp]
#else
ldr w1, [sp]
#endif
cmp x1, #0
b.ne Lnew

对于这里的汇编代码,我们肯定也没必要逐行分析,我们直接定位到 bl 语句后面(bl 在汇编层面是跳转的意思):

1
2
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm

看注释可以知道,调用位于 dyldbootstrap 命名空间下的 start 方法,我们继续搜索一下这个 start 方法,结果位于 dyldInitialization.cpp 文件(从文件名我们可以看出该文件主要是用来初始化 dyld),这里查找 start 的时候可能会有很多结果,我们其实可以先搜索命名空间,再搜索 start 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//
// This is code to bootstrap dyld. This work in normally done for a program by dyld and crt.
// In dyld we have to do this manually.
//
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
// dyld 重定向
rebaseDyld(dyldsMachHeader);

// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];

// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;

// set up random value for stack canary
// 栈溢出保护
__guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
// 初始化dyld
runDyldInitializers(argc, argv, envp, apple);
#endif

_subsystem_init(apple);

// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

start方法主要做了很多dyld初始化工作:

  • rebaseDyld dyld重定位
  • __guard_setup 栈溢出保护

在结束dyld初始化工作后,函数调用 dyld::_main() 函数,再将返回值传递给__dyld_start去调用真正的main函数。

dyld::_main

1
2
3
4
5
6
7
8
9
10
11
12
13
//
// Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
///省略代码
}

_main方法 官方的注释如下:

dyld 的入口。内核加载了 dyld 然后跳转到 __dyld_start 来设置一些寄存器的值然后调用到了这个方法。
返回 __dyld_start 所跳转到的目标程序的 main 函数地址。

dyld流程可总结为九个步骤:

  • 第一步:设置运行环境
  • 第二步:加载共享缓存
  • 第三步:实例化主程序
  • 第四步:加载插入的动态库
  • 第五步:链接主程序
  • 第六步:链接插入的动态库
  • 第七步:执行弱引用绑定
  • 第八步:执行初始化方法
  • 第九步:查找程序入口main然后返回
第一步:设置运行环境

这一步主要是设置程序的运行环境运行条件等准备工作,包括环境平台版本路径主机信息,设置程序上下文信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取主程序的hash
mainExecutableCDHash = mainExecutableCDHashBuffer;
// 获取主程序的macho_header结构
sMainExecutableMachHeader = mainExecutableMH;
// 获取主程序的slide值
sMainExecutableSlide = mainExecutableSlide;
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
// 获取主程序路径
sExecPath = _simple_getenv(apple, "executable_path");
// 进程的头环境配置
configureProcessRestrictions(mainExecutableMH, envp);
// 检测环境变量
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
// 获取主机信息 可理解为 程序结构
getHostInfo(mainExecutableMH, mainExecutableSlide);
第二步:加载共享缓存

首先检查dyld共享缓存区是否禁用iOS必须开启,在checkSharedRegionDisable里面iOS环境下注释:

// iOS cannot run without shared region

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 检查缓存共享区域是否开启
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
// 共享缓存加载
mapSharedCache();
// mapSharedCache 函数
static void mapSharedCache()
{
dyld3::SharedCacheOptions opts;
opts.cacheDirOverride = sSharedCacheOverrideDir;
opts.forcePrivate = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);


#if __x86_64__ && !TARGET_OS_SIMULATOR
opts.useHaswell = sHaswell;
#else
opts.useHaswell = false;
#endif
opts.verbose = gLinkContext.verboseMapping;
//--- 加载dyld缓存主函数
loadDyldCache(opts, &sSharedCacheLoadInfo);

// update global state
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
gLinkContext.dyldCache = sSharedCacheLoadInfo.loadAddress;
dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
dyld::gProcessInfo->sharedCacheSlide = sSharedCacheLoadInfo.slide;
dyld::gProcessInfo->sharedCacheBaseAddress = (unsigned long)sSharedCacheLoadInfo.loadAddress;
sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, sSharedCacheLoadInfo.path, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
}
}
// loadDyldCache函数
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->errorMessage = nullptr;

#if TARGET_OS_SIMULATOR
// simulator only supports mmap()ing cache privately into process
return mapCachePrivate(options, results);
#else
if ( options.forcePrivate ) {
// mmap cache into this process only
return mapCachePrivate(options, results);
}
else {
// fast path: when cache is already mapped into shared region
bool hasError = false;
if ( reuseExistingCache(options, results) ) {
hasError = (results->errorMessage != nullptr);
} else {
// slow path: this is first process to load cache
hasError = mapCacheSystemWide(options, results);
}
return hasError;
}
#endif
}

mapSharedCache函数主要调用loadDyldCache 函数,loadDyldCache函数主要有三种方式加载共享缓存:

  • mapCachePrivate() 仅加载到当前进程
  • 共享缓存已经加载过,不做任何处理
  • mapCacheSystemWide() 未加载过,首次加载
第三步:实例化主程序

这一步主要是将主程序的Mach-O加载进内存,并实例化一个ImageLoaderinstantiateFromLoadedImage() 首先调用isCompatibleMachO() 函数检测当前进程的magiccputypecpusubtype 等相关属性,判断Mach-O文件的兼容性,如果兼容性满足,就调用ImageLoaderMachO::instantiateMainExecutable()实例化主程序的ImageLoader

1
2
3
4
// 第三步:实例化主程序
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
1
2
3
4
5
6
7
8
9
10
11
12
13
// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}

throw "main executable not a known format";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}

instantiateMainExecutable() 函数中通过sniffLoadCommands() 判断这个mach-o文件是普通的还是压缩的LINKEDIT,以及它有多少段。
最后根据compressed 是否压缩来实例化最后返回的ImageLoader

第四步:加载插入的动态库
1
2
3
4
5
// 第四步:加载插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}

loadInsertedDylib() 函数中设置了一个LoadContext,并为它配置一些参数后,调用load() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
{
......
// try all path permutations and check against existing loaded images

ImageLoader* image = loadPhase0(path, orgPath, context, cacheIndex, NULL);
if ( image != NULL ) {
CRSetCrashLogMessage2(NULL);
return image;
}

// try all path permutations and try open() until first success
std::vector<const char*> exceptions;
image = loadPhase0(path, orgPath, context, cacheIndex, &exceptions);
#if !TARGET_OS_SIMULATOR
// <rdar://problem/16704628> support symlinks on disk to a path in dyld shared cache
if ( image == NULL)
image = loadPhase2cache(path, orgPath, context, cacheIndex, &exceptions);
#endif
CRSetCrashLogMessage2(NULL);
if ( image != NULL ) {
// <rdar://problem/6916014> leak in dyld during dlopen when using DYLD_ variables
for (std::vector<const char*>::iterator it = exceptions.begin(); it != exceptions.end(); ++it) {
free((void*)(*it));
}
// if loaded image is not from cache, but original path is in cache
// set gSharedCacheOverridden flag to disable some ObjC optimizations
if ( !gSharedCacheOverridden && !image->inSharedCache() && image->isDylib() && dyld3::MachOFile::isSharedCacheEligiblePath(path) && inSharedCache(path) ) {
gSharedCacheOverridden = true;
}
return image;
}
......
}
第五步:链接主程序
1
2
3
// 第五步:链接主程序        Executable:可执行的意思
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH,
true, ImageLoader::RPathChain(NULL, NULL), -1);

调用link()函数将实例化后的主程序进行动态修正,让二进制变为可正常执行的状态。link()函数内部调用了ImgaeLoader::link() 函数,主要做了下面几件事:

  • this->recursiveLoadLibraries() 递归加载依赖库进内存
  • this->recursiveUpdateDepth() 递归更新依赖库的路径
  • this->recursiveRebaseWithAccounting() 递归重定位主程序和依赖库
  • this->recursiveBindWithAccounting() 递归将主程序和依赖库执行符号表绑定(链接动态库使用)
  • this->weakBind() 如果不是正在链接主程序二进制,那就主程序弱符号绑定(链接动态库使用)
  • this->recursiveApplyInterposing() 递归申请可插入依赖库权限
  • this->recursiveMakeDataReadOnly() 递归设置所有信息只读(链接动态库使用)
  • this->recursiveGetDOFSections() 注册DOF节

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
uint64_t t0 = mach_absolute_time();
// 递归加载依赖库进内存
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);

// we only do the loading step for preflights
if ( preflightOnly )
return;

uint64_t t1 = mach_absolute_time();
// 递归更新依赖库的路径
context.clearAllDepths();
this->recursiveUpdateDepth(context.imageCount());

__block uint64_t t2, t3, t4, t5;
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
t2 = mach_absolute_time();
// 递归重定位主程序和依赖库
this->recursiveRebaseWithAccounting(context);
context.notifyBatch(dyld_image_state_rebased, false);

t3 = mach_absolute_time();
if ( !context.linkingMainExecutable )
// 递归将主程序和依赖库执行符号表绑定
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);

t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
// 如果不是正在链接主程序二进制,那就主程序弱符号绑定
this->weakBind(context);
t5 = mach_absolute_time();
}

// interpose any dynamically loaded images
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
// 递归申请可插入依赖库权限
this->recursiveApplyInterposing(context);
}

// now that all fixups are done, make __DATA_CONST segments read-only
if ( !context.linkingMainExecutable )
// 递归设置所有信息只读
this->recursiveMakeDataReadOnly(context);

if ( !context.linkingMainExecutable )
context.notifyBatch(dyld_image_state_bound, false);
uint64_t t6 = mach_absolute_time();

if ( context.registerDOFs != NULL ) {
std::vector<DOFInfo> dofs;
// 注册DOF节
this->recursiveGetDOFSections(context, dofs);
context.registerDOFs(dofs);
}
uint64_t t7 = mach_absolute_time();

// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);

fgTotalLoadLibrariesTime += t1 - t0;
fgTotalRebaseTime += t3 - t2;
fgTotalBindTime += t4 - t3;
fgTotalWeakBindTime += t5 - t4;
fgTotalDOF += t7 - t6;

// done with initial dylib loads
fgNextPIEDylibAddress = 0;
}
第六步:链接插入的动态库

这一步跟链接主程序一样,将sAllImages中的ImageLoader遍历出来,然后调用link()进行链接,需要注意的是,sAllImages中保存的第一个是主程序的镜像,所以要获取所有的动态库的ImageLoader,就必须从i + 1 开始遍历:

1
2
3
4
5
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
第七步:执行主程序弱符号绑定

weakBind()首先通过getCoalescedImages()合并所有动态库的弱符号到一个列表里,然后调用initializeCoalIterator()对需要绑定的弱符号进行排序,接着调用incrementCoalIterator()读取dyld_info_command结构的weak_bind_offweak_bind_size字段,确定弱符号的数据偏移与大小,最终进行弱符号绑定

第八步:执行初始化方法

这一步就开始进行初始化工作了,initializeMainExecutable() 函数中调用 runInitializers() 函数,接着依次调用 processInitializers() 函数进行一些初始化准备工作,接着recursiveInitialization() 函数调用进行初始化工作,接着全局搜索recursiveInitialization(,找到ImageLoader.cpp中的此方法定义,看重点,函数里面我们看到noffitySingle()单个通知注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);

if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
// 先初始化底层依赖库
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}

// record termination order
// 记录终端序号
if ( this->needsTermination() )
context.terminationRecorder(this);

// let objc know we are about to initialize this image
// 单个通知注入 通知告知大家我要开始初始化啦,你们赶紧去做初始化的工作
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

// initialize this image
bool hasInitializers = this->doInitialization(context);

// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);

if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}

recursiveSpinUnLock();
}

探索 _objc_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;

// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}

通过LLDB来断掉调试_objc_init,然后通过bt命令打印出当前的调用堆栈,根据上一节我们探索dyld源码,感觉很清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001002d29c4 libobjc.A.dylib`_objc_init at objc-os.mm:925:9
frame #1: 0x000000010044f0bc libdispatch.dylib`_os_object_init + 13
frame #2: 0x000000010045fafc libdispatch.dylib`libdispatch_init + 282
frame #3: 0x00007fff6a99c791 libSystem.B.dylib`libSystem_initializer + 220
frame #4: 0x000000010002f1d3 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 535
frame #5: 0x000000010002f5de dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #6: 0x0000000100029ffb dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 493
frame #7: 0x0000000100029f66 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
frame #8: 0x00000001000280b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #9: 0x0000000100028154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #10: 0x0000000100016662 dyld`dyld::initializeMainExecutable() + 129
frame #11: 0x000000010001bbba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
frame #12: 0x0000000100015227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
frame #13: 0x0000000100015025 dyld`_dyld_start + 37

我们可以看到 dyld 的最后一个流程是 doModInitFunctions 方法的执行。

我们打开 libSystem 的源码,全局搜索 libSystem_initializer 可以看到:

1
2
3
4
5
6
7
8
9
10

_dyld_initializer();
_libSystem_ktrace_init_func(DYLD);

libdispatch_init();
_libSystem_ktrace_init_func(LIBDISPATCH);

#if !TARGET_OS_DRIVERKIT
_libxpc_initializer();
_libSystem_ktrace_init_func(LIBXPC);

然后我们打开 libDispatch 的源码,全局搜索 libdispatch_init 可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
	void
libdispatch_init(void)
{
//省略代码
_dispatch_hw_config_init();
_dispatch_time_init();
_dispatch_vtable_init();
//调用
_os_object_init();
_voucher_init();
_dispatch_introspection_init();
}

我们再搜索_os_object_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
_os_object_init(void)
{
_objc_init();
Block_callbacks_RR callbacks = {
sizeof(Block_callbacks_RR),
(void (*)(const void *))&objc_retain,
(void (*)(const void *))&objc_release,
(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}

完美~,_objc_init 在这里就被调用了。所以 _objc_init 的流程是

dyld -> libSystem -> libDispatch -> libObjc -> _objc_init

总结

本文主要探索了 app 启动之后 dyld 的流程,整个分析过程确实比较复杂,但在探索的过程中,我们不仅对底层源码有了新的认知,同时对于优化我们 app 启动也是有很多好处的。下一章,我们会对 objc_init 内部的 map_images 和 load_images 进行更深入的分析。

static_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors,
* so we have to do it ourselves.
**********************************************************************/
static void static_init()
{
size_t count;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
inits[i]();
}
auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
UnsignedInitializer init(offsets[i]);
init();
}
}

这里会运行 C++ 的静态构造函数,在 dyld 调用我们的静态构造函数之前,libc 会调用 _objc_init,所以这里我们必须自己来做,并且这里只会初始化系统内置的 C++ 静态构造函数,我们自己代码里面写的并不会在这里初始化。

通过其注释,我们大概知道static_init函数的作用是运行C++的静态构造函数。其原因在于dyld调用我们的静态构造函数晚于libc调用_objc_init函数。

继续深入讲解static_init方法,可以看出,getLibobjcInitializers方法是它的实现主体,点击进入可以看到如下实现:

1
GETSECT(getLibobjcInitializers,       UnsignedInitializer, "__objc_init_func");

这就能理解我们文章开头提出的static_init()方法的含义了。其实就是找出__objc_init_func区的数据,获取了Initializer指针,然后按顺序调用。

由于全局变量都在 mod_init_func 这个区中,那这就给我们优化 App 启动提供了一个思路:我们可以通过 hook 这个区中所有的函数为自己的函数,并在我们的函数中添加时间节点来计时,从而了解在 main 函数之前的耗时情况,这也可以为我们 APP 提供优化借鉴。

其想法很简单,就是在 load 方法中 hook __mod_init_func 的方法。部分源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (void)load{

sInitInfos = [NSMutableArray new];

g_initializer = new std::vector<MemoryType>();

g_cur_index = -1;

g_aslr = 0;



hookModInitFunc();

}

以及

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static void hookModInitFunc(){

Dl_info info;

dladdr((const void *)hookModInitFunc, &info);



#ifndef __LP64__

// const struct mach_header *mhp = _dyld_get_image_header(0); // both works as below line

const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;

unsigned long size = 0;

MemoryType *memory = (uint32_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);

#else /* defined(__LP64__) */

const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;

unsigned long size = 0;

MemoryType *memory = (uint64_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);

#endif /* defined(__LP64__) */

for(int idx = 0; idx < size/sizeof(void*); ++idx){

MemoryType original_ptr = memory[idx];

//保存原先的方法指针

g_initializer->push_back(original_ptr);

//将原先的方法替换为我们自己的方法

memory[idx] = (MemoryType)myInitFunc_Initializer;

}



NSLog(@"zero mod init func : size = %@",@(size));



[sInitInfos addObject:[NSString stringWithFormat:@"ASLR=%p",mhp]];

g_aslr = (MemoryType)mhp;

}

最后是展示方法以及消耗时长:

1
2
3
4
5
6
7
8
9
10
11
12
13
void myInitFunc_Initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct MyProgramVars* vars){
++g_cur_index;
OriginalInitializer func = (OriginalInitializer)g_initializer->at(g_cur_index);

CFTimeInterval start = CFAbsoluteTimeGetCurrent();

func(argc,argv,envp,apple,vars);

CFTimeInterval end = CFAbsoluteTimeGetCurrent();
sSumInitTime += 1000.0 * (end-start);
NSString *cost = [NSString stringWithFormat:@"%p=%@",func,@(1000.0*(end - start))];
[sInitInfos addObject:cost];
}

gdb_objc_realized_classes

gdb_objc_realized_classes 的作用已经很明显了,即是对所有的类进行缓存:从对应的 section 中读取所有的类,取出来后以 mangledName 作为键,以 class 结构体作为值。

remapped_class_map

1
2
3
4
5
6
7
8
9
10
11
12
13
/***********************************************************************
* remappedClasses
* Returns the oldClass => newClass map for realized future classes.
* Returns the oldClass => nil map for ignored weak-linked classes.
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static objc::DenseMap<Class, Class> *remappedClasses(bool create)
{
static objc::LazyInitDenseMap<Class, Class> remapped_class_map;
runtimeLock.assertLocked();
// start big enough to hold CF's classes and a few others
return remapped_class_map.get(create, 32);
}

remap class,字面意思是 重新映射 class,那肯定有一个映射者和映射结果。map 的键是 cls,也就是 section 中拿到的 cls,而 value 就是我们 remap 的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->nonlazyMangledName();

if (missingWeakSuperclass(cls)) {
// No superclass (probably weak-linked).
// Disavow any knowledge of this subclass.
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING class '%s' with "
"missing weak-linked superclass",
cls->nameForLogging());
}
addRemappedClass(cls, nil);
cls->setSuperclass(nil);
return nil;
}

cls->fixupBackwardDeployingStableSwift();

Class replacing = nil;
if (mangledName != nullptr) {
if (Class newCls = popFutureNamedClass(mangledName)) {
// This name was previously allocated as a future class.
// Copy objc_class to future class's struct.
// Preserve future's rw data block.

if (newCls->isAnySwift()) {
_objc_fatal("Can't complete future class request for '%s' "
"because the real class is too big.",
cls->nameForLogging());
}

class_rw_t *rw = newCls->data();
const class_ro_t *old_ro = rw->ro();
memcpy(newCls, cls, sizeof(objc_class));

// Manually set address-discriminated ptrauthed fields
// so that newCls gets the correct signatures.
newCls->setSuperclass(cls->getSuperclass());
newCls->initIsa(cls->getIsa());

rw->set_ro((class_ro_t *)newCls->data());
newCls->setData(rw);
freeIfMutable((char *)old_ro->getName());
free((void *)old_ro);

addRemappedClass(cls, newCls);

replacing = cls;
cls = newCls;
}
}

if (headerIsPreoptimized && !replacing) {
// class list built in shared cache
// fixme strict assert doesn't work because of duplicates
// ASSERT(cls == getClass(name));
ASSERT(mangledName == nullptr || getClassExceptSomeSwift(mangledName));
} else {
if (mangledName) { //some Swift generic classes can lazily generate their names
addNamedClass(cls, mangledName, replacing);
} else {
Class meta = cls->ISA();
const class_ro_t *metaRO = meta->bits.safe_ro();
ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");
ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");
}
addClassTableEntry(cls);
}

// for future reference: shared cache never contains MH_BUNDLEs
if (headerIsBundle) {
cls->data()->flags |= RO_FROM_BUNDLE;
cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
}

return cls;
}

通过这个函数我们发现,有两个分支有机会进入方法 addRemappedClass,一个是 missingWeakSuperclass 方法是否为真,另外一个是 popFutureNamedClass(mangledName) 方法返回是否为真。这里会有两个概念

  1. WeakSuperclass
  2. FutureNamedClass

而这两个条件为true 的情况就是需要 remap 的情况。

missingWeakSuperclass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/***********************************************************************
* missingWeakSuperclass
* Return YES if some superclass of cls was weak-linked and is missing.
**********************************************************************/
static bool
missingWeakSuperclass(Class cls)
{
ASSERT(!cls->isRealized());

if (!cls->getSuperclass()) {
// superclass nil. This is normal for root classes only.
return (!(cls->data()->flags & RO_ROOT));
} else {
// superclass not nil. Check if a higher superclass is missing.
Class supercls = remapClass(cls->getSuperclass());
ASSERT(cls != cls->getSuperclass());
ASSERT(cls != supercls);
if (!supercls) return YES;
if (supercls->isRealized()) return NO;
return missingWeakSuperclass(supercls);
}
}

代码的注释已经一目了然:该函数就是判断某个 class 的父类是否是 weak-linked 并且缺失了。那 什么叫做 weak-linked?比如 NSObject 是否是 weak-linbked ?很显然不是的。那么什么是 weak-linked?

weak-linked
我们开发的时候,都会使用最新的SDK,但是为了让老的设备可以下载并运行我们的应用,就要将Deployment Target设置成之前系统的版本号。例如我们应用使用iOS 8.1的SDK,Deployment Target设置成iOS 5.1.1,虽然我们开发的时候使用的是8.1的SDK,但是程序运行在的设备中却可能是6.0 or 7.0的SDK上,按照苹果的说法,如果我们应用使用了最新SDK引入的特性,比如符号、函数等,那么在版本较旧的设备上就运行不了。下面是苹果官方文档的一段话:
Normally, if an application uses a new feature in a framework, it is unable to run on earlier versions of the framework that do not support that feature. Such applications would either fail to launch or crash when an attempt to use the feature was made.
那么为什么我们使用最新的SDK开发的应用却可以运行在旧的系统中呢?答案是使用了弱引用。资料里面说过,我们自己创建的framework,如果需要做版本兼容,那么就要对今后加入的符号等使用弱引用,使用了弱引用之后,即使在版本较旧的环境下跑,也可以运行,只是相应的符号是NULL,下面就是教我们怎样定义弱引用。有一点需要说明的是,如果一个framework没有为新加入的符号加入弱引用,那也不必担心,我们只要在链接时弱引用整个framework就好,方法就是链接的时候使用 -weak_framework frameworkName

一个使用 weak 的实例为:

1
2
3
4
5
6
7
8
9
10
11
extern int MyWeakLinkedFunction() __attribute__((weak_import));
int main()
{
int result = 0;
if (MyWeakLinkedFunction != NULL)
{
result = MyWeakLinkedFunction();
}
return result;

}

以上代码片段来自苹果官网。

这么一来, missingWeakSuperclass 的作用的作用就不言而喻了:查看 cls 类的祖宗类中是否有类是 weak-linked 的,并且已经 missing,祖宗类里有 missing weak-linked 的,则 cls 的所有信息也是不可信的,所以将其添加到重映射表里,映射为nil,即 cls -> nil。

future_named_class_map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/***********************************************************************
* futureNamedClasses
* Returns the classname => future class map for unrealized future classes.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static NXMapTable *future_named_class_map = nil;
static NXMapTable *futureNamedClasses()
{
runtimeLock.assertLocked();
if (future_named_class_map) return future_named_class_map;
// future_named_class_map is big enough for CF's classes and a few others
future_named_class_map =
NXCreateMapTable(NXStrValueMapPrototype, 32);
return future_named_class_map;
}

上面代码是创建代码,下面我们看一下使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 
* Used by CoreFoundation's toll-free bridging.
* Return the id of the named class.
*
* @return The id of the named class, or an uninitialized class
* structure that will be used for the class when and if it does
* get loaded.
*
* @warning Do not call this function yourself.
*/
OBJC_EXPORT Class _Nonnull
objc_getFutureClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0)
OBJC_ARC_UNAVAILABLE;

看注释有个警告,不要主动调用这个函数。下面是具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/***********************************************************************
* objc_getFutureClass. Return the id of the named class.
* If the class does not exist, return an uninitialized class
* structure that will be used for the class when and if it
* does get loaded.
* Not thread safe.
**********************************************************************/
Class objc_getFutureClass(const char *name)
{
Class cls;
// YES unconnected, NO class handler
// (unconnected is OK because it will someday be the real class)
cls = look_up_class(name, YES, NO);
if (cls) {
if (PrintFuture) {
_objc_inform("FUTURE: found %p already in use for %s",
(void*)cls, name);
}

return cls;
}
// No class or future class with that name yet. Make one.
// fixme not thread-safe with respect to
// simultaneous library load or getFutureClass.
return _objc_allocateFutureClass(name);
}

其中函数 look_up_class 这里先不多做介绍了,里面逻辑较多,而且不是这个函数的重点,重点是最后一句:_objc_allocateFutureClass(name) 它才是从 hash map 中获取对应值的函数,其实现如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/***********************************************************************
* _objc_allocateFutureClass
* Allocate an unresolved future class for the given class name.
* Returns any existing allocation if one was already made.
* Assumes the named class doesn't exist yet.
* Locking: acquires runtimeLock
**********************************************************************/
Class _objc_allocateFutureClass(const char *name)
{
mutex_locker_t lock(runtimeLock);
Class cls;
NXMapTable *map = futureNamedClasses();

if ((cls = (Class)NXMapGet(map, name))) {
// Already have a future class for this name.
return cls;
}
cls = _calloc_class(sizeof(objc_class));
addFutureNamedClass(name, cls);
return cls;
}

很容易理解:有的话就通过方法 NXMapGet 取出来,没有的话则创建。

我们再看一下一出代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/***********************************************************************
* popFutureNamedClass
* Removes the named class from the unrealized future class list,
* because it has been realized.
* Returns nil if the name is not used by a future class.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static Class popFutureNamedClass(const char *name)
{
runtimeLock.assertLocked();

Class cls = nil;

if (future_named_class_map) {
cls = (Class)NXMapKeyFreeingRemove(future_named_class_map, name);
if (cls && NXCountMapTable(future_named_class_map) == 0) {
NXFreeMapTable(future_named_class_map);
future_named_class_map = nil;
}
}

return cls;
}

这个方法有点熟悉了,正是上文介绍的 remap 的条件之一。上一篇文章讲述的是第一个条件,本文讲的是第二个条件。这两个条件无论哪一个符合都会调用方法:addRemappedClass,即向 remapped_class_map 中插入数据

总结

future named class 的介绍就到这里了,希望大家有所收获。其实笔者看来,不管是上一篇文章的 remapped_class_map 还是本文的 future_named_class_map 里面的数据都是空。remapped_class_map 有数据是需要一定条件,而 future_named_class_map 有数据也是需要先 add 的,所以大家对于这两个 map 只需要有个大概的概念就好,后面如果真的碰到他们有数据,我们在详细分析。

懒加载类其实就是指类的加载在第一次消息发送之前,但是如果我们在类中实现了+load方法,那么类的加载就会提前到pre-main之前,提前加载的类就称之为非懒加载类。

类的加载

realizeClassWithoutSwift我们先看其源码实现,因为我们这里主要探究的是加载,其中加载具体做的事情不做过多说明,把部分源码进行了省略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
runtimeLock.assertLocked();
class_rw_t *rw;//读写区,运行期间进行
Class supercls;
Class metacls;

if (!cls) return nil;
if (cls->isRealized()) {
validateAlreadyRealizedClass(cls);
return cls;
}
ASSERT(cls == remapClass(cls));

// fixme verify class is not in an un-dlopened part of the shared cache?

auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro();
ASSERT(!isMeta);
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
}

cls->cache.initializeToEmptyOrPreoptimizedInDisguise();

#if FAST_CACHE_META
if (isMeta) cls->cache.setBit(FAST_CACHE_META);
#endif

// Choose an index for this class.
// Sets cls->instancesRequireRawIsa if indexes no more indexes are available
cls->chooseClassArrayIndex();

if (PrintConnecting) {
_objc_inform("CLASS: realizing class '%s'%s %p %p #%u %s%s",
cls->nameForLogging(), isMeta ? " (meta)" : "",
(void*)cls, ro, cls->classArrayIndex(),
cls->isSwiftStable() ? "(swift)" : "",
cls->isSwiftLegacy() ? "(pre-stable swift)" : "");
}

// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
// This assumes that none of those classes have Swift contents,
// or that Swift's initializers have already been called.
// fixme that assumption will be wrong if we add support
// for ObjC subclasses of Swift classes.
//父类加载
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
//元类也加载了
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

#if SUPPORT_NONPOINTER_ISA
if (isMeta) {
// Metaclasses do not need any features from non pointer ISA
// This allows for a faspath for classes in objc_retain/objc_release.
cls->setInstancesRequireRawIsa();
} else {
// Disable non-pointer isa for some classes and/or platforms.
// Set instancesRequireRawIsa.
bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
bool rawIsaIsInherited = false;
static bool hackedDispatch = false;

if (DisableNonpointerIsa) {
// Non-pointer isa disabled by environment or app SDK version
instancesRequireRawIsa = true;
}
else if (!hackedDispatch && 0 == strcmp(ro->getName(), "OS_object"))
{
// hack for libdispatch et al - isa also acts as vtable pointer
hackedDispatch = true;
instancesRequireRawIsa = true;
}
else if (supercls && supercls->getSuperclass() &&
supercls->instancesRequireRawIsa())
{
// This is also propagated by addSubclass()
// but nonpointer isa setup needs it earlier.
// Special case: instancesRequireRawIsa does not propagate
// from root class to root metaclass
instancesRequireRawIsa = true;
rawIsaIsInherited = true;
}

if (instancesRequireRawIsa) {
cls->setInstancesRequireRawIsaRecursively(rawIsaIsInherited);
}
}
// SUPPORT_NONPOINTER_ISA
#endif

// Update superclass and metaclass in case of remapping
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);

// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);

// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);

// Copy some flags from ro to rw
if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}

// Propagate the associated objects forbidden flag from ro or from
// the superclass.
if ((ro->flags & RO_FORBIDS_ASSOCIATED_OBJECTS) ||
(supercls && supercls->forbidsAssociatedObjects()))
{
rw->flags |= RW_FORBIDS_ASSOCIATED_OBJECTS;
}

// Connect this class to its superclass's subclass lists
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}

// Attach categories
methodizeClass(cls, previously);

return cls;
}

methodizeClass

关注到methodizeClass(cls,previously)方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static void methodizeClass(Class cls, Class previously)
{
runtimeLock.assertLocked();

bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro();
auto rwe = rw->ext();

// Methodizing for the first time
if (PrintConnecting) {
_objc_inform("CLASS: methodizing class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}

// Install methods and properties that the class implements itself.
// 方法列表
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
if (rwe) rwe->methods.attachLists(&list, 1);
}
// 属性列表
property_list_t *proplist = ro->baseProperties;
if (rwe && proplist) {
rwe->properties.attachLists(&proplist, 1);
}
// 协议列表
protocol_list_t *protolist = ro->baseProtocols;
if (rwe && protolist) {
rwe->protocols.attachLists(&protolist, 1);
}

// Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);
}

// Attach categories.
if (previously) {
if (isMeta) {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_METACLASS);
} else {
// When a class relocates, categories with class methods
// may be registered on the class itself rather than on
// the metaclass. Tell attachToClass to look for those.
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_CLASS_AND_METACLASS);
}
}
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

#if DEBUG
// Debug: sanity-check all SELs; log method list contents
for (const auto& meth : rw->methods()) {
if (PrintConnecting) {
_objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-',
cls->nameForLogging(), sel_getName(meth.name()));
}
ASSERT(sel_registerName(sel_getName(meth.name())) == meth.name());
}
#endif
}

由此可得出以下结论:

  • 虽然实现类调用的methodizeClass->attachToClass后没调用attachCategories方法,但是load_images->loadAllCategories后会调用attachCategories方法。
  • attachCategories方法中进行了rwe初始化。

根据方法的注释我们可以解读出以下信息:

  • 对类cls执行首次初始化
  • 包括分配读写数据。
  • 不执行任何Swift侧初始化。
  • 返回类的实际类结构。
  • 锁定:runtimeLock必须由调用者写锁

对方法的实现的一些概念进行解读
ro:干净内存(Clean Memory),存放的是类的原始数据
rw:脏内存(Dirty Memory) ,运行时会对类内存进行动态的修改所以才有rw,rw最初是从ro中读取的数据。
rwe:新增内容,运行时动态修改类才会生成rwe,rwe的原始数据是从rw中读取的。
supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);沿着继承链递归调用realizeClassWithoutSwift。
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);沿着isa走位递归调用realizeClassWithoutSwift。
所以如果一个类加载了,其继承链上的父类、isa对应的元类等都会加载。

懒加载类

我们实现了+load方法类的加载就会提前,+load是如何影响类的加载的时机的呢?
load_images源码中有说明在dyld映射的镜像中处理+load,我们需要去看看load_images中是如何处理+load方法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}

// Return without taking locks if there are no +load methods here.
//如果没有+load方法 则返回 不带锁
if (!hasLoadMethods((const headerType *)mh)) return;
//加递归锁
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}

// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}

hasLoadMethods:判断的条件有两个:

  • _getObjc2NonlazyClassList:获取所有类中的Load方法数量
  • _getObjc2NonlazyCategoryList:获取所有分类中的Load方法数量
1
2
3
4
5
6
7
8
// Quick scan for +load methods that doesn't take a lock.
bool hasLoadMethods(const headerType *mhdr)
{
size_t count;
if (_getObjc2NonlazyClassList(mhdr, &count) && count > 0) return true;
if (_getObjc2NonlazyCategoryList(mhdr, &count) && count > 0) return true;
return false;
}

如果存在+load方法,后面加锁后开始进入prepare_load_methods函数,它的作用是发现所有的+load方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
//获取非懒加载类
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
//循环遍历加载非懒加载类的load方法到loadable_classes
schedule_class_load(remapClass(classlist[i]));
}
//获取非懒加载分类列表
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
//如果类没有初始化就去初始化
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}

prepare_load_methods中分为两部分:
1.获取非懒加载类列表,猜测这里应该已经加载了对应的类,循环遍历加载非懒加载类的load方法到loadable_classes.其中关键的方法schedule_class_load、add_class_to_loadable_list。
2.获取非懒加载分类列表,循环遍历去加载非懒加载分类的 load 方法到 loadable_categories。
其中关键的方法add_category_to_loadable_list。
非懒加载分类遍历时,有一个处理realizeClassWithoutSwift(cls, nil),在遍历加载非懒加载类的load方法时,会调用realizeClassWithoutSwift,如果分类对应的类没有记载,在这里就会被加载。

懒加载类

对于懒加载类,是在第一次消息发送objc_msgSend,调用lookUpImpOrForward

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.

// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.

runtimeLock.lock();

// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
checkIsKnownClass(cls);
//关注这里
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;

// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().

for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}

if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}

// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}

// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}

// No implementation found. Try method resolver once.

if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}

done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
runtimeLock.assertLocked();
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}

if (slowpath(initialize && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again

// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
return cls;
}
static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();

if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
// Non-Swift class. Realize it now with the lock still held.
// fixme wrong in the future for objc subclasses of swift classes
realizeClassWithoutSwift(cls, nil);
if (!leaveLocked) lock.unlock();
} else {
// Swift class. We need to drop locks and call the Swift
// runtime to initialize it.
lock.unlock();
cls = realizeSwiftClass(cls);
ASSERT(cls->isRealized()); // callback must have provoked realization
if (leaveLocked) lock.lock();
}

return cls;
}

我们可以清晰的看到lookUpImpOrForward中也会调用到realizeClassWithoutSwift,对类进行加载。

总结

懒加载类情况 类加载延迟到第一次消息发送。
lookUpImOrForward
realizeClassMaybeSwiftMaybeRelock
relizeClassWithoutSwift
methodizeClass

非懒记载类调用了+load方法,类就会提前加载。
getObjc2NonlazyClassList
readClass
realizeClassWithoutSwift
methodizeClass

前文我们探索了iOS类的底层原理,简单了解了四个重要的属性。这篇文章主要探索第三个属性cache_t,对于这个属性,我们可以学习到苹果对于缓存的设计和理解。

探索cache_t

从数据结构开始

cache_t的基本结构

我们看一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct cache_t {
//省略代码
public:
// The following four fields are public for objcdt's use only.
// objcdt reaches into fields while the process is suspended
// hence doesn't care for locks and pesky little details like this
// and can safely use these.
unsigned capacity() const;
struct bucket_t *buckets() const;//重点
Class cls() const;

#if CONFIG_USE_PREOPT_CACHES
const preopt_cache_t *preopt_cache() const;
#endif

mask_t occupied() const;
void initializeToEmpty();
//省略代码
}

发现有一个buckets()成员,它是一个结构体bucket_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
// Compute the ptrauth signing modifier from &_imp, newSel, and cls.
uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
}
// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
return (uintptr_t)
ptrauth_auth_and_resign(newImp,
ptrauth_key_function_pointer, 0,
ptrauth_key_process_dependent_code,
modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
}

public:
static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
inline SEL sel() const { return _sel.load(memory_order_relaxed); }

#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
return (IMP)imp;
}

inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}

template <Atomicity, IMPEncoding>
void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

从源码定义中不难看出,bucket_t 其实缓存的是方法实现 IMP。这里有一个注意点,就是 IMP-firstSEL-first

IMP-first is better for arm64e ptrauth and no worse for arm64.

  • IMP-first 对 arm64e 的效果更好,对 arm64 不会有坏的影响。

SEL-first is better for armv7* and i386 and x86_64.

  • SEL-first 适用于 armv7 * 和 i386 和 x86_64。

通过上面的源码,我们大致了解了 bucket_t 类型的结构,那么现在问题来了,类中的 cache 是在什么时候以什么样的方式来进行缓存的呢?我们使用LLDB查看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
lldb) x pClass
0x100008220: f8 81 00 00 01 00 00 00 40 71 35 00 01 00 00 00 ........@q5.....
0x100008230: 10 d4 22 01 01 00 00 00 03 00 00 00 24 80 01 00 ..".........$...
(lldb) p (cache_t *)0x100008230 //0x100008220 + 16字节(isa 8字节 + superclass 8字节)
(cache_t *) $2 = 0x0000000100008230
(lldb) p *$2
(cache_t) $3 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4314027024
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 3
}
}
_flags = 32804
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001802400000003
}
}
}
}
(lldb) p $3.buckets()
(bucket_t *) $4 = 0x000000010122d410
(lldb) p *$4
(bucket_t) $5 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 3365936
}
}
}

_occupied 应该是表示当前已经占用了多少缓存(每调用一个实例方法会+1)。下面验证一下

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
Class pClass = object_getClass(p);
[p sayHello];
[p sayOk];//断点打在这

}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(lldb) x pClass
0x100008240: 18 82 00 00 01 00 00 00 40 71 35 00 01 00 00 00 ........@q5.....
0x100008250: 00 dc 74 00 01 00 00 00 03 00 00 00 24 80 02 00 ..t.........$...
(lldb) p (cache_t *)0x100008250
(cache_t *) $1 = 0x0000000100008250
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4302625792
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 3//注意
}
}
_flags = 32804
_occupied = 2
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0002802400000003
}
}
}
}

可以看到_occupied = 2验证了我们的猜想,我们继续走断点发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
Class pClass = object_getClass(p);
[p sayHello];
[p sayOk];
//断点在最后一步
}
return 0;
}

(lldb) p *$1
(cache_t) $3 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4305857472
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7//注意
}
}
_flags = 32804
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001802400000007
}
}
}
}

我们发现_occupied=1,_maybeMaskValue=7(上一步=3),这是为什么

如果读者了解并掌握散列表这种数据结构的话,相信已经看出端倪了。是的,这里其实就是用到了 开放寻址法 来解决散列冲突(哈希冲突)。

关于哈希冲突,可以借助鸽笼理论,即把 11 只鸽子放进 10 个抽屉里面,肯定会有一个抽屉里面有 2 只鸽子。是不是理解起来很简单?

通过上面的测试,我们明确了方法缓存使用的是哈希表存储,并且为了解决无法避免的哈希冲突使用的是开放寻址法,而开放寻址法必然要在合适的时机进行扩容,这个时机肯定不是会在数据已经装满的时候,我们可以进源码探索一下,我们快速定位到 cache_t 的源码处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
///省略代码
ASSERT(sel != 0 && cls()->isInitialized());

// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
///取当前占用的空间大小
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
///分配空间
reallocate(oldCapacity, capacity, true);
}
///省略代码
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
// When we have a cache end marker it fills a bucket slot, so having a
// initial cache size of 2 buckets would not be efficient when one of the
// slots is always filled with the end marker. So start with a cache size
// 4 buckets.
INIT_CACHE_SIZE_LOG2 = 2,
#else
// Allow an initial bucket size of 2 buckets, since a large number of
// classes, especially metaclasses, have very few imps, and we support
// the ability to fill 100% of the cache before resizing.
INIT_CACHE_SIZE_LOG2 = 1,
#endif
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),

看上面代码可以看出,换出的初始大小为4(metaclasses这种有很少imps为2),最后调用reallocate函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);

// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this

ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

setBucketsAndMask(newBuckets, newCapacity - 1);

if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
// objc_msgSend uses mask and buckets with no locks.
// It is safe for objc_msgSend to see new buckets but old mask.
// (It will get a cache miss but not overrun the buckets' bounds).
// It is unsafe for objc_msgSend to see old buckets and new mask.
// Therefore we write new buckets, wait a lot, then write new mask.
// objc_msgSend reads mask first, then buckets.

#ifdef __arm__
// ensure other threads see buckets contents before buckets pointer
mega_barrier();

_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);

// ensure other threads see new buckets before new mask
mega_barrier();

_maybeMask.store(newMask, memory_order_relaxed);
_occupied = 0;
#elif __x86_64__ || i386
// ensure other threads see buckets contents before buckets pointer
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);

// ensure other threads see new buckets before new mask
_maybeMask.store(newMask, memory_order_release);
_occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}

可以看到_bucketsAndMaybeMask里的newBucketsnewCapacity - 1,我们看capacity()方法也能验证这一点

1
2
3
4
unsigned cache_t::capacity() const
{
return mask() ? mask()+1 : 0;
}

继续探索cache_t

通过前面的探索,我们知道了 cache_t 实质上是缓存了我们类的实例方法,那么对于类方法来说,自然就是缓存在了元类上了。这一点我相信读者应该都能理解。

方法缓存策略

直观的感受就是会在insert的时候缓存,我们继续看insert方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();//加锁
// Never cache before +initialize is done
if (slowpath(!cls()->isInitialized())) {//initialize调用之后才会缓存
return;
}
if (isConstantOptimizedCache()) {//内联函数 return false 所以if里不会执行
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif

ASSERT(sel != 0 && cls()->isInitialized());

// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}

bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;

// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));

bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

方法缓存是否有序

方法缓存是无序的,这是因为计算缓存下标是一个哈希算法:

1
2
3
4
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
return (mask_t)(key & mask);
}

通过 cache_hash 之后计算出来的下标并不是有序的,下标值取决于 keymask 的值。

bucket 与 mask, capacity, sel, imp 的关系

一个类有一个属性 cache_t,而一个 cache_tbuckets 会有多个 bucket。一个 bucket 存储的是 impcache_key_t

mask 的值对于 bucket 来说,主要是用来在缓存查找时的哈希算法。
capacity 则可以获取到 cache_tbucket 的数量。

sel 在缓存的时候是被强转成了 cache_key_t 的形式,更方便查询使用。
imp 则是函数指针,也就是方法的具体实现,缓存的主要目的就是通过一系列策略让编译器更快的执行消息发送的逻辑。

总结

  • OC 中实例方法缓存在类上面,类方法缓存在元类上面。
  • cache_t 缓存会提前进行扩容防止溢出。
  • 方法缓存是为了最大化的提高程序的执行效率。
  • 苹果在方法缓存这里用的是开放寻址法来解决哈希冲突。
  • 通过 cache_t 我们可以进一步延伸去探究 objc_msgSend,因为查找方法缓存是属于 objc_msgSend 查找方法实现的快速流程。

class的本质

类是我们面向对象开发中使用很频繁的概念,我们看一下类的结构是什么。

首先我们定义一个类Person

1
2
3
4
5
6
7
8
9
10
11
12
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@end

NS_ASSUME_NONNULL_END

// Animal.m
@implementation Person

@end

我们在main函数里初始化Person对象

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *obj = [[Person alloc] init];
NSLog(@"%p",obj);
}
return 0;
}

在terminal里执行clang命令

clang -rewrite-objc main.m -o main.cpp

会生成main.cpp文件,在main.cpp文件里搜索Person找到下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
//1
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif
//2
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};


/* @end */

#pragma clang assume_nonnull end



// @implementation Person

// @end

int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//3 objc_getClass("Person")
Person *obj = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders__s_l_p47fxn36j8d2_n75spyb7c0000gn_T_main_971c67_mi_0,obj);
}
return 0;
}

我们主要看代码注释1、2、3的位置,全局搜索typedef struct objc_object,发现有845个结果,结果数有点多,我们再全局搜索*Class,这次比较幸运,只有7个结果,我们可以快速定位到其中的一行

typedef struct objc_class *Class;

看到这里我们就知道了Class 类型的实际是一个叫objc_class的结构体。我们去objc源码里搜索struct objc_class

1
2
3
4
5
6
7
8
9
10
11
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
///省略后面的set get函数
}

看源码可以知道objc_class继承自objc_object,说明Class其实也是一个对象。

类的结构

看上面这段源码,我们知道类的本质也是对象,我们日常开发中常见的成员变量、属性、方法、协议等都在类里面存在的,我们接下来探索一下这些内容在雷利是怎么存储的。

从上一小节最后的源码我们看到,类中的四个属性:

  • isa指针
  • superclass指针
  • cache
  • bits

isa指针是隐藏属性(继承自objc_object)。

isa指针

我们之前已经探索过,在对象初始化的时候,通过isa可以让对象和类关联,类中isa的作用也类似,关联类和元类。

superclass指针

这个看名字就很好理解,指向了父类,一般来说,类的根父类都是NSObject类。根元类的父类也是NSObject类。

cache

cache_t里的东西比较多,我们暂时先了解这个是缓存,里面会有方法的缓存。具体内容后面再探究。

bits属性

bits 的数据结构类型是 class_data_bits_t,同时也是一个结构体类型。而我们阅读 objc_class 源码的时候,会发现很多地方都有 bits 的身影,比如:

1
2
3
4
5
6
7
8
9
10
11
12
class_rw_t *data() { 
return bits.data();
}

bool hasCustomRR() {
return ! bits.hasDefaultRR();
}

bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}

这里值得我们注意的是,objc_classdata() 方法其实是返回的 bitsdata() 方法,而通过这个 data() 方法,我们发现诸如类的字节对齐、ARC、元类等特性都有 data() 的出现,这间接说明 bits 属性其实是个大容器,有关于内存管理、C++ 析构等内容在其中有定义。

这里我们会遇到一个十分重要的知识点: class_rw_tdata() 方法的返回值就是 class_rw_t 类型的指针对象。我们在本文后面会重点介绍。

探索 bits 属性

bits属性的结构体是class_data_bits_t,看一下它的源码,class_data_bits_t声明了objc_class是它的友元类,可见关系不一般。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
struct class_data_bits_t {
friend objc_class;//友元类

// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit) const
{
return bits & bit;
}

// Atomically set the bits in `set` and clear the bits in `clear`.
// set and clear must not overlap.
void setAndClearBits(uintptr_t set, uintptr_t clear)
{
ASSERT((set & clear) == 0);
uintptr_t newBits, oldBits = LoadExclusive(&bits);
do {
newBits = (oldBits | set) & ~clear;
} while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
}

void setBits(uintptr_t set) {
__c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
}

void clearBits(uintptr_t clear) {
__c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
}

public:

class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
ASSERT(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}

// Get the class's ro data, even in the presence of concurrent realization.
// fixme this isn't really safe without a compiler barrier at least
// and probably a memory barrier when realizeClass changes the data field
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}

#if SUPPORT_INDEXED_ISA
void setClassArrayIndex(unsigned Idx) {
// 0 is unused as then we can rely on zero-initialisation from calloc.
ASSERT(Idx > 0);
data()->index = Idx;
}
#else
void setClassArrayIndex(__unused unsigned Idx) {
}
#endif

unsigned classArrayIndex() {
#if SUPPORT_INDEXED_ISA
return data()->index;
#else
return 0;
#endif
}

bool isAnySwift() {
return isSwiftStable() || isSwiftLegacy();
}

bool isSwiftStable() {
return getBit(FAST_IS_SWIFT_STABLE);
}
void setIsSwiftStable() {
setAndClearBits(FAST_IS_SWIFT_STABLE, FAST_IS_SWIFT_LEGACY);
}

bool isSwiftLegacy() {
return getBit(FAST_IS_SWIFT_LEGACY);
}
void setIsSwiftLegacy() {
setAndClearBits(FAST_IS_SWIFT_LEGACY, FAST_IS_SWIFT_STABLE);
}

// fixme remove this once the Swift runtime uses the stable bits
bool isSwiftStable_ButAllowLegacyForNow() {
return isAnySwift();
}

_objc_swiftMetadataInitializer swiftMetadataInitializer() {
// This function is called on un-realized classes without
// holding any locks.
// Beware of races with other realizers.
return safe_ro()->swiftMetadataInitializer();
}
};

我们主要看下面两块代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}

先看class_rw_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif

explicit_atomic<uintptr_t> ro_or_rw_ext;

Class firstSubclass;
Class nextSiblingClass;
//省略代码 ...

const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}

const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}

const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
}

这个结构体里有一些我们熟悉的字样methodspropertiesprotocols。这里的properties是不是就是存储属性的地方呢。让我们验证一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@end

NS_ASSUME_NONNULL_END

@implementation Person

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *p = [[Person alloc] init];
Class pClass = object_getClass(p);
NSLog(@"%s", p);
}
return 0;
}

我们定义一个Person类,在里面定义一个成员变量和一个属性。在main函数里打断点,我们用LLDB打印看一下pClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(lldb) x/4gx pClass
0x1000081d0: 0x00000001000081a8 0x0000000100357140
0x1000081e0: 0x000000010072fdc0 0x0001802400000003
(lldb) p (class_data_bits_t *)0x1000081f0//0x1000081d0+0x20 bits地址偏移量
(class_data_bits_t *) $16 = 0x00000001000081f0
(lldb) p $16->data()
(class_rw_t *) $17 = 0x0000000100731e10
(lldb) p *$17
(class_rw_t) $18 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000208
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}

可以看最后$18里面并没有打印出属性列表。我们接着探索class_ro_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};

explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
//隐藏代码
}

发现class_ro_t接口中包含了baseMethodListbaseProtocolsbaseProperties等属性。验证一下属性是不是存储在这里了。和上面方式类似,我们利用LLDB查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 p (class_data_bits_t *)0x1000081f0
(class_data_bits_t *) $19 = 0x00000001000081f0
(lldb) p $19->safe_ro()
(const class_ro_t *) $20 = 0x0000000100008090
(lldb) p $20
(const class_ro_t) $21 = {
flags = 388
instanceStart = 8
instanceSize = 24
reserved = 0
= {
ivarLayout = 0x0000000100003f1d "\x02"
nonMetaclass = 0x0000000100003f1d
}
name = {
std::__1::atomic<const char *> = "Person" {
Value = 0x0000000100003f16 "Person"
}
}
baseMethodList = 0x00000001000080d8
baseProtocols = 0x0000000000000000
ivars = 0x0000000100008128
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000100008170
_swiftMetadataInitializer_NEVER_USE = {}
}
}
(lldb) p $20.baseProperties
(property_list_t *const) $22 = 0x0000000100008170
Fix-it applied, fixed expression was:
$20->baseProperties
(lldb) p *$22
(property_list_t) $23 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 1)
}
(lldb) p $23.get(0)
(property_t) $28 = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")

可以看到了nickName属性,这里的count是1,说明只有这一个属性。我们接下来看一下ivars属性,继续使用LLDB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(lldb) p $20.ivars
(const ivar_list_t *const) $24 = 0x0000000100008128
Fix-it applied, fixed expression was:
$20->ivars
(lldb) p *$24
(const ivar_list_t) $25 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 2)
}
(lldb) p $25.get(1)
(ivar_t) $26 = {
offset = 0x00000001000081a0
name = 0x0000000100003f5b "_nickName"
type = 0x0000000100003f89 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $25.get(0)
(ivar_t) $27 = {
offset = 0x0000000100008198
name = 0x0000000100003f55 "hobby"
type = 0x0000000100003f89 "@\"NSString\""
alignment_raw = 3
size = 8
}

可以看到ivars属性里一共有两个值,_nickName和hobby。这一结果证实了编译器会帮助我们给属性 nickName 生成一个带下划线前缀的实例变量 _nickName

至此,我们可以得出以下结论:

class_ro_t 是在编译时就已经确定了的,存储的是类的成员变量、属性、方法和协议等内容。
class_rw_t 是可以在运行时来拓展类的一些属性、方法和协议等内容。

类的方法存储在哪

我们用相同的方法验证一下,先在Person类中增加一个sayHello 的实例方法和一个 sayHappy 的类方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end

NS_ASSUME_NONNULL_END

@implementation Person

- (void)sayHello
{
NSLog(@"%s", __func__);
}

+ (void)sayHappy
{
NSLog(@"%s", __func__);
}

@end

按照上面的思路,我们直接读取 class_ro_t 中的 baseMethods() 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) p $3.baseMethods()
(method_list_t *) $7 = 0x00000001000080f0
(lldb) p *$7
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 4)
}
(lldb) p $8.get(0)
(method_t) $9 = {}
(lldb) p $8.get(1)
(method_t) $10 = {}
(lldb) p $8.get(2)
(method_t) $11 = {}
(lldb) p $8.get(3)
(method_t) $12 = {}

可以看到baseMethods有四个方法。

类的类方法存储在哪

我们用Runtime的API来实际测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// main.m
void testInstanceMethod_classToMetaclass(Class pClass){

const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);

Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));

NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *p = [[Person alloc] init];
Class pClass = object_getClass(p);

testInstanceMethod_classToMetaclass(pClass);
}
return 0;
}

运行后打印结果:

1
2
0x100008110-0x0-0x0-0x1000080a8
testInstanceMethod_classToMetaclass

首先 testInstanceMethod_classToMetaclass 方法测试的是分别从类和元类去获取实例方法、类方法的结果。由打印结果我们可以知道:

  • 对于类对象来说,sayHello 是实例方法,存储于类对象的内存中,不存在于元类对象中。而 sayHappy 是类方法,存储于元类对象的内存中,不存在于类对象中。
  • 对于元类对象来说,sayHello 是类对象的实例方法,跟元类没关系;sayHappy 是元类对象的实例方法,所以存在元类中。

类和元类的创建时机

先看结论:

类和元类是在编译期创建的,即在进行alloc操作之前,类和元类就已经被编译器床架你出来了。

我们通过LLDB打印类和元类的指针来验证一下,断点打在main的第一行,这时候Person对象还没有初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(lldb) p/x Person.class
(Class) $1 = 0x0000000100008220 Person
(lldb) x/4gx 0x0000000100008220//Person 类对象
0x100008220: 0x00000001000081f8 0x0000000100357140
0x100008230: 0x000000010034f360 0x0000802400000000
(lldb) p/x 0x00000001000081f8 & 0x00007ffffffffff8
(long) $2 = 0x00000001000081f8//Person 元类对象
(lldb) po $2
Person

(lldb) x/4gx 0x00000001000081f8
0x1000081f8: 0x00000001003570f0 0x00000001003570f0
0x100008208: 0x0000000100615130 0x0001e03500000003
(lldb) p/x 0x00000001003570f0 & 0x00007ffffffffff8
(long) $3 = 0x00000001003570f0//NSObject 根元类对象
(lldb) po $3
NSObject

(lldb) x/4gx 0x00000001003570f0
0x1003570f0: 0x00000001003570f0 0x0000000100357140
0x100357100: 0x0000000101104ac0 0x0004e03100000007
(lldb) p/x 0x00000001003570f0 & 0x00007ffffffffff8
(long) $4 = 0x00000001003570f0//NSObject 根元类对象
(lldb) po $4
NSObject

还有一种方式是通过MachoView应用打开编译的二进制mach-o文件,MachoView的使用就不介绍了。

总结

  • 类和元类创建于编译时,可以通过 LLDB 来打印类和元类的指针,或者 MachOView 查看二进制可执行文件
  • 万物皆对象:类的本质就是对象
  • 类在 class_ro_t 结构中存储了编译时确定的属性、成员变量、方法和协议等内容。
  • 实例方法存放在类中
  • 类方法存放在元类中

calloc底层

calloc的源码在libMalloc,不是之前用的libObjc

1
2
3
4
5
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

可以看出calloc调用了_malloc_zone_calloc函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

void *ptr;
if (malloc_check_start) {
internal_check();
}
// 这里调用了calloc函数
ptr = zone->calloc(zone, num_items, size);

if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}

MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}

可以看到,如果继续调用就又回到了calloc函数,这个时候我们借助LLDB打印内存地址。

1
2
p zone->calloc
会输出:(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x00000001002e1b87 (.dylib`default_zone_calloc at malloc.c:385)

可以看出这里调用了malloc.c文件的385行

1
2
3
4
5
6
7
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();

return zone->calloc(zone, num_items, size);
}

default_zone_calloc里依然会调用calloc,我们用相同的方法查看具体执行的代码。

1
2
p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x00000001002e72cd (.dylib`nano_calloc at nano_malloc.c:878)

这次执行的代码在nano_malloc.c的878行。nano_calloc函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
//判断是否有足够内存分配??
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}

if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}

看代码,我们后面应该继续看_nano_malloc_check_clear函数,分析_nano_malloc_check_clear函数,内存大小分配应该是调用segregated_size_to_fit函数。

1
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
//size = 40
size_t k, slot_bytes;

if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
// 40 + 16-1 >> 4 << 4 (先除以16余数舍去,再乘以16)
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!

return slot_bytes;
}

这里可以看出进行的是 16 字节对齐,那么也就是说我们传入的 size 是 40,在经过 (40 + 16 - 1) >> 4 << 4 操作后,结果为48,也就是16的整数倍。

小结:

  • 对象的属性是进行8字节对齐

  • 对象自己进行的是16字节对齐,原因:1、内存是连续的,通过 16 字节对齐规避风险和容错,防止访问溢出。2、提高了寻址访问效率,也就是空间换时间

isa底层

isa是一个联合体,是从内存管理层面来设计的,因为联合体是所有成员共享一个内存,联合体内存的大小取决于内部成员内存大小最大的那个元素,对应isa指针来说,就不用额外声明很多属性,直接在内部ISA_BITFIELD保存信息。同时由于联合体属性间是互斥的,所以 clsbitsisa 初始化流程时是在两个分支中被赋值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

uintptr_t bits;

private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;

public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};

bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif

void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};

下面是isa的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());

isa_t newisa(0);

if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}

// 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 = newisa;
}

isa是一个联合体,有一个结构体属性是ISA_BITFIELD,其大小为 8 个字节,也就是 64 位。

1
2
3
4
5
6
7
8
9
10
#   define ISA_BITFIELD                                                        \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
  • nonpointer: 表示是否对 isa 指针开启指针优化 0:纯 isa 指针 1:不止是类对象地址, isa 中包含了类信息、对象的引用计数等
  • has_assoc: 关联对象标志位,0 没有,1 存在
  • has_cxx_dtor: 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced: 标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • has_sidetable_rc: 当对象引用技术大于 10 时,则需要借用该变量存储进位
  • extra_rc: 当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

isa关联对象和类

isa 是对象中的第一个属性,因为这一步是在继承的时候发生的,要早于对象的成员变量,属性列表,方法列表以及所遵循的协议列表。

我们在探索 alloc 底层原理的时候,有一个方法叫做 initIsa

这个方法的作用就是初始化 isa 联合体位域。其中有这么一行代码:

1
newisa.shiftcls = (uintptr_t)cls >> 3;

通过这行代码,我们知道 shiftcls 这个位域其实存储的是类的信息。这个类就是实例化对象所指向的那个类。

这里的左移右移操作其实很好理解,首先我们先观察 isaISA_BITFIELD 位域的结构:

1
2
3
4
5
6
7
8
9
10
#   define ISA_BITFIELD                                                        \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8

我们可以看到,ISA_BITFIELD 的前 3 位是 nonpointerhas_assochas_cxx_dtor,中间 44 位是 shiftcls ,后面 17 位是剩余的内容,同时因为 iOS 是小端模式,那么我们就需要去掉右边的 3 位和左边的 17位,所以就会采用 >>3<<3 然后 <<17>>17 的操作了。

通过这个测试,我们就知道了 isa 实现了对象与类之间的关联。

我们还可以探索 object_getClass 底层,可以发现有这样一行代码:

1
return (Class)(isa.bits & ISA_MASK);

这行代码就是将 isa 中的联合体位域与上一个蒙版,这个蒙版定义是怎么样的呢?

1
#   define ISA_MASK        0x00007ffffffffff8ULL

x00007ffffffffff8ULL 这个值我们转成二进制表示:

1
2
0000 0000 0000 0000 0111 1111 1111 1111 
1111 1111 1111 1111 1111 1111 1111 1000

结果一目了然,这个蒙版就是帮我们去过滤掉除 shiftcls 之外的内容。

我们直接将对象的 isa 地址与上这个mask之后,就会得到 object.class 一样的内存地址。

isa走位分析

类与元类

我们都知道对象可以创建多个,但是类是否可以创建多个呢?
答案很简单,一个。那么如果来验证呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//MARK: - 分析类对象内存存在个数
void lgTestClassNum(){
Class class1 = [LGPerson class];
Class class2 = [LGPerson alloc].class;
Class class3 = object_getClass([LGPerson alloc]);
Class class4 = [LGPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}

// 打印输出如下:

0x100002108-
0x100002108-
0x100002108-
0x100002108

所以我们就知道了类在内存中只会存在一份。

1
2
3
4
5
6
7
8
9
10
(lldb) x/4gx LGTeacher.class
0x100001420: 0x001d8001000013f9 0x0000000100b38140
0x100001430: 0x00000001003db270 0x0000000000000000
(lldb) po 0x001d8001000013f9
17082823967917874

(lldb) p 0x001d8001000013f9
(long) $2 = 8303516107936761
(lldb) po 0x100001420
LGTeacher

我们通过上面的打印,就发现 类的内存结构里面的第一个结构打印出来还是 LGTeacher,那么是不是就意味着 对象->类->类 这样的死循环呢?这里的第二个类其实是 元类。是由系统帮我们创建的。这个元类也无法被我们实例化。

对象的本质

在我们认知里面,OC 对象的本质就是一个结构体,这个结论在 libObjc 源码的 objc-private.h 源文件中可以得到证实。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_object {
private:
isa_t isa;

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();

...省略其他的内容...
}

而对于对象所属的类来说,我们也可以在 objc-runtime-new.h 源文件中找到

1
2
3
4
5
6
7
8
9
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

...省略其他的内容...

}

也就是说 objc_class 内存中第一个位置是 isa,第二个位置是 superclass

不过我们本着求真的态度可以用 clang 来重写我们的 OC 源文件来查看是不是这么回事。

1
clang -rewrite-objc main.m -o main.cpp

这行命令会把我们的 main.m 文件编译成 C++ 格式,输出为 main.cpp

我们可以看到 LGPerson 对象在底层其实是一个结构体 objc_object

而我们的 Class 在底层也是一个结构体 objc_class

上文中在分析alloc函数的时候,发现有个步骤是给对象分配内存空间,在分配内存空间的时候有一个内存对齐的概念,本文就详细说明一下内存对齐。

NSObject对象占用多少内存空间

看下面这段代码

1
2
3
4
NSObject *obj = [[NSObject alloc] init];
NSLog(@"实际占用: class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
NSLog(@"系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
NSLog(@"NSObject类型占用:sizeOf = %zd", sizeof(obj));

可以看到打印结果是

1
2
3
实际占用: class_getInstanceSize = 8
系统分配:malloc_size = 16
NSObject类型占用:sizeOf = 8

说明NSObject对象占用的内存空间是8个字节,但是系统分配的空间是16字节。这是为什么,我们看一下核心代码,根据上文可以知道,各个函数的调用是allocWithZone->_objc_rootAllocWithZone->_class-createInstanceFromZone,前两个函数都是简单的调用后一个函数,代码集中在_class-createInstanceFromZone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
//这里是分配内存空间的
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;

id obj;
if (zone) {
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;
}

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;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}

最后我们看instanceSize函数

1
2
3
4
5
6
7
8
9
10
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}

size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}

可以看到当size<16的时候,系统会分配16个字节。

内存对齐规则

1、结构体变量的首地址是其最长基本类型成员的整数倍;

2、结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足;

3、结构体的总大小为结构体最大基本类型成员变量大小的整数倍;

4、结构体中的成员变量都是分配在连续的内存空间中。

自定义一个类验证一下

1
2
3
4
5
6
@interface TestMemory : NSObject{
int _age;
NSString *_name;
int _weight;
}
@end

像刚开始一样,我们打印TestMemory对象的内存大小

1
2
3
4
TestMemory *obj = [[TestMemory alloc] init];
NSLog(@"实际占用: class_getInstanceSize = %zd", class_getInstanceSize([TestMemory class]));
NSLog(@"系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
NSLog(@"TestMemory类型占用:sizeOf = %zd", sizeof(obj));

打印结果

1
2
3
实际占用: class_getInstanceSize = 32
系统分配:malloc_size = 32
TestMemory类型占用:sizeOf = 8

TestMemory转成C++代码看一下结构体内容

1
xcrun -sdk iphonesimulator clang -rewrite-objc TestMemory.m

结构体内容

1
2
3
4
5
6
struct TestMemory_IMPL {
struct NSObject_IMPL NSObject_IVARS;//8个字节
int _age;//int 正常4个字节
NSString *_name;//8个字节
int _weight;//4个字节
}

根据内存对齐规则2,_name变量的起始地址将会是8的整数倍,_age变量后面空了4个字节,可以计算出大小为8+4+4(填充4字节)+8+4=28,根据规则3,需要是最大成员的整数倍,所以28向上对8取整,内存大小为32。

下面我们修改一下TestMemory成员变量的顺序

1
2
3
4
5
6
@interface TestMemory : NSObject{
int _age;
int _weight;
NSString *_name;
}
@end

现在打印的结果是:

1
2
3
实际占用: class_getInstanceSize = 24
系统分配:malloc_size = 32
TestMemory类型占用:sizeOf = 8

可以看到实际占用的大小为24了,我们看一下c++的结构体代码

1
2
3
4
5
6
struct TestMemory_IMPL {
struct NSObject_IMPL NSObject_IVARS;//8字节
int _age;//4字节
int _weight;//4字节
NSString *_name;//8字节
};

因为我们调整了一下顺序,_name的起始地址是8的倍数,所以不需要再_weight字节后面填充字节,大小就为8+4+4+8=24,因为24是8的倍数,所以也不需要根据规则3对8向上取整,实际占用内存空间就是24。

到这里我们可以得出一个结论,成员变量顺序不同可以影响对象站内内存的空间大小。现在我们可能会想如果我们实际开发中定义属性的顺序会不会影响内存大小呢,如果影响如果不注意岂不是对象的内存大小会有很大差别?带着这个问题我们看一下定义属性的c++代码。

1
2
3
4
5
6
7
8
9
10
@interface TestMemory : NSObject{
// int _age;
// int _weight;
// NSString *_name;
}
@property (nonatomic,assign)int age;
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)int weight;

@end

我们定义属性的顺序和第一次测试的顺序相同。c++代码

1
2
3
4
5
6
struct TestMemory_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _weight;
NSString * _Nonnull _name;
};

可以看到结构体里成员变量的顺序已经改变过了,说明苹果已经进行了优化,我们实际开发定义属性的时候,不会因为属性定义顺序的不同而影响对象占用的内存大小。

参考文档