欢迎阅读iOS底层系列(建议按顺序)
1.本文概述
本文旨在分析dyld
初始化主程序时,类结构是如何被加载的,类数据是如何处理的。这部分也隶属于main()
函数前的流程。
2.类加载探索
2.1 寻找切入点
上篇dyld是如何加载app的分析了dyld
的流程,说明了在准备初始化主程序时,libObjc
会来_objc_init()
到对项目中所有的类结构进行初始化。因此,_objc_init()
就是切入点。
2.2 _objc_init()分析
直接在libObjc
中搜索_objc_init(
,
先看它的定义,了解下它是做什么的:
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
译:启动初始化。通过dyld注册我们的图像通知。在库初始化时间之前由libSystem调用
来到实现源码,在看它是怎么做的:
① 如果已经执行过初始化,直接返回。保证只初始化一次。
② 读取影响运行时的环境变量。如果需要,也打印环境变量help。
内部通过字符串匹配读取已设置的环境变量。这里的环境变量基本是以OBJC_
开头的,区别于dyld
流程中以dyld_
开头的环境变量。
可以在iTerm2
中输入export OBJC_HELP=1
查看系统提供的环境变量。其中OBJC_PRINT_LOAD_METHODS
是较为常用的,它会打印出所有实现的+load()
方法,开发者可以根据其输出,选择性的删除不必要的+load()
方法,提高启动速度。
③ 设置objc的预定义的线程特定键和键的析构函数,来存储objc的私有数据。
④ 运行c++静态构造函数
内部通过getLibobjcInitializers
获取macho
下__objc_init_func
段。因为libObjc
在dyld
调用静态构造函数之前就会先调用_objc_init()
,因此这里只能先手动调用,所以这里都是调用系统类的c++静态构造函数。自己写的将会在后续调用。
⑤ 无任何操作
void lock_init(void)
{
}
内部是个空实现。libObjc
是用c
和c++
实现的,它们自身有一套锁的机制,说明这套机制在oc中同样适用。这里默认使用这套机制,不做任何操作。这行代码的意义可能在于增加可读性。
⑥ 初始化异常处理系统
内部通过@try@catch
保证程序执行过程中出现的异常能详细的输出。
⑦ 在dyld初始化主程序时,通过指针回调实现images的map,load,unmap操作
这是_objc_init()
的核心部分,前六步只是准备工作。
接下来就来看看,libObjc
从dyld
那里接手的map
,load
,unmap
是在做些什么。
2.3 map_images() 分析
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);
}
map_images()
内部通过调用map_images_nolock()
,参数是dyld
传递来的镜像文件个数count
,文件路径paths
,macho
头文件信息mach_header
。
...
while (i--) {
const headerType *mhdr = (const headerType *)mhdrs[i];
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
continue;
}
if (mhdr->filetype == MH_EXECUTE) {
#if __OBJC2__
size_t count;
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
...
map_images_nolock
内部通过遍历头文件信息,当头文件类型是MH_EXECUTE
可执行文件时,获取macho
下__objc_selrefs
和__objc_msgrefs
段,为注册方法作准备。
...
if (firstTime) {
sel_init(selrefCount);
arr_init();
...
然后执行一次性的运行时初始化,该初始化必须延迟到找到可执行文件本身。此初始化包括:
① 注册部分系统方法选择器
#define s(x) SEL_##x = sel_registerNameNoLock(#x, NO)
#define t(x,y) SEL_##y = sel_registerNameNoLock(#x, NO)
s(load);
s(initialize);
t(resolveInstanceMethod:, resolveInstanceMethod);
t(resolveClassMethod:, resolveClassMethod);
t(.cxx_construct, cxx_construct);
t(.cxx_destruct, cxx_destruct);
s(retain);
s(release);
s(autorelease);
s(retainCount);
s(alloc);
t(allocWithZone:, allocWithZone);
s(dealloc);
s(copy);
s(new);
t(forwardInvocation:, forwardInvocation);
t(_tryRetain, tryRetain);
t(_isDeallocating, isDeallocating);
s(retainWeakReference);
s(allowsWeakReference);
可以看到好多熟悉的方法,那为什么只有这些?
因为这一系列方法系统内部要使用到,需要提早注册,其他方法选择器将会在类结构初始化时注册。
② 自动释放池初始化,全局散列表初始化
void arr_init(void)
{
AutoreleasePoolPage::init();
SideTableInit();
}
这里的散列表用来存储后续的weak
表,引用计数表等。
map_images_nolock()
函数最后执行_read_images()
开始读取macho
初始化类信息。
...
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
2.4 _read_images() 分析
首先,_read_images()
的目的是读取macho初始化类信息,读取的内容必然需要容器来存储。
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
所以,刚进来,libObjc
就先创建了两张表gdb_objc_realized_classes
和allocatedClasses
做准备。
那为什么需要两张表?
-
gdb_objc_realized_classes :未在dyld共享缓存中的已命名类,无论是否实现。并且根据当前类数量做动态扩容。
-
allocatedClasses : 已分配的所有类(元类)。
这也容易理解,系统后续需要依赖表做相关处理,因此总表保存原始所有数据,小表用来保存需要初始化的数据,提高查询效率。
创建好容易,接下来就开始读取数据。
如果宏观的查看此方法,会发现它的写法很有意思,富有仪式感
可以看到,它依次做了相关处理,并且实现的逻辑类似,最后都有对应输出。
主体流程清晰后,来看看其中比较重要的几个处理:
① 类处理
for (EACH_HEADER) {
classref_t *classlist = _getObjc2ClassList(hi, &count);
if (! mustReadClasses(hi)) {
continue;
}
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->isPreoptimized();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
...
}
}
}
GETSECT(_getObjc2ClassList, classref_t, "__objc_classlist");
点击_getObjc2ClassList
,来到这个宏定义,意思为
从macho
中__objc_classlist
段下读取类信息(后面其他的处理也是如此,只是读取的字段不一样),遍历读取的类调用readClass()。
乍看之下,readClass里面很多ro
,rw
相关的代码,那rw
肯定就是在这被设置了,很多文章也是这样说明的。过于草率,可能会忽略细节。
其实这里的设置rw
有个判断条件popFutureNamedClass()
,这是苹果预留的FutureClass
的判断,正常的流程是不会进来,有疑问的同学也可以自行加以断点验证。所以readClass主要做了两件事:
- 不会执行
- addNamedClass()
- addClassTableEntry()
而addNamedClass()
和addClassTableEntry()
也很类似,把读取到的类和元类插入到gdb_objc_realized_classes
总表中。
这里的类处理,只是把类添加到表中,并为做加载操作。
② 方法编号处理
static size_t UnfixedSelectors{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->isPreoptimized()) 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]);
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
}
先从macho
中__objc_selrefs
段下读取方法编号信息,遍历读取的方法编号调用sel_registerNameNoLock()
插入到方法编号哈希表中。类似上面系统注册自身使用的方法,这里注册了剩余的其他方法。
③ 非懒加载类实现
for (EACH_HEADER) {
classref_t *classlist = _getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
...
addClassTableEntry(cls);
...
realizeClassWithoutSwift(cls);
}
}
先从macho
中__objc_nlclslist
段下读取非懒加载类,遍历读取的非懒加载类调用remapClass()
和addClassTableEntry()
保证已经添加到对应的表中。然后调用realizeClassWithoutSwift() 实现类。
需要注意,整个流程仅处理非懒加载类,懒加载类不加载。
那为什么只加载非懒加载类呢?
原因是这样的:目前的流程还是在main()
函数之前,一个工程中一般都是几千几万个类起步,而大部分的类是在启动后才被使用到,甚至有些类永远不会被使用。如果启动前都需要去加载,那么启动时间可想而知会有多长,苹果只去加载启动时需要用到的类是很合理的也很必要的。
④ 分类处理
for (EACH_HEADER) {
category_t **catlist = _getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
catlist[i] = nil;
...
continue;
}
bool classExists = NO;
if (cat->instanceMethods || cat->protocols || cat->instanceProperties) {
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
...
}
if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) {
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
...
}
}
}
先从macho
中__objc_catlist
段下读取分类,遍历读取的分类调用remethodizeClass()
,内部在调用attachCategories()
,把分类的method
、protocol
、property
附加到类。
文章2.6
会有部分分析,分类相关的下一章会进一步分析(涉及到懒加载和非懒加载到情况),这里先分析下重点函数realizeClassWithoutSwift()。
2.5 realizeClassWithoutSwift()分析
从字面意思来看,realizeClassWithoutSwift()
是非swift
环境下实现类的调用,那是否存在swift
环境下实现类的调用。尝试搜索下,果然看到_objc_realizeClassFromSwift()
。
看其实现:
本质上依然是调用realizeClassWithoutSwift()
,所以无论是否swift
,分析realizeClassWithoutSwift()
即可。
还是先看它是做什么的:
Performs first-time initialization on class cls,including allocating its read-write data.
Returns the real class structure for the class.
译:在类上执行首次初始化,包括分配其读写数据。返回类的真实类结构
然后在看它是怎么做的:
① 绝大多数情况都执行Normal class
,这里是真正开始设置rw
的地方,但是需要注意,rw
只赋值了ro
和flgs
,其他的methods
,protocol
还未赋值。
② 递归实现类的父类和元类,递归出口是cls=nil
,会一直递归到NSObject
。
③ 对类的结构体内部的isa
,supercls
赋值,类的结构包含isa
,supercls
,cache_t
,bits
。很好理解,实现类的同时,肯定需要对内部属性做处理。
④ 如果存在supercls
,反向把cls
添加为supercls
对子类,否则直接设置为rootClass。③和④相当于双向链表关联cls
和supercls
。
⑤ methodizeClass() 的作用是修复cls
的方法列表、协议列表和属性列表。附加任何额外的类别。
2.6 methodizeClass()分析
来到methodizeClass()
,
它保持先类后分类的顺序做了两件事(部分情况时,此时的ro
已经存在分类的数据,下一章分析):
- 往
rw
中依次attachLists()
方法列表、协议列表和属性列表 unattachedCategoriesForClass()
获取未附加的分类列表,调用attachCategories()
往rw
中也依次attachLists()
方法列表、协议列表和属性列表。
来到 attachCategories()
static void attachCategories(Class cls, category_list *cats, bool flush_caches){
if (!cats) return;
...
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
可以看到内部同样是调用attachLists()
。
有个有趣的现象,方法列表、协议列表和属性列表都可以调用attachLists()
,说明它们的底层结构是类似的。
来到 attachLists(),
这是数据的具体操作步骤。
- 在确定个数且为多个的情况下,旧数据整体向后移动新数据的个数,把新数据插入到列表的前面
- 在确定个数且为一个情况下,新数据直接插在第一个
- 不确定个数的情况下,旧数据往后移动一个,新数据插入一个在前面,反复执行到添加结束
总的来说,新数据总是在旧数据前面,这也就解释了为什么相同方法,分类方法会有"覆盖"主类方法的假象(+load除外)。
至此,_objc_init
从dyld
接手的map_images()
工作结束,类的结构被加载,类数据被处理。但是需要注意的是,以上仅限于非懒加载类。
3.部分相关问题总结和面试题
1.分类和主类实现了相同的方法会被覆盖嘛,谁覆盖谁?
都不会覆盖,分类和主类的方法同时存在,只是分类存储位先于主类,所以调用方法优先读取到分类的方法,产生了一种主类方法被分类覆盖的假象
2.类和分类的数据谁先被加入rw?
先类后分类。分类的数据是需要被attach
到rw
中才能生效,所以需要先加载类,使其拥有rw
。即使有些情况分类先被执行,但也只是被保存起来,等到类被执行后才attach
分类
3.为什么有了ro,还需要rw
因为oc
是动态的,除了编译期的数据,还能在运行时添加数据
4.ro和rw的关系
ro
存储了当前类在编译期就已经确定的属性、方法以及遵循的协议;
rw
是在运行时才确定,它会先将ro
的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。rw
是ro
的超集
5.macho里面的数据是怎么到内存的
读取macho
的对应字段下的内容,用哈希表存储,然后根据表初始化
4.写在后面
相对于dyld
,类的加载原理流程较为简单,但是根据它衍生出来的面试题较多,因此了解它的流程是性价比很高的一件事,值得我们花时间去深入。
下一章是分类的加载分析,涉及到类和分类在懒加载和非懒加载时的流程,是对本章内容的补充。