内容简介:在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。除了在项目设置里显式出现的动态库外,还会有一些隐式存在的动态库。例如
<简书 — 刘小壮> https://www.jianshu.com/p/4fb2d7014e9e
程序加载过程
在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。
除了在项目设置里显式出现的动态库外,还会有一些隐式存在的动态库。例如 objc
和 Runtime
所属的 libobjc.dyld
和 libSystem.dyld
,在 libSystem
中包含常用的 libdispatch(GCD)
、 libsystem_c
(C语言基础库)、 libsystem_blocks(Block)
等。
使用动态库的优点:
App
加载过程
在应用程序启动后,由 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(↦_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(↦_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 麻烦各位大佬点个赞,谢谢!:grin:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
内容创业:内容、分发、赢利新模式
张贵泉、张洵瑒 / 电子工业出版社 / 2018-6 / 49
越来越多的内容平台、行业巨头、资本纷纷加入内容分发的战争中,竞争非常激烈。优质的原创性内容将成为行业中最宝贵的资源。在这样的行业形势下,如何把内容创业做好?如何提高自身竞争力?如何在这场战争中武装自己?是每一位内容创业者都应该认真考虑的问题。 《内容创业:内容、分发、赢利新模式》旨在帮助内容创业者解决这些问题,为想要进入内容行业的创业者出谋划策,手把手教大家如何更好地进行内容创业,获得更高的......一起来看看 《内容创业:内容、分发、赢利新模式》 这本书的介绍吧!