该文章属于 < 简书 — 刘小壮 > 原创,转载请注明:
< 简书 — 刘小壮 > https://www.jianshu.com/p/4fb2d7014e9e
程序加载过程
在 iOS 程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有 iOS 程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。
除了在项目设置里显式出现的动态库外,还会有一些隐式存在的动态库。例如 objc
和Runtime
所属的 libobjc.dyld
和libSystem.dyld
,在 libSystem
中包含常用的 libdispatch(GCD)
、libsystem_c
(C 语言基础库)、libsystem_blocks(Block)
等。
使用动态库的优点:
- 防止重复。iOS 系统中所有
App
公用一套系统动态库,防止重复的内存占用。 - 减少包体积。因为系统动态库被内置到 iOS 系统中,所以打包时不需要把这部分代码打进去,可以减小包体积。
- 动态性。因为系统动态库是动态加载的,所以可以在更新系统后,将动态库换成新的动态库。
加载过程
在应用程序启动后,由 dyld(the dynamic link editor)
进行程序的初始化操作。大概流程就像下面列出的步骤,其中第 3、4、5 步会执行多次,在 ImageLoader
加载新的 image
进内存后就会执行一次。
- 在引用程序启动后,由
dyld
将应用程序加载到二进制中,并完成一些文件的初始化操作。 -
Runtime
向dyld
中注册回调函数。 - 通过
ImageLoader
将所有image
加载到内存中。 -
dyld
在image
发生改变时,主动调用回调函数。 -
Runtime
接收到dyld
的函数回调,开始执行map_images
、load_images
等操作,并回调+load
方法。 - 调用
main()
函数,开始执行业务代码。
ImageLoader
是 image
的加载器,image
可以理解为编译后的二进制。
下面是在 Runtime
的map_images
函数打断点,观察回调情况的汇编代码。可以看出,调用是由 dyld
发起的,由 ImageLoader
通知 dyld
进行调用。
关于 dyld
我并没有深入研究,有兴趣的同学可以到 Github 上下载源码研究一下。
动态加载
一个 OC 程序可以在运行过程中动态加载和链接新类或 Category
,新类或Category
会加载到程序中,其处理方式和其他类是相同的。动态加载还可以做许多不同的事,动态加载允许应用程序进行自定义处理。
OC 提供了 objc_loadModules
运行时函数,执行 Mach-O
中模块的动态加载,在上层 NSBundle
对象提供了更简单的访问API
。
map images
在 Runtime
加载时,会调用 _objc_init
函数,并在内部注册三个函数指针。其中 map_images
函数是初始化的关键,内部完成了大量 Runtime
环境的初始化操作。
在 map_images
函数中,内部也是做了一个调用中转。然后调用到 map_images_nolock
函数,内部核心就是 _read_images
函数。
void _objc_init(void)
{
// .... 各种 init
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{rwlock_writer_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{if (hCount > 0) {_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
}
在 _read_images
函数中完成了大量的初始化操作,函数内部代码量比较大,下面是精简版带注释的源代码。
先整体梳理一遍 _read_images
函数内部的逻辑:
- 加载所有类到类的
gdb_objc_realized_classes
表中。 - 对所有类做重映射。
- 将所有
SEL
都注册到namedSelectors
表中。 - 修复函数指针遗留。
- 将所有
Protocol
都添加到protocol_map
表中。 - 对所有
Protocol
做重映射。 - 初始化所有非懒加载的类,进行
rw
、ro
等操作。 - 遍历已标记的懒加载的类,并做初始化操作。
- 处理所有
Category
,包括Class
和Meta Class
。 - 初始化所有未初始化的类。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
TimeLogger ts(PrintImageTimes);
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
if (!doneOnce) {
doneOnce = YES;
// 实例化存储类的哈希表,并且根据当前类数量做动态扩容
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
}
// 由编译器读取类列表,并将所有类添加到类的哈希表中,并且标记懒加载的类并初始化内存空间
for (EACH_HEADER) {if (! mustReadClasses(hi)) {continue;}
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->isPreoptimized();
/** 将新类添加到哈希表中 */
// 从编译后的类列表中取出所有类,获取到的是一个 classref_t 类型的指针
classref_t *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
// 数组中会取出 OS_dispatch_queue_concurrent、OS_xpc_object、NSRunloop 等系统类,例如 CF、Fundation、libdispatch 中的类。以及自己创建的类
Class cls = (Class)classlist[i];
// 通过 readClass 函数获取处理后的新类,内部主要操作 ro 和 rw 结构体
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
// 初始化所有懒加载的类需要的内存空间
if (newCls != cls && newCls) {
// 将懒加载的类添加到数组中
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
// 将未映射 Class 和 Super Class 重映射,被 remap 的类都是非懒加载的类
if (!noClassesRemapped()) {for (EACH_HEADER) {
// 重映射 Class,注意是从_getObjc2ClassRefs 函数中取出类的引用
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {remapClassRef(&classrefs[i]);
}
// 重映射父类
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {remapClassRef(&classrefs[i]);
}
}
}
// 将所有 SEL 都注册到哈希表中,是另外一张哈希表
static size_t UnfixedSelectors;
sel_lock();
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]);
// 注册 SEL 的操作
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
// 修复旧的函数指针调用遗留
for (EACH_HEADER) {message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
if (count == 0) continue;
for (i = 0; i < count; i++) {
// 内部将常用的 alloc、objc_msgSend 等函数指针进行注册,并 fix 为新的函数指针
fixupMessageRef(refs+i);
}
}
// 遍历所有协议列表,并且将协议列表加载到 Protocol 的哈希表中
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
// cls = Protocol 类,所有协议和对象的结构体都类似,isa 都对应 Protocol 类
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
// 获取 protocol 哈希表
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
// 从编译器中读取并初始化 Protocol
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
// 修复协议列表引用,优化后的 images 可能是正确的,但是并不确定
for (EACH_HEADER) {
// 需要注意到是,下面的函数是_getObjc2ProtocolRefs,和上面的_getObjc2ProtocolList 不一样
protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
for (i = 0; i < count; i++) {remapProtocolRef(&protolist[i]);
}
}
// 实现非懒加载的类,对于 load 方法和静态实例变量
for (EACH_HEADER) {
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {Class cls = remapClass(classlist[i]);
if (!cls) continue;
// 实现所有非懒加载的类(实例化类对象的一些信息,例如 rw)
realizeClass(cls);
}
}
// 遍历 resolvedFutureClasses 数组,实现所有懒加载的类
if (resolvedFutureClasses) {for (i = 0; i < resolvedFutureClassCount; i++) {
// 实现懒加载的类
realizeClass(resolvedFutureClasses[i]);
resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
}
free(resolvedFutureClasses);
}
// 发现和处理所有 Category
for (EACH_HEADER) {
// 外部循环遍历找到当前类,查找类对应的 Category 数组
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
// 内部循环遍历当前类的所有 Category
for (i = 0; i < count; i++) {category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// 首先,通过其所属的类注册 Category。如果这个类已经被实现,则重新构造类的方法列表。bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
// 将 Category 添加到对应 Class 的 value 中,value 是 Class 对应的所有 category 数组
addUnattachedCategoryForClass(cat, cls, hi);
// 将 Category 的 method、protocol、property 添加到 Class
if (cls->isRealized()) {remethodizeClass(cls);
classExists = YES;
}
}
// 这块和上面逻辑一样,区别在于这块是对 Meta Class 做操作,而上面则是对 Class 做操作
// 根据下面的逻辑,从代码的角度来说,是可以对原类添加 Category 的
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {remethodizeClass(cls->ISA());
}
}
}
}
// 初始化从磁盘中加载的所有类,发现 Category 必须是最后执行的
// 从 runtime objc4-532 版本源码来看,DebugNonFragileIvars 字段一直是 -1,所以不会进入这个方法中
if (DebugNonFragileIvars) {realizeAllClasses();
}
#undef EACH_HEADER
}
其内部还调用了很多其他函数,后面会详细介绍函数内部实现。
load images
在项目中经常用到 load
类方法,load
类方法的调用时机比 main
函数还要靠前。load
方法是由系统来调用的,并且在整个程序运行期间,只会调用一次,所以可以在 load
方法中执行一些只执行一次的操作。
一般 Method Swizzling
都会放在 load
方法中执行,这样在执行 main
函数前,就可以对类方法进行交换。可以确保正式执行代码时,方法肯定是被交换过的。
如果对一个类添加 Category 后,并且重写其原有方法,这样会导致 Category 中的方法覆盖原类的方法。但是 load 方法却是例外,所有 Category 和原类的 load 方法都会被执行。
源码分析
load
方法由 Runtime
进行调用,下面我们分析一下 load
方法的实现,load
的实现源码都在 objc-loadmethod.mm
中。
在一个新的工程中,我们创建一个 TestObject
类,并在其 load
方法中打一个断点,看一下系统的调用堆栈。
从调用栈可以看出,是通过系统的动态链接器 dyld
开始的调用,然后调到 Runtime
的load_images
函数中。load_images
函数是通过 _dyld_objc_notify_register
函数,将自己的函数指针注册给 dyld
的。
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();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
在 load_images
函数中主要做了两件事,首先通过 prepare_load_methods
函数准备 Class load list
和Category load list
,然后通过 call_load_methods
函数调用已经准备好的两个方法列表。
void
load_images(const char *path __unused, const struct mach_header *mh)
{if (!hasLoadMethods((const headerType *)mh)) return;
prepare_load_methods((const headerType *)mh);
call_load_methods();}
首先我们看一下 prepare_load_methods
函数的实现,看一下其内部是怎么查找 load
方法的。可以看到,其内部主要分为两部分,查找 Class
的load
方法列表和查找 Category
方法列表。
准备类的方法列表时,首先通过 _getObjc2NonlazyClassList
函数获取所有非懒加载类的列表,这时候获取到的是一个 classref_t
类型的数组,然后遍历数组添加 load
方法列表。
Category
过程也是类似,通过 _getObjc2NonlazyCategoryList
函数获取所有非懒加载 Category
的列表,得到一个 category_t
类型的数组,需要注意的是这是一个指向指针的指针。然后对其进行遍历并添加到 load
方法列表,Class
和 Category
的load
方法列表是两个列表。
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
// 获取到非懒加载的类的列表
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 设置 Class 的调用列表
schedule_class_load(remapClass(classlist[i]));
}
// 获取到非懒加载的 Category 列表
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
// 忽略弱链接的类别
if (!cls) continue;
// 实例化所属的类
realizeClass(cls);
// 设置 Category 的调用列表
add_category_to_loadable_list(cat);
}
}
在添加类的 load
方法列表时,内部会递归遍历把所有父类的 load
方法都添加进去,顺着继承者链的顺序添加,会先把父类添加在前面。然后会调用 add_class_to_loadable_list
函数,将自己的 load
方法添加到对应的数组中。
static void schedule_class_load(Class cls)
{if (!cls) return;
// 已经添加 Class 的 load 方法到调用列表中
if (cls->data()->flags & RW_LOADED) return;
// 确保 super 已经被添加到 load 列表中,默认是整个继承者链的顺序
schedule_class_load(cls->superclass);
// 将 IMP 和 Class 添加到调用列表
add_class_to_loadable_list(cls);
// 设置 Class 的 flags,表示已经添加 Class 到调用列表中
cls->setInfo(RW_LOADED);
}
而 Category
则不需要考虑父类的问题,所以直接在 prepare_load_methods
函数中遍历 Category
数组,然后调用 add_category_to_loadable_list
函数即可。
在 add_category_to_loadable_list
函数中,会判断当前 Category
有没有实现 load
方法,如果没有则直接 return
,如果实现了则添加到loadable_categories
数组中。
类的 add_class_to_loadable_list
函数内部实现也是类似,区别在于类的数组叫做loadable_classes
。
void add_category_to_loadable_list(Category cat)
{
IMP method;
// 获取 Category 的 load 方法的 IMP
method = _category_getLoadMethod(cat);
// 如果 Category 没有 load 方法则 return
if (!method) return;
// 如果已使用大小等于数组大小,对数组进行动态扩容
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
到此为止,loadable_classes
和 loadable_categories
两个数组已经准备好了,load_images
会调用 call_load_methods
函数执行这些 load
方法。在这个方法中,call_class_loads
函数是负责调用类方法列表的,call_category_loads
负责调用 Category
的方法列表。
void call_load_methods(void)
{
bool more_categories;
void *pool = objc_autoreleasePoolPush();
do {
// 反复执行 call_class_loads 函数,直到数组中没有可执行的 load 方法
while (loadable_classes_used > 0) {call_class_loads();
}
more_categories = call_category_loads();} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
下面是调用类方法列表的代码,内部主要是通过对 loadable_classes
数组进行遍历,并获取到 loadable_class
的结构体,结构体中存在 Class
和IMP
,然后直接调用即可。
Category
的调用方式和类的一样,就不在下面贴代码了。需要注意的是,load
方法都是直接调用的,并没有走运行时的 objc_msgSend
函数。
static void call_class_loads(void)
{
int i;
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
for (i = 0; i < used; i++) {Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
(*load_method)(cls, SEL_load);
}
if (classes) free(classes);
}
struct loadable_class {
Class cls; // may be nil
IMP method;
};
根据上面的源码分析,我们可以看出 load
方法的调用顺序应该是 “父类 -> 子类 -> 分类” 的顺序。因为执行加载 Class
的时机是在 Category
之前的,而且 load
子类之前会先 load
父类,所以是这种顺序。
initialize
和 load
方法类似的也有 initialize
方法,initialize
方法也是由 Runtime
进行调用的,自己不可以直接调用。与 load
方法不同的是,initialize
方法是在第一次调用类所属的方法时,才会调用 initialize
方法,而 load
方法是在 main
函数之前就全部调用了。所以理论上来说 initialize
可能永远都不会执行,如果当前类的方法永远不被调用的话。
下面我们研究一下
initialize
在Runtime
中的源码。
在向对象发送消息时,lookUpImpOrForward
函数中会判断当前类是否被初始化,如果没有被初始化,则先进行初始化再调用类的方法。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);
// .... 省略好多代码
// 第一次调用当前类的话,执行 initialize 的代码
if (initialize && !cls->isInitialized()) {_class_initialize (_class_getNonMetaClass(cls, inst));
}
// .... 省略好多代码
在进行初始化的时候,和 load
方法的调用顺序一样,会按照继承者链先初始化父类。_class_initialize
函数中关键的两行代码是 callInitialize
和lockAndFinishInitializing
的调用。
// 第一次调用类的方法,初始化类对象
void _class_initialize(Class cls)
{
Class supercls;
bool reallyInitialize = NO;
// 递归初始化父类。initizlize 不用显式的调用 super,因为 runtime 已经在内部调用了
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {_class_initialize(supercls);
}
{monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {cls->setInitializing();
reallyInitialize = YES;
}
}
if (reallyInitialize) {_setThisThreadIsInitializingClass(cls);
if (MultithreadedForkChild) {performForkChildInitialize(cls, supercls);
return;
}
@try {// 通过 objc_msgSend()函数调用 initialize 方法
callInitialize(cls);
}
@catch (...) {@throw;}
@finally {
// 执行 initialize 方法后,进行系统的 initialize 过程
lockAndFinishInitializing(cls, supercls);
}
return;
}
else if (cls->isInitializing()) {if (_thisThreadIsInitializingClass(cls)) {return;} else if (!MultithreadedForkChild) {waitForInitializeToComplete(cls);
return;
} else {_setThisThreadIsInitializingClass(cls);
performForkChildInitialize(cls, supercls);
}
}
}
通过 objc_msgSend
函数调用 initialize
方法。
void callInitialize(Class cls)
{((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
lockAndFinishInitializing
函数中会完成一些初始化操作,其内部会调用 _finishInitializing
函数,在函数内部会调用 class
的setInitialized
函数,核心工作都由 setInitialized
函数完成。
static void lockAndFinishInitializing(Class cls, Class supercls)
{monitor_locker_t lock(classInitLock);
if (!supercls || supercls->isInitialized()) {_finishInitializing(cls, supercls);
} else {_finishInitializingAfter(cls, supercls);
}
}
负责初始化类和元类,函数内部主要是查找当前类和元类中是否定义了某些方法,然后根据查找结果设置类和元类的一些标志位。
void
objc_class::setInitialized()
{
Class metacls;
Class cls;
// 获取类和元类对象
cls = (Class)this;
metacls = cls->ISA();
bool inherited;
bool metaCustomAWZ = NO;
if (MetaclassNSObjectAWZSwizzled) {
metaCustomAWZ = YES;
inherited = NO;
}
else if (metacls == classNSObject()->ISA()) {
// 查找是否实现了 alloc 和 allocWithZone 方法
auto& methods = metacls->data()->methods;
for (auto mlists = methods.beginCategoryMethodLists(),
end = methods.endCategoryMethodLists(metacls);
mlists != end;
++mlists)
{if (methodListImplementsAWZ(*mlists)) {
metaCustomAWZ = YES;
inherited = NO;
break;
}
}
}
else if (metacls->superclass->hasCustomAWZ()) {
metaCustomAWZ = YES;
inherited = YES;
}
else {auto& methods = metacls->data()->methods;
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{if (methodListImplementsAWZ(*mlists)) {
metaCustomAWZ = YES;
inherited = NO;
break;
}
}
}
if (!metaCustomAWZ) metacls->setHasDefaultAWZ();
if (PrintCustomAWZ && metaCustomAWZ) metacls->printCustomAWZ(inherited);
bool clsCustomRR = NO;
if (ClassNSObjectRRSwizzled) {
clsCustomRR = YES;
inherited = NO;
}
// 查找元类是否实现 MRC 方法,如果是则进入 if 语句中
if (cls == classNSObject()) {auto& methods = cls->data()->methods;
for (auto mlists = methods.beginCategoryMethodLists(),
end = methods.endCategoryMethodLists(cls);
mlists != end;
++mlists)
{if (methodListImplementsRR(*mlists)) {
clsCustomRR = YES;
inherited = NO;
break;
}
}
}
else if (!cls->superclass) {
clsCustomRR = YES;
inherited = NO;
}
else if (cls->superclass->hasCustomRR()) {
clsCustomRR = YES;
inherited = YES;
}
else {
// 查找类是否实现 MRC 方法,如果是则进入 if 语句中
auto& methods = cls->data()->methods;
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{if (methodListImplementsRR(*mlists)) {
clsCustomRR = YES;
inherited = NO;
break;
}
}
}
if (!clsCustomRR) cls->setHasDefaultRR();
if (PrintCustomRR && clsCustomRR) cls->printCustomRR(inherited);
metacls->changeInfo(RW_INITIALIZED, RW_INITIALIZING);
}
需要注意的是,initialize
方法和 load
方法不太一样,Category
中定义的 initialize
方法会覆盖原方法而不是像 load
方法一样都可以调用。
简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github
上,下载 Runtime PDF
合集。把所有 Runtime
文章总计九篇,都写在这个 PDF
中,而且左侧有目录,方便阅读。
下载地址:Runtime PDF
麻烦各位大佬点个赞,谢谢!????