前言
上一篇文章主要分析dyld
的整个流程以及dyld
与_objc_init
之间的交互,_objc_init
向dyld
注册了回调函数,所以_objc_init
在dyld
中尤为关键,那么我们今天继续往下探讨。
准备工作
_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?
//环境的初始化
environ_init();
//析构函数的绑定
tls_init();
//全局静态c++函数调用,这里只是调用objc自己的。在dyld调用之前,相当于objc的c++构造函数是自己调用的,不是dyld调用的。
static_init();
//runtime相关的两张表的初始化
runtime_init();
//初始化 libobjc 的异常处理系统。
exception_init();
#if __OBJC2__
// //缓存初始化
cache_t::init();
#endif
//启动回调机制
_imp_implementationWithBlock_init();
//map_images:管理文件中和动态库中所有的符号 (class Protocol selector category)
//load_images:加载执行load方法
//unmap_image:释放类相关资源。
_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
调用我们的静态构造函数
之前,lib
会调用_objc_init
先调用自己的C++
构造函数。 -
runtime_init
:runtime
运行时环境初始化,里面主要是unattachedCategories
和allocatedClasses
两张表。 -
exception_init
:初始化libobjc
库的异常处理
系统。 -
cache_t::init
:缓存条件
初始化。 -
_imp_implementationWithBlock_init
:启动回调机制
。通常不会做什么,因为所有的初始化都是惰性
的,但是对于某些进程,会迫不及待的加载trampolines dylib
。 -
_dyld_objc_notify_register
: 向dyld
的注册回调。
environ_init
void environ_init(void)
{
……
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);
}
}
}
由源码可以看出打印的条件是由PrintHelp || PrintOptions
进行判断的。
日志输出
跳过PrintHelp || PrintOptions
判断,去掉这个判断修改最后的代码成如下那样:
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
_objc_inform("%s: %s", opt->env, opt->help);
_objc_inform("%s is set", opt->env);
}
运行结果:
如果没有
objc
源码的话,那么我们可以通过终端
输出日志。使用命令:
export OBJC_HELP=1
可以看出终端
输出环境日志还是比较方便的,那么我们还可以通过xcode
进行配置环境变量,操作如下:
Xcode
中环境变量配置的位置:选中运行的target
--> Edit scheme
-->Run
--> Arguments
--> Environment Variables
在环境变量中有两个配置:OBJC_DISABLE_NONPOINTER_ISA
与OBJC_PRINT_LOAD_METHODS
。
OBJC_DISABLE_NONPOINTER_ISA
OBJC_DISABLE_NONPOINTER_ISA
就是判断是否是优化
的指针。YES
表示纯指针
,NO
表示优化后的指针就是nonpointer isa
。
- Xcode环境变量中
不选择OBJC_DISABLE_NONPOINTER_ISA
,通过lldb
调试如下:
isa
的最低位是1
,表示是优化后的isa
,而且高位
上也有数据。 - Xcode环境变量中
选择上OBJC_DISABLE_NONPOINTER_ISA
,通过lldb
调试如下:
isa
的最低位是0
,表示是纯isa
,而且高位
上没有数据。
OBJC_PRINT_LOAD_METHODS
环境变量OBJC_PRINT_LOAD_METHODS
打印出程序中所有的load
方法,在自定义类中添加load
方法,配置环境变量OBJC_PRINT_LOAD_METHODS = YES
。
在
XJLPerson
类和LGPerson
类中添加+load
方法,已经在上图打印了出来。通过这样子可以检测哪位写的代码+load
方法比较多,+load
方法太多会影响程序的启动速度
。
tls_init
tls_init方法是关于线程key的绑定,比如每个线程数据的析构函数。
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
//创建线程的缓存池
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
//析构函数
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
static_init
运行C++
静态构造函数,在dyld
调用静态函数之前,libc
会调用_objc_init
方法先调用自己的C++
构造函数,也就是说libobjc
会调用自己的全局的C++
函数,而且在dyld调用之前!
调试结果表明确实是
libobjc
系统库自己调用了内部的C++
函数。
runtime_init
runtime
运行时环境初始化,里面主要是unattachedCategories
和allocatedClasses
两张表,实现代码如下:
void runtime_init(void)
{
//对两张表进行初始化
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
exception_init
初始化libobjc
库的异常处理系统,主要是注册异常回调
。就像objc
向dyld
中注册回调函数差不多,exception_init
回调异常提供开发人员做异常处理
。
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
当程序出现崩溃现象或者用了不符合规则代码的时候,就会进入set_terminate
方法,通过_objc_terminate
方法来发出异常信息。
** _objc_terminate**
static 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)();
}
}
}
通过以上源码的分析,发现没有查询最后报异常处理也会走到_objc_terminate
方法,在_objc_terminate
方法发现了(*uncaught_handler)((id)e)
它会把异常抛出去,全局搜索uncaught_handler
。
uncaught_handler
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn;
return result;
}
通过uncaught_handler = fn
可以知道我们可以自己传一个函数的句柄, fn
可以是自己定义的函数,然后回调时,可以自己处理异常的信息。
cache_t::init
此方法是缓存条件
的初始化。
void cache_t::init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
mach_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
启动回调机制
。通常不会做什么,因为所有的初始化都是惰性
的,但是对于某些进程,会迫不及待的加载 trampolines dylib
。
void
_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
仅供objc
运行时调用并没有方法的实现,其方法的实现在dyld
源码中。
// _dyld_objc_notify_register
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
//主要的方法是registerObjCNotifiers
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
registerObjCNotifiers
// _dyld_objc_notify_init
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}
// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}
}
_dyld_objc_notify_register
注册了map_images
、load_images
、unmap_image
的回调。实现在dyld
中,三个参数分别是:
-
map_images
:管理文件中和动态库中所有的符号 (class
、Protocol
、selector
、category
)。(dyld
将image
加载到内存
中会调用该函数) -
load_images
:加载执行load
方法(dyld
初始化所有的image
文件会调用)。 -
unmap_image
:释放类相关资源。
疑问:
在dyld加载过程中map_images
与load_images
的调用没有区别,但是在这里传递的参数map_images
有&
(取址)操作。map_images
是指针拷贝,load_images
是值传递
。map_images
需要同步变化
,否则有可能发生错乱。而load_images
比较简单只是load
的调用,不需要同步变化。
补充:
在dyld中全局搜索sNotifyObjCMapped方法,发现如下:
sNotifyObjCMapped
调用的地方是在notifyBatchPartial
方法中,而notifyBatchPartial
方法是在registerObjCNotifiers
调用,在objc
初始化注册通知时就调用了,所以是调用map_images
后调用load_images
。
map_images分析
进入map_images
源码:
void
map_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
源码:
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);
}
……
}
ap_images_nolock
的核心逻辑是要找镜像文件
是怎么被加载
的,也就是对class
、Protocol
、 selector
、category
等相关的操作。在map_images_nolock
中最终发现了_read_images
的调用。那么_read_images
就是核心的研究对象了!!请往下看。
_read_images
进入_read_images
的源码,发现比较多,我们一步步来。
通过读取源码发现_read_images
多算的判断都是输出日志,根据输出日志可以知道没段代码做的是什么,那么我们关闭这些判断得到以下明显的代码逻辑结构:
void _read_images(header_info **hList, uint32_t hCount, int
totalClasses, int
unoptimizedTotalClasses){
......
// 条件控制进行一次的加载
if (!doneOnce) { ... }
......
//修复编译截断selector混乱问题
//不同的类中有相同方法名的方法,但是他们的地址必须不一样
static size_t UnfixedSelectors;
......
ts.log("IMAGE TIMES: fix up selector references");
//处理错误混乱的类(class)
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.
//当类中有协议的时候-->readProtocol
for (EACH_HEADER) {......}
ts.log("IMAGE TIMES: discover protocols");
......
//修复没有被加载的协议
for (EACH_HEADER) {......}
ts.log("IMAGE TIMES: fix up @protocol references");
//对分类的处理
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
ts.log("IMAGE TIMES: discover categories");
//类的加载处理
for (EACH_HEADER) {
classref_t const *classlist = hi->nlclslist(&count);
......
}
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_images方法主要做以下事情:
- 条件控制进行一次加载
- 修复预编译阶段的@selector的混乱的问题
- 错误混乱的类处理
- 修复重映射一些没有被镜像文件加载进来的类
- 修复一些消息
- 当类中有协议时:
readProtocol
- 修复没有被加载的协议
- 分类的处理
- 类的加载处理
- 没有被处理的类,优化那些被侵犯的类
既然read_images
做了那么多事情,那么就提取重点
的在以下进行分析。
只加载一次
if(!doneOnce) {
//控制进行只进行一次加载,进来之后修改状态
doneOnce = YES;
launchTime = YES;
...
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
//计算所需class表的大小,负载因子是3/4
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
//创建哈希表 存放所有的类
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
ts.log("IMAGE TIMES: first time tasks");
}
- 加载一次后控制判断的
doneOnce
条件等于YES
,确保只加载一次
,下次来的时候直接到创建表
的操作。 -
gdb_objc_realized_classes
,表里存放所有的类
不管是实现的还是没有实现。
修复@selector的混乱
//修复编译截断selector混乱问题
//不同的类中有相同方法名的方法,但是他们的地址必须不一样
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->hasPreoptimizedSelectors()) continue;
bool isBundle = hi->isBundle();
//从macho文件中获取方法名列表
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
SEL sel = sel_registerNameNoLock(name, isBundle);
if (sels[i] != sel) {
sels[i] = sel;
}
}
}
}
ts.log("IMAGE TIMES: fix up selector references");
不同的类
下面可以创建相同
的方法的,区分这些方法的话必须是让他们都有各自独立
的方法地址
。因为方法是存放在类
中的,所以即使方法名相同
但是存放在的类
不一样,那么方法地址肯定不一样
。
错误混乱的类处理
//处理错误混乱的类(class)
bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
for (EACH_HEADER) {
if (! mustReadClasses(hi, hasDyldRoots)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
//从mach-o文件中获取class信息
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;
}
}
}
ts.log("IMAGE TIMES: discover classes");
运行代码,在创建newCls的地方断点,看看cls与newCls的地址是怎样子的
看以看出
cls
地址与newCls
的地址是不一样
的,在newCls
没有被赋值的时候,系统已经给它分配了一个内存地址(脏地址
),所以newCls
有数据。
让newCls赋值,然后断点查看情况
得出结论:
read_class
是将类
跟地址
关联起来的。
自定义两个类分别是XJLPerson
和LGPerson
,编译之后查看mach-o
文件
可以看出
XJLPerson
的地址为:0000000100004780
,LGPerson
的地址为:0000000100004730
。
在代码中加上拦截
代码,看看编译之后的地址是否对应
macho
和源码
对应起来的,XJLPerson
和LGPerson
的地址跟在mach-o
记录的地址一样
。
readClass
这是绑定cls
与newCls
关系的核心代码
,那么先看它的代码实现逻辑是怎么样子的。
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 { ...}
//将关联的类插入到另一张哈希表中
addClassTableEntry(cls);
}
// for future reference: shared cache never contains MH_BUNDLEs
if (headerIsBundle) { ... }
return cls;
}
-
nonlazyMangledName
获取类名 -
rw
的赋值和ro
的获取并不在readClass
里面 -
addNamedClass
将类名
和地址
关联绑定
起来
-addClassTableEntry
将关联的类插入
到哈希表
中,这张表中都是初始化
过的类
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 rw
// rw有值 直接从rw中的ro获取
return maybe_rw->ro();
} else
// maybe_rw is actually ro
// 直接从ro中获取,ro是macho中的数据
return (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 {
//更新gdb_objc_realized_classes表,将key设置为 name value 设置为cls
NXMapInsert(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());
}
最后更新gdb_objc_realized_classes
哈希表,key
是name
,value
是cls`,这样子类和地址就关联起来了。
进入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.
// allocatedClasses
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
中runtime_init
运行时环境初始化,里面主要是unattachedCategories
和allocatedClasses
两张表,此时插入allocatedClasses
表中。 -
addMeta = true
将元类添加allocatedClasses
表中。
注意:rw
的赋值和ro
的获取并不在readClass
里面!!
类的加载
类的加载是比较复杂的,需要单独一个篇章进行分析探索,这里只是简单分析以下类加载的一些流程。
注释很明显的提示初始化
非懒加载类
,什么是非懒加载类?其实就是实现了load
方法或者静态的实例方法
,图中添加断点地方没有断住,就是因为XJLPerson
是懒加载类。现在给XJLPerson
添加load
方法:当
XJLPerson
添加+load
方法时候,能够进入断点。
疑点:realizeClassWithoutSwift(cls, nil);
方法明显就是类加载的核心点
,那么它是怎么样子实现的呢?那就要在下篇文章详细分解了哦。