程序的加载原理
代码的编译过程
我们编写完成代码是需要通过编译器来完成编译后,才能变成可以执行的文件,也就是我们通常说的可执行文件。
那么编译过程是怎样的呢,下面我们来通过流程图为大家分析一下:
程序的执行,就是把可执行的文件加载到内存中进行执行。我们将可执行文件叫做Mach-O,Mach-O的运行需要依赖运行库(.a .lib .so),运行库分为动态库和静态库,这些库是可执行的二进制文件,能够被加载倒内存中。
静态库和动态库
- 静态库:例如.a和.framework。静态库链接时,会被完整地复制到可执行文件中,被使用到了多次时,就会被复制到内存中多次,这样就会产生拷贝冗余,造成内存浪费。
- 动态库:例如.lib和.framework。动态库链接时,只会存在一份,不会复制多份。动态库在内存中是共享的,系统只加载一次,加载完成后谁用就会去引用这块内存,这样就既节省了时间又省了内存。
- 静态库就是我们常说的值拷贝,而动态库就是我们常说的地址拷贝。
- 图解

动态链接库加载倒内存的过程就是由dyld(dynamic link editor)动态链接器完成的。
dyld动态连接器
dyld的介绍:dyld是iOS操作系统的一个重要组成部分,再系统内核做好程序准备工作之后,会交由dyld负责余下的工作。
- dyld的作用:加载各个库,也就是image镜像文件,由dyld从内存中读到列表中,加载主程序,link链接各个动静态库,进行主程序的初始化工作。
- dyld工作流程图如下,图中简单描述了动态库的注册和动态库的加载过程,具体的分析还需要看底层的源码,此处暂时不做深入探究。
_objc_init与dyld
_objc_init源码
首先我们再objc源码中全局搜索_objc_init,在objc_os.mm文件下可以看到实现源码如下:
void _objc_init(void){static bool initialized = false;if (initialized) return;initialized = true;// fixme defer initialization until an objc-using image is found?// 读取影响运行时的环境变量// 可以再Edite Scheme中的Environment Variable中进行配置,来打印环境变量帮助environ_init();// 关于线程key的绑定tls_init();// 运行C++静态构造函数 在dyld调用我们的静态构造函数之前,libc会调用objc_init,因此我们必须自己做static_init();// runtime运行时环境变量初始化runtime_init();// 初始化libobjc的异常处理方法exception_init();#if __OBJC2__// 缓存条件初始化cache_t::init();#endif// 启动回调机制_imp_implementationWithBlock_init();// 注册dyld_dyld_objc_notify_register(&map_images, load_images, unmap_image);#if __OBJC2__didCallDyldNotifyRegister = true;#endif}
根据源码所知,主要分为一下几部分:
- environ_init:初始化一系列的环境变量,病读取影响运行的环境变量
- tls_init:关于线程key的绑定
- static_init:运行C++静态构造函数(只会运行系统级别的构造函数),在dyld调用静态析构函数之前,libc会调用_objc_init
- runtime_init:runtime运行时环境初始化,里面操作是unattachedCategories、allocatedClasses表的初始化
- exception_init:初始化libobjc的异常处理系统
- cache_t::init:cache缓存初始化
- _imp_implementationWithBlock_init:启动回调机制,通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib
_dyld_objc_notify_register:dyld的注册
- 仅供objc运行时使用。注册应用程序,以便再映射、取消映射和初始化objc镜像文件时使用,dyld将使用包含objc_image_info的镜像文件数组,回调mapped函数。
- map_images:dyld将image镜像文件加载进内存时,会触发该函数
- load_image:dyld初始化image会触发该函数
unmap_image:dyld将image移除时会触发该函数
environ_init方法:环境变量初始化
environ_init源码如下,我截取了比较关键的部分,此处主要循环打印环境变量信息
void environ_init(void){// ……省略// Print OBJC_HELP and OBJC_PRINT_OPTIONS output.if (PrintHelp || PrintOptions) {// …… 省略// 核心代码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);}}}
环境变量打印
方式一:修改源码的方式进行打印

首先我们移除掉内层和外层的if条件,只保留上述代码块;
- 运行我们的objc源码项目;
- 查看控制台打印结果,会打印出所有的环境变量信息
-
方式二:通过终端命令打印环境变量
需要再终端中输入export OBJC_HELP=1指令,打印环境变量
方式三:打印指定的环境变量信息
设置方式:
可以通过target — Edit Scheme — Run —Arguments — Environment Variables配置,其中常用的环境变量主要有以下几个: DYLD_PRINT_STATISTICS:设置DYLD_PRINT_STATISTICS为YES,控制台就会打印APP的加载时长,包括整体加载时长和动态库加载时长,即main函数之前dyld的处理时间,可以通过设置了解耗时部分,并对其进行启动优化。
- OBJC_DISABLE_NONPOINTER_ISA:杜绝生成相应的nonpointer isa制(nonpointer isa 优化后的isa,指针地址未优化的isa末尾为1,优化后的isa末尾为0),生成的都是普通的isa
- OBJC_PRINT_LOAD_METHODS:打印Class即Category的load方法的调用信息
- NSDoubleLocalizedStrings:项目国际化本地化的时候时一个耗时的工作,想要检测国际化翻译好的文字UI会变成什么样子,可以指定这个启动项,可以设置NSDoubleLocalizedStrings为YES。
- NSShowNonLocalizedStrings:在完成国际化的时候,偶尔会有一些字符串没有做本地化,这时就可以设置NSShowNonLocalizedStrings为YES,所有没有本地化的字符串全都会变成大写。
OBJC_DISABLE_NONPOINTER_ISA
以OBJC_DISABLE_NONPOINTER_ISA为例,将其设置为YES
未设置OBJC_DISABLE_NONPOINTER_ISA时打印objc对象,isa地址的二进制末尾为1
设置OBJC_DISABLE_NONPOINTER_ISA时打印机objc对象,isa地址的二进制末尾为0
所以OBJC_DISABLE_NONPOINTER_ISA可以控制isa开关,从而优化整个内存结构tls_init:线程key的绑定
主要是本地线程池的初始化以及析构,源码:void tls_init(void){#if SUPPORT_DIRECT_THREAD_KEYSpthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);#else_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);#endif}
static_init:运行系统级别的C++静态构造函数
主要是运行系统级别的C++静态构造函数,在dyld调用我们的静态构造函数之前,libc调用_objc_init方法,即系统级别的C++构造函数,自定义的C++构造函数运行,源码: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();}}
runtime_init:运行时环境初始化
主要是运行时的初始化,主要分为两部分:分类初始化、类的表初始化,源码:void runtime_init(void){objc::unattachedCategories.init(32);objc::allocatedClasses.init();}
exception_init:初始化libobjc的异常处理系统
主要是初始化libobjc的异常处理系统,注册异常处理的回调,从而监控异常的处理,源码: ```objectivec
void exception_init(void) { old_terminate = std::set_terminate(&_objc_terminate); }
- 当有crash发生时,就会到old_terminate方法,最终走到uncaught_handler进行异常抛出- crash:是指系统发生了一下不被允许的指令,然后系统会发出的一个信号- old_terminate源码```objectivecstatic 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)();}}}
- 搜索uncaught_handler,我们发现是一个全局静态变量,是在应用层通过objc_setUncaughtExceptionHandler传入一个异常处理的回调方法,并在该方法内将异常处理方法赋值给uncaught_handler。 ```objectivec
// uncaught_handler初始 static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
// APP传入处理异常用的回调方法 objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn) { objc_uncaught_exception_handler result = uncaught_handler; uncaught_handler = fn; return result; }
<a name="COqJj"></a>### cache_init:缓存初始化主要是缓存初始化,源码:```objectivecvoid cache_t::init(){#if HAVE_TASK_RESTARTABLE_RANGESmach_msg_type_number_t count = 0;kern_return_t kr;while (objc_restartableRanges[count].location) {count++;}kr = task_restartable_ranges_register(mach_task_self(),objc_restartableRanges, count);if (kr == KERN_SUCCESS) return;_objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",kr, mach_error_string(kr));#endif // HAVE_TASK_RESTARTABLE_RANGES}
_imp_implementationWithBlock_init:启动回调机制
该方法主要是启动回调机制,通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载libobjc-trampolines.dylib,源码
_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}
_dyld_objc_notify_register:dyld注册
_dyld_objc_notify_register方法
这个方法是dyld的注册方法,其源码是在dyld源码中,下面是_dyld_objc_notify_register的声明
//// 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镜像是调用
dyld将会通过一个包含objc-image-info的镜像文件的数组回调mapped函数
_dyld_objc_notify_register调用
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
方法中的三个参数含义:
&map_images:dyld将image镜像文件加载倒内存中,会触发该函数
- load_image:dyld初始化image会触发该函数
- unmap_image:dyld将image移除时,会触发该函数
load_image方法其实就是调用load方法,map_image方法,&map_image是指针传递,指向是同一块实现的地址,如果有什么变化就可以第一时间知道。
接下来我们就研究一下map_images
map_images
map_images源码
首先,我们先来看一下map_images的源码
voidmap_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方法,下面我们看看map_images_nolock源码
map_images_nolock源码
map_images_nolock(unsigned mhCount, const char * const mhPaths[],const struct mach_header * const mhdrs[]){static bool firstTime = YES;header_info *hList[mhCount];uint32_t hCount;size_t selrefCount = 0;// …………省略if (hCount > 0) {_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);}firstTime = NO;// Call image load funcs after everything is set up.for (auto func : loadImageFuncs) {for (uint32_t i = 0; i < mhCount; i++) {func(mhdrs[i]);}}}
map_images_nolock中的关键就在read_image方法,加下来我们就来完整的看一下read_image具体做了什么
read_image
read_image源码
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;bool launchTime = NO;TimeLogger ts(PrintImageTimes);runtimeLock.assertLocked();hIndex = 0; \hIndex < hCount && (hi = hList[hIndex]); \hIndex++// 条件控制进行一次加载if (!doneOnce) {……}// Fix up @selector references// 修复预编译阶段的@selector的混乱问题// 就是不同类中有相同方法 但是相同方法的地址是不一样的static size_t UnfixedSelectors;{……}ts.log("IMAGE TIMES: fix up selector references");// Discover classes. Fix up unresolved future classes. Mark bundle classes.// 错误混乱的类处理bool hasDyldRoots = dyld_shared_cache_some_image_overridden();for (EACH_HEADER) {……}ts.log("IMAGE TIMES: discover classes");// Fix up remapped classes// Class list and nonlazy class list remain unremapped.// Class refs and super refs are remapped for message dispatching.// 修复重映射一些没有被镜像文件加载进来的类if (!noClassesRemapped()) {……}ts.log("IMAGE TIMES: remap classes");#if SUPPORT_FIXUP// Fix up old objc_msgSend_fixup call sites// 修复一些消息for (EACH_HEADER) {……}ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");#endif// Discover protocols. Fix up protocol refs.// 当类中有协议时 readProtocolfor (EACH_HEADER) {……}ts.log("IMAGE TIMES: discover protocols");// Fix up @protocol references// Preoptimized images may have the right// answer already but we don't know for sure.// 修复没有被加载的协议for (EACH_HEADER) {……}ts.log("IMAGE TIMES: fix up @protocol references");// 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);}}ts.log("IMAGE TIMES: discover categories");// Category discovery MUST BE Late to avoid potential races// when other threads call the new category code before// this thread finishes its fixups.// +load handled by prepare_load_methods()// Realize non-lazy classes (for +load methods and static instances)// 类的加载处理for (EACH_HEADER) {……}ts.log("IMAGE TIMES: realize non-lazy classes");// Realize newly-resolved future classes, in case CF manipulates them// 没有被处理的类 优化那些被侵犯的类if (resolvedFutureClasses) {……}ts.log("IMAGE TIMES: realize future classes");#undef EACH_HEADER}
从整体上可以看出read_image是对一些log日志的打印输出,主要如下:
- 条件控制运行一次加载
- 修复预编译阶段的@selector的混乱的问题
- 错误混乱的类处理
- 修复重映射一些没有被镜像文件加载进来的类
- 修复一些消息
- 当类中有协议时 readProtocol
- 修复没有被加载的协议
- 分类的处理
- 类的加载处理
- 没有被处理的类,优化那些被侵犯的类
doneOnce
源码
if (!doneOnce) {doneOnce = YES;launchTime = YES;// ……省略// namedClasses// Preoptimized classes don't go in this table.// 4/3 is NXMapTable's load factorint namedClassesSize =(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;// 创建哈希表 存放所有类gdb_objc_realized_classes =NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);ts.log("IMAGE TIMES: first time tasks");}
加载一次下次就不会再次进入判断。只有第一次进来时创建表gdb_objc_realized_classes,表里存放所有的类(不管是实现的还是未实现的都存放在里面),是一张存放类的总表
UnfixedSelectors
源码
// Fix up @selector referencesstatic size_t UnfixedSelectors;{mutex_locker_t lock(selLock);for (EACH_HEADER) {if (hi->hasPreoptimizedSelectors()) continue;bool isBundle = hi->isBundle();// 从Mach-O中获取方法列表SEL *sels = _getObjc2SelectorRefs(hi, &count);UnfixedSelectors += count;for (i = 0; i < count; i++) {const char *name = sel_cname(sels[i]);// 从dyld中获取方法SEL sel = sel_registerNameNoLock(name, isBundle);if (sels[i] != sel) {sels[i] = sel;}}}}
不同类中可能存在相同的方法,但是相同的方法地址是不同的,dyld的方法是准确的,所以此处需要对Mach-O的方法进行纠正。
下面我们通过debug断电来打印一下。
sels是通过_getObjc2SelectorRefs获取Mach-O中的方法信息,Mach-O有相对位移地址偏移地址。
sel是通过sel_registerNameNoLock获取的dyld的方法信息,dyld是链接整个程序的,所以dyld是最准确的。因为方法是存放在类中,没给类中的位置是不一样的,所以方法的地址也就不一样,那么久必须对那些混乱的方法进行修复处理。
总结:
Mach-O中存储的是对当前类的相对地址,dyld是加载过程中内存中的实际地址,所以需要将dyld的imp为准进行纠正。
错误混乱的类处理
源码
for (EACH_HEADER) {if (! mustReadClasses(hi, hasDyldRoots)) {// Image is sufficiently optimized that we need not call readClass()continue;}// 从macho中读取类列表信息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;}}}
- cls:是我们从macho中读取出来的,指向的是一块内存地址。
- newCls:是我们通过readClass获取的,但是newCls有特殊性,在我们未调用readClass为其赋值时,系统会给newClas分配一块脏地址,一旦调用了readClass后我们会对其重新赋值,并且会为他设置名称,我们我对象的名称也就是在这里进行设置的。
- 下面我们通过代码来调试一下


通过上述调试,也证实了readClass的作用就是把类名和地址关联起来。
readClass
源码
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized){// 获取类名const char *mangledName = cls->nonlazyMangledName();if (missingWeakSuperclass(cls)) {……}cls->fixupBackwardDeployingStableSwift();Class replacing = nil;if (mangledName != nullptr) {……}if (headerIsPreoptimized && !replacing) {……} 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_BUNDLEsif (headerIsBundle) {……}return cls;}
关键方法:
- nonlazyMangledName获取类名
- addNamedClass将类名和地址关联起来
- addClassTableEntry将关联好的类插入到另一张哈希表中,这张表中都是已经初始化完成的类
下面我们通过对cls进行过滤,研究我们自己创建的Person类
此过程通过nonlazyMangledName获取到了类的名称
nonlazyMangledName源码:
const char *nonlazyMangledName() const {return bits.safe_ro()->getName();}
safe_ro源码:
const class_ro_t *safe_ro() const {class_rw_t *maybe_rw = data();if (maybe_rw->flags & RW_REALIZED) {// maybe_rw is rwreturn maybe_rw->ro();} else {// maybe_rw is actually roreturn (class_ro_t *)maybe_rw;}}
addNamedClass将类名和地址关联
static void addNamedClass(Class cls, const char *name, Class replacing = nil){runtimeLock.assertLocked();Class old;if ((old = getClassExceptSomeSwift(name)) && old != replacing) {inform_duplicate(name, old, cls);// getMaybeUnrealizedNonMetaClass uses name lookups.// Classes not found by name lookup must be in the// secondary meta->nonmeta table.addNonMetaClass(cls);} else {// 更新存储所有类的哈希表 key是name value是clsNXMapInsert(gdb_objc_realized_classes, name, cls);}ASSERT(!(cls->data()->flags & RO_META));// wrong: constructed classes are already realized when they get here// ASSERT(!cls->isRealized());}
通过NXMapInsert方法更新gdb_objc_realized_classes哈希表,key是name,value是cls
NXMapInsert源码:
void *NXMapInsert(NXMapTable *table, const void *key, const void *value) {MapPair *pairs = (MapPair *)table->buckets;unsigned index = bucketOf(table, key);MapPair *pair = pairs + index;if (key == NX_MAPNOTAKEY) {_objc_inform("*** NXMapInsert: invalid key: -1\n");return NULL;}unsigned numBuckets = table->nbBucketsMinusOne + 1;if (pair->key == NX_MAPNOTAKEY) {pair->key = key; pair->value = value;table->count++;if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);return NULL;}// …………省略}
addClassTableEntry源码
static voidaddClassTableEntry(Class cls, bool addMeta = true){runtimeLock.assertLocked();// This class is allowed to be a known class via the shared cache or via// data segments, but it is not allowed to be in the dynamic table already.// allocatedClasses就是_objc_alloc runtime_init中初始化的auto &set = objc::allocatedClasses.get();ASSERT(set.find(cls) == set.end());if (!isKnownClass(cls))set.insert(cls);if (addMeta)addClassTableEntry(cls->ISA(), false);}
allocatedClasses在_objc_init中进行初始化,主要是unattachedCategories(分类)和allocatedClasses(类)两张表,此时addClassTableEntry的操作是插入到allocatedClasses表中。同时还对元类进行了相应的处理。
通过对源码的分析和断电调试,我们发现rw和ro的获取和赋值并不是在readClass里面,那么重新回到_read_image继续向下看。
最终我们断电继续向下走,走到了类加载的区域发现,调用了realizeClassWithoutSwift方法,先透露一下,rw和ro的查找和赋值就是在这个方法中完成的,这个方法我们下节进行探讨
