这是我参与8月更文挑战的第3天,活动详情查看: 8月更文挑战
在之前应用程序加载的文章中我们梳理了dyld
的加载流程,接下来我们以_objc_init
为入口,详细分析代码逻辑;
objc_init分析
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
// 环境变量的初始化 终端打开环境变量帮助 export OBJC_HELP=1
environ_init();
// 关于线程key的绑定,比如线程数据的析构函数
tls_init();
// 系统级C++全局静态函数调用,在dyld调用我们的静态构造函数之前,lldb会调用_objc_init,因此我们必须自己做
static_init();
// 运行时环境初始化
runtime_init();
// 异常处理系统初始化
exception_init();
#if __OBJC2__
// 缓存条件初始化 只在OBjective-C++ 2.0版本中生效
cache_t::init();
#endif
// 启动回调机制,通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载tramplienes dylib
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
复制代码
environ_init()
:环境变量的初始化 终端打开环境变量帮助指令为:export OBJC_HELP=1
;tls_init()
:关于线程key的绑定,比如线程数据的析构函数;static_init()
:系统级C++全局静态函数
调用,在dyld
调用我们的静态构造函数
之前,lldb
会调用_objc_init
,因此我们必须自己做;runtime_init()
:运行时环境初始化;exception_init()
:异常处理系统初始化;cache_t::init()
:缓存条件初始化 只在OBjective-C++ 2.0
版本中生效;_imp_implementationWithBlock_init
:启动回调
机制,通常不会做什么,因为所有的初始化都是惰性的
,但是对于某些进程,我们会迫不及待的加载tramplienes dylib
;
environ_init
主要是运行时环境变量的一些初始化操作,这些环境变量可以在调试的时候给我们提供帮助,源码如下:
其中核心代码是最后的for
循环,将其抽出,我们看一下它的打印结果:
该方法会打印出我们可能会用到的一些环境变量,这些环境变量会在我们开发进行调试时提供帮助;
除了循环打印外,我们可以在终端通过export OBJC_HELP=1
来打印环境变量:
这些环境变量,均可以通过target
— Edit Scheme
— Run
—Arguments
— Environment Variables
中进行配置;
我们以OBJC_DISABLE_NONPOINTER_ISA
为例:
环境变量设置之前,Person
对象的isa
设置环境变量:
查看Person
对象的isa
:
OBJC_DISABLE_NONPOINTER_ISA
可以控制是否是nonpointer_isa
我们再来尝试一下OBJC_PRINT_LOAD_METHODS
这个环境变量:
运行程序:
OBJC_PRINT_LOAD_METHODS
环境变了可以打印出项目中所有的load
方法,为我们的优化提供思路(load方法会影响程序启动)
tls_init
线程key的绑定,比如线程数据的析构函数,源码如下:
static_init
系统级C++全局静态函数
调用,在dyld
调用我们的静态构造函数
之前,llbc
会调用_objc_init
,因此我们必须自己做,源码如下:
在之前的分析中,我们分析出C++构造函数
会在doInitialization
->doModInitFunctions
的流程中被调用,那么为什么此处还会自己在调用呢?根据注释我们知道,_objc_init
会比dyld
更早的调用objc
自己的静态构造函数;因为dyld
执行的过程中,需要以来一些底层库,为了能够让dyld
及时执行,所以提前调用了C++构造函数
;
我们用代码验证一下,我们在_objc_init
方法上边添加一个C++构造函数
:
断点执行源码程序:
继续执行:
可以看到C++构造函数
确实是在此方法中被调用的;
runtime_init
运行时环境初始化,源码如下:
通过init
方法,我们知道unattachedCategories
和allocatedClasses
是两张表,后续会详细讲解;
exception_init
异常处理系统的初始化,源码如下:
_objc_terminate
对应的实现为:
crash
是指系统发生的一些异常的指令之后,发出的信号;当有crash
发生时,会来到_objc_terminate
方法,然后在uncaught_handler
中抛出异常;
在App
层我们可以定义一个函数,将函数赋值给fn
也就是uncaught_handler
,这样,在App
中我们就能通过这个函数接收跑出的异常,并进行处理;
crash
处理的流程:
cache_t::init
缓存条件的初始化,源码如下:
_imp_implementationWithBlock_init
启动回调机制,通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载tramplienes dylib,源码如下:
_dyld_objc_notify_register
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
复制代码
这里有个问题,为什么map_images
前边加了&
,而load_images
方法前边你没有呢?
是因为load_images
方法中的操作比较简单,只是调用了load
方法,而map_images
中的操作就相对复杂;&
表示取地址,&map_images
表示指针传递;这样做有什么好处呢?由于map_images
需要映射镜像文件,操作比较耗时,指针传递能确保map_images
调用保持同步;
map_images
中调用了map_images_nolock
,该方法主要实现如下:
void
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;
// Perform first-time initialization if necessary.
// This function is called before ordinary library initializers.
// fixme defer initialization until an objc-using image is found?
if (firstTime) {
// 相关初始化
preopt_init();
}
if (PrintImages) {
_objc_inform("IMAGES: processing %u newly-mapped images...\n", mhCount);
}
// Find all images with Objective-C metadata.
// 查找所有带有Objective-C元数据的映像
hCount = 0;
// Count classes. Size various table based on the total.
// 计算类的个数,取决于表的总大小
int totalClasses = 0;
int unoptimizedTotalClasses = 0;
{
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)":"");
}
}
}
// Perform one-time runtime initialization that must be deferred until
// the executable itself is found. This needs to be done before
// further initialization.
// (The executable may not be present in this infoList if the
// executable does not contain Objective-C code but Objective-C
// is dynamically loaded later.
if (firstTime) {
sel_init(selrefCount);
arr_init();
#if SUPPORT_GC_COMPAT
// Reject any GC images linked to the main executable.
// We already rejected the app itself above.
// Images loaded after launch will be rejected by dyld.
for (uint32_t i = 0; i < hCount; i++) {
auto hi = hList[i];
auto mh = hi->mhdr();
if (mh->filetype != MH_EXECUTE && shouldRejectGCImage(mh)) {
_objc_fatal_with_reason
(OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
OS_REASON_FLAG_CONSISTENT_FAILURE,
"%s requires Objective-C garbage collection "
"which is no longer supported.", hi->fname());
}
}
#endif
#if TARGET_OS_OSX
// Disable +initialize fork safety if the app is too old (< 10.13).
// Disable +initialize fork safety if the app has a
// __DATA,__objc_fork_ok section.
// if (!dyld_program_sdk_at_least(dyld_platform_version_macOS_10_13)) {
// DisableInitializeForkSafety = true;
// if (PrintInitializing) {
// _objc_inform("INITIALIZE: disabling +initialize fork "
// "safety enforcement because the app is "
// "too old.)");
// }
// }
for (uint32_t i = 0; i < hCount; i++) {
auto hi = hList[i];
auto mh = hi->mhdr();
if (mh->filetype != MH_EXECUTE) continue;
unsigned long size;
if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) {
DisableInitializeForkSafety = true;
if (PrintInitializing) {
_objc_inform("INITIALIZE: disabling +initialize fork "
"safety enforcement because the app has "
"a __DATA,__objc_fork_ok section");
}
}
break; // assume only one MH_EXECUTE image
}
#endif
}
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]);
}
}
}
复制代码
preopt_init()
:初始化相关环境;hCount
:存放所有带有Objective-C元数据的镜像
的个数,其在while
循环时的hList[hCount++] = hi
进行hCount++
赋值操作;totalClasses
:存放类
的个数,类
的个数取决于表
的总大小;_read_images
:加载镜像文件;loadImageFuncs
:所有工作准备就绪之后,调用镜像文件加载功能;
那么在这里,镜像文件
究竟是如何被加载的呢?其核心是方法
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
复制代码
read_image流程(重点)
此方法看上去较为复杂,那么我们将所有的判断分支关闭,来整体看一下这个函数:
如此一看,就比较清晰了,里边有很多log
信息,根据log
信息大致分析出_read_images
函数的主要流程:
- 1.条件控制进行一次的加载
- 2.修复预编译阶段的
@selector
的混乱问题 - 3.错误混乱的类处理
- 4.修复重映射一些没有被镜像文件加载进来的类
- 5.修复一些消息
- 6.当类中有协议的时候的处理:
readProtocol
- 7.修复没有被加载的协议
- 8.分类的处理
- 9.类的加载处理
- 10.没有被处理的类,优化那些被删除没有回收的类(未来类)
下边我们逐步分析其流程:
1.条件控制进行一次的加载
initializeTaggedPointerObfuscator
:小对象类型,里边主要做了一些混淆的操作,不作为重点;namedClassesSize
:计算创建表格需要的大小,此处是之前cache
中3/4的反向计算,计算容量NXCreateMapTable
:创建一张哈希表
,用来存放类信息,表的大小为namedClassesSize
;
通过NXCreateMapTable
创建了一张哈希表
,用来存放类信息
;
gdb_objc_realized_classes
的解释如下:
// This is a misnomer: gdb_objc_realized_classes is actually a list of
// named classes not in the dyld shared cache, whether realized or not.
// This list excludes lazily named classes, which have to be looked up
// using a getClass hook.
NXMapTable *gdb_objc_realized_classes; // exported for debuggers in objc-gdb.h
复制代码
意思是,这个哈希表
用来存储不在共享缓存且已命名的类
,无论类是否实现,总容量是类的数量的4/3;
2.修复预编译阶段的@selector
的混乱问题
修复
@selector
引用,sel
是一串带地址的字符串
_getObjc2SelectorRefs
: 通过_getObjc2SelectorRefs
拿到MachO
中的静态段__objc_selrefs
;sel_registerNameNoLock
: 注册sel
,将其添加到哈希表namedSelectors
中;sels[i] = sel
:从MachO
中读取的sel
的地址不是真实的地址,需要重新赋值,以从dyld
中读取的sel
地址为准;
此代码块主要是通过_getObjc2SelectorRefs
拿到MachO
中的静态段__objc_selrefs
,遍历列表,通过sel_registerNameNoLock
将sel
添加到哈希表中,而sel_registerNameNoLock
是从dyld
中读取的数据;
_getObjc2SelectorRefs
的源码如下:
GETSECT(_getObjc2SelectorRefs, SEL, "__objc_selrefs");
复制代码
通过它可以获取到MachO
中的静态段__objc_selrefs
;
发现同样的retain
方法,两个地址却不一样,所以需要将地址重新赋值;
3.错误混乱的类处理(类有了名字和地址)
Class cls = (Class)classlist[i]
:获取cls
,此时cls
只是一个地址;readClass
:读取类,在此之后cls
才会真正的具备名字;
断点打印cls
:
执行过readClass
之后:
通过断点调试发现,经过readClass
步骤之后,类有了地址
和名字
;
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;
}
复制代码
这段代码经过断点发现,并不会执行;此处是处理一些未来类
,什么是未来类
呢?根据注释可以知道已经被移动过,应该删除却没有删除的类称为未来类
,这部分类需要在此处进行处理;
4.修复重映射一些没有被镜像文件加载进来的类
主要是将未映射的Class
和Super Class
进行重新映射:
_getObjc2ClassRefs
用来获取MachO
中静态段__objc_classrefs
,即获取类的引用
;_getObjc2SuperRefs
用来获取MachO
中静态段__objc_superrefs
,即获取父类的引用
;
通过注释可知,被remapClassRef
操作的类都是懒加载的类
;
5.修复一些消息
_getObjc2MessageRefs
:获取MachO
的静态段__objc_msgrefs
;fixupMessageRef
:将函数指针进行注册,并修复为新指针;
通过_getObjc2MessageRefs
获取MachO
中的__objc_msgrefs
,遍历并通过fixupMessageRef
将函数指针进行注册,并修复为新指针
6.当类中有协议的时候的处理:readProtocol
遍历所有协议列表,并且将协议列表加载到Protocol的哈希表中
Class cls = (Class)&OBJC_CLASS_$_Protocol;
:cls = Protocol类,所有协议和对象的结构体都类似,isa都对应Protocol类NXMapTable *protocol_map = protocols();
创建protocol哈希表
,表名为protocol_map
_getObjc2ProtocolList
:通过_getObjc2ProtocolList
获取到MachO中
的静态段__objc_protolist
协议列表,即从编译器中读取并初始化protocol
readProtocol
:循环遍历,通过readProtocol
方法将协议添加到protocol_map
哈希表中
7.修复没有被加载的协议
_getObjc2ProtocolRefs
:获取到MachO的静态段 __objc_protorefsremapProtocolRef
:比较当前协议和协议列表中的同一个内存地址的协议是否相同,如果不同则替换
remapProtocolRef
方法实现源码:
/***********************************************************************
* remapProtocolRef
* Fix up a protocol ref, in case the protocol referenced has been reallocated.
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static size_t UnfixedProtocolReferences;
static void remapProtocolRef(protocol_t **protoref)
{
runtimeLock.assertLocked();
// 获取协议列表中统一内存地址的协议
protocol_t *newproto = remapProtocol((protocol_ref_t)*protoref);
if (*protoref != newproto) { // 如果当前协议 与 同一内存地址协议不同,则替换
*protoref = newproto;
UnfixedProtocolReferences++;
}
}
复制代码
主要通过_getObjc2ProtocolRefs
获取MachO
中的__objc_protorefs
,然后遍历需要修复的协议,通过remapProtocolRef
方法比较当前协议和协议列表中的同一个内存地址的协议是否相同,不同则替换;
8.分类的处理
主要用来处理分类,需要在分类初始化并将数据加载到类之后才能执行,对于运行时的分类,会在_dyld_objc_notify_register
的执行完成之后的第一个load_images
调用之后发现
9.类的加载处理(返回一个类的真实数据结构)
实现非懒加载的类,对于load方法和静态实例变量
nlclslist
方法源码:
const classref_t *header_info::nlclslist(size_t *outCount) const
{
#if __OBJC2__
// This field is new, so temporarily be resilient to the shared cache
// not generating it
if (isPreoptimized() && hasPreoptimizedSectionLookups()) {
*outCount = nlclslist_count;
const classref_t *list = (const classref_t *)(((intptr_t)&nlclslist_offset) + nlclslist_offset);
#if DEBUG
size_t debugCount;
assert((list == _getObjc2NonlazyClassList(mhdr(), &debugCount)) && (*outCount == debugCount));
#endif
return list;
}
return _getObjc2NonlazyClassList(mhdr(), outCount);
#else
return NULL;
#endif
}
复制代码
addClassTableEntry
方法源码:
/***********************************************************************
* addClassTableEntry
* Add a class to the table of all classes. If addMeta is true,
* automatically adds the metaclass of the class as well.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
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);
}
复制代码
- 通过
_getObjc2NonlazyClassList
获取MachO
的静态段__objc_nlclslist
,即非懒加载类的表 addClassTableEntry(cls);
将类及其元类插入表realizeClassWithoutSwift
对类进行初始化(第三步中只有名字和地址,data数据并没有被加载),分配读写数据(比如rw),返回一个类的真实结构
注意,此处操作的为非懒加载类
10.没有被处理的类,优化那些被删除没有回收的类(未来类)
realizeClassWithoutSwift
:实现类realizeAllClasses
:实现所有类
通过对流程的梳理,发现其核心内容在第3步的
readClass
和第9步realizeClassWithoutSwift
两个方法