在 APP加载流程探索之dyld 中,我们探讨了dyld动态链接的流程,当时使用的源码是dyld-852.2,里面是dyld3版本。
我们现在用dyld-941.5再来看一个小地方。运行项目,在ViewController中断点,bt一下,看栈信息:
看到这里用的是dyld4,其中有个notifyObjCInit方法,到源码查找一下,发现是在RuntimeState::setObjCNotifiers方法里赋的值,查找setObjCNotifiers,发现它也是在_dyld_objc_notify_register方法中调用的,跟dyld3中是一样的。所以,从源码上看,dyld3和dyld4虽然有一点区别,但是整体的逻辑还是一样的。
_objc_init
在 APP加载流程探索之dyld 中说到,_dyld_objc_notify_register有三个参数:
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
那这三个参数具体做了什么呢?在探讨这个问题之前,我们先看看_objc_init方法里都有啥。
我们在_objc_init方法里看到,在调用_dyld_objc_notify_register之前,还有一堆带有“init”的方法,我们看看它们是干什么的。
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
这个方法主要是做环境变量的初始化。
我们首先在Xcode中打开项目的Edit Scheme:
在跳出来的弹框中,添加两个参数,OBJC_PRINT_OPTIONS和OBJC_HELP,value都设置为YES:
之后再运行项目时,environ_init方法就能拿到这两个值,并赋值给PrintHelp和PrintOptions变量,项目就会打印这些环境变量。
看看打印的结果:
这里打印出了很多的环境变量,以及对应的说明。我们拿其中的一个变量OBJC_PRINT_LOAD_METHODS举例子,它后面的说明是”log calls to class and category +load methods“,就是说如果设置了OBJC_PRINT_LOAD_METHODS变量为YES,那么就会把项目中所有实现了+(void)load方法的类打印出来。我们来试一下,创建一个XXPerson类,实现load方法,结果打印如下:
确实把XXPerson类打印出来了。
由于环境变量数量较多,就不一一解释了,这里有张环境变量列表:
tls_init
这个方法是为每一个线程创建一个析构方法。 其实系统是为每一个线程都开辟了独立的空间,来存储线程的数据。这一部分的内容以后再介绍,本文先一笔带过。
static_init
static_init()方法主要是去运行c++的静态构造函数,走到init()的时候会走到上面的构造函数function()。
_objc_init是在dyld调用静态构造方法之前,如果我们在自己写的类里面也添加这个function()方法,那么代码就会先走static_init()这里的function(),然后走我们自己的类中的load()方法,再走我们的类中的function()方法。
runtime_init
runtime_init方法主要是创建两张表:一张是分类表,一张是类表。后面分类的加载和类的加载,就会分别往这两张表中添加数据。
exception_init
这个方法主要是初始化libobjc的异常处理系统,比如数组越界、找不到方法实现等问题的报错,都是在这里初始化的。
cache_t::init
初始化相关缓存。
_dyld_objc_notify_register
以上几个方法走完,就到了我们今天的重点了。因为第三个参数unmap_image是在库被删除时调用的,所以这里我们暂时不讲它。
load_images
点击进入load_images方法:
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();
}
找到prepare_load_methods((const headerType *)mh),看注释Discover load methods,是要找load方法,点进去看看:
这个方法主要是做两步工作:
1、找到非懒加载的类,记录下来
2、找到非懒加载的分类,记录下来
_getObjc2NonlazyClassList就是找到所有非懒加载类。
什么叫非懒加载?就是实现了+(void)load方法的类。找到所有非懒加载类,然后遍历,对每个类执行schedule_class_load(remapClass(classlist[i]));
schedule_class_load方法内部会进行递归的调用,一直对参数中的类的父类进行递归调用。而且是先找到父类,再执行add_class_to_loadable_list方法。
add_class_to_loadable_list是把类存到一个“可加载的类”的表中,所以如果一个类实现了+(void)load方法,那么它的继承链向上的那些父类都会被记录到这个表中,每个类存储的信息是类和load方法IMP。
在记录非懒加载的分类时,存到“可加载的类”的表中的是:分类和load方法IMP。
当prepare_load_methods执行完毕后,就走到call_load_methods方法了。这个方法做的事情很简单,就是:去上面说到的存储非懒加载类和分类的表中,
1、找到类,执行其load方法
2、找到分类,执行其load方法
至此,我们知道了,load_images方法主要是针对非懒加载类及其父类,和非懒加载分类的load进行了调用,它们调用的顺序是:父类 → 子类 → 分类
map_images
map_images方法里执行了map_images_nolock方法,点击进去,这个方法在第一次调用时会执行preopt_init方法,是关于共享缓存优化的处理。
继续走,有个while方法,这个while主要做的是找到mach-o文件里面的所有的类。mach-o文件的头部有个配置文件,这个配置文件描述了mach-o文件里哪一部分是代码哪一部分是数据,dyld就是根据它来读取mach-o文件的。这段代码没必要细看,知道它的作用就行。
{
uint32_t i = mhCount;
while (i--) {
const headerType *mhdr = (const headerType *)mhdrs[i];
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
// no objc data in this entry
continue;
}
if (mhdr->filetype == MH_EXECUTE) {
// Size some data structures based on main executable's size
#if __OBJC2__
// If dyld3 optimized the main executable, then there shouldn't
// be any selrefs needed in the dynamic map so we can just init
// to a 0 sized map
if ( !hi->hasPreoptimizedSelectors() ) {
size_t count;
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
}
#else
_getObjcSelectorRefs(hi, &selrefCount);
#endif
#if SUPPORT_GC_COMPAT
// Halt if this is a GC app.
if (shouldRejectGCApp(hi)) {
_objc_fatal_with_reason
(OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
OS_REASON_FLAG_CONSISTENT_FAILURE,
"Objective-C garbage collection "
"is no longer supported.");
}
#endif
}
hList[hCount++] = hi;
if (PrintImages) {
_objc_inform("IMAGES: loading image for %s%s%s%s%s\n",
hi->fname(),
mhdr->filetype == MH_BUNDLE ? " (bundle)" : "",
hi->info()->isReplacement() ? " (replacement)" : "",
hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "",
hi->info()->optimizedByDyld()?" (preoptimized)":"");
}
}
}
读取完所有的类以后,要执行两个方法:
sel_init:初始化c++的构造函数和析构方法;
arr_init:自动释放池的初始化、散列表的初始化、关联对象的相关初始化
之后就会走_read_images方法,这个方法要重点看下。
_read_images方法的代码很长,我就挑着说了,首先看到这个方法里有很多的fix up的log:
ts.log("IMAGE TIMES: fix up selector references");
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
这是因为现在使用的是虚拟内存,每次app启动时,可执行文件或者动态库的地址都是随机生成的,这个时候就需要dyld通过rebase和binding两步操作,来修复镜像中的资源指针,让其指向正确的地址。
继续看代码,第一次进入这个方法会默认打开指针优化:
DisableNonpointerIsa = true;
接着,根据环境变量,设置是否使用存储小对象的taggedPointer,taggedPointer会在以后专门详细讲解。
if (DisableTaggedPointers) {
disableTaggedPointers();
}
之后调用NXCreateMapTable方法,这个方法会创建一个表,用来存放不在dyld共享缓存中的一些类。这个我们暂时不管。
再往下,这段代码是对所有方法的修复:
看打印的信息:
sels里的地址之所以是错的,原因也是我们上面提到的虚拟内存的问题。
接着是获取所有的要加载的类,逐个进行加载:
在readClass方法里,为了让代码断在我自己写的类XXTeacher中,我添加了一段代码:
断点断在XXTeacher类时,一步一步往下调试,发现并没有对这个类的rw、ro等进行操作,而是停在了这一行:
看addClassTableEntry方法代码:
static void
addClassTableEntry(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.
auto &set = objc::allocatedClasses.get();
ASSERT(set.find(cls) == set.end());
if (!isKnownClass(cls))
set.insert(cls);
if (addMeta)
addClassTableEntry(cls->ISA(), false);
}
我们看到allocatedClasses,它就是上面runtime_init方法创建的类表,addClassTableEntry方法就是将类存入这个类表中。这个方法是个递归方法,也会把类的元类存到这个表中。
再回到_read_images方法中,在将类存到表中以后,我们看到后面还有实现非懒加载类的方法,代码会走到这里:
这个方法是真正的去给类创建rw空间,给类的data赋值,在之前的文章里也说过,rw是运行时创建的。
上图中,我也让代码断在我自己写的XXTeacher类,所以我们可以查看一下这个类的数据是不是都存在了。
我们看到,确实拿到了XXTeacher类的属性和方法。
至此,我们今天的讲解先告一段落。这篇文章简单地介绍了load_images和map_images两个方法做了哪些事情。它们都是对非懒加载类和分类进行操作,至于懒加载类是怎么加载的,我们下一篇再介绍。