前言
IOS底层原理之dyld程序加载 过程中,_objc_init
是一个很关键的方法。因为_objc_init
方法向dyld
中注册了回调函数,下面探究下_objc_init
方法
准备工作
_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?
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
:读取影响运行时的环境变量,如果需要还可以打印环境变量帮助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)
{ /*省略部分内容*/
// Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
if(PrintHelp || PrintOptions) {
...
if (PrintOptions) {
_objc_inform("OBJC_PRINT_OPTIONS is set");
}
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
条件下,可以答应所有的环境变量,那么现在把这些判断条件都去掉,直接环境变量
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
源码中运行才可以,如果没有objc
源码,可以在终端显示 export OBJC_HELP
= 1
终端显示也是相当方便,最关键的是用终端帅啊。这些环境变量是可以通过Xcode
配置的,下面就举几个常见的例子演示下
Xcode
中环境变量配置的位置:选中运行的target
--> Edit scheme...
--> Run
--> Arguments
--> Environment Variables
OBJC_DISABLE_NONPOINTER_ISA
环境变量OBJC_DISABLE_NONPOINTER_ISA
就是判断是否是优化的指针。YES
表示存指针,NO
表示优化后的指针就是nonpointer isa
。首先不设置环境变量看下nonpointer isa
isa
低位0
号位是1
,表示是优化后的isa
,而且高位上也有其他发数据
将环境变量 OBJC_DISABLE_NONPOINTER_ISA
= YES
再次探究下看看isa
是怎么样的
isa
低位0
号位是0
,表示是isa
是存指针,而且高位除了cls
也没有其他的数据
OBJC_PRINT_LOAD_METHODS
环境变量OBJC_PRINT_LOAD_METHODS
打印出程序中所有的load
方法,在自定义类中添加load
方法,配置环境变量OBJC_PRINT_LOAD_METHODS
= YES
+[LWPerson load]
这个是自定义LWPerson
类中的load
方法,其它的都是系统级别的load
方法。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
调用我们的静态构造函数之前,lib
会调用_objc_init
先调用自己的C++
构造函数,简单说的就是libobjc
会调用自己的全局的C++
函数,因为比较重要所以比dyld
先调用
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();
}
}
下面测试下是不是libobjc
自己调用的,而且是在dyld
,在objc
源码库添加C++
函数
调试结果表明确实是libobjc
系统库自己调用了内部的C++
函数
runtime_init
runtime
运行时环境初始化,里面主要是unattachedCategories
和allocatedClasses
两张表
void runtime_init(void)
{
objc::unattachedCategories.init(32);//分类表的初始化
objc::allocatedClasses.init();//类表的初始化
}
exception_init
初始化libobjc
库的异常处理系统,这个和objc
像dyld
中注册回调差不多,留给大家去处理异常
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
当应用出现crash
,什么是crash
当你上层的代码出现一些不符合系统底层规则时,系统会发出异常的信号。通过出现异常会进去_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
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)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
// _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
}
// 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
中有3
个参数
&map_images
:dyld
将image
加载到内存中会调用该函数load_images
:dyld
初始化所有的image
文件会调用unmap_image
:将image
移除时会调用
前面探究了load_images
方法 其实就是调用load
方法,现在探究map_images
方法,&map_images
是指针传递,指向是同一块实现的地址,因为&map_images
是第一个参数,在dyld
中全局搜索sNotifyObjCMapped
sNotifyObjCMapped
调用的地方是在notifyBatchPartial
方法中,而notifyBatchPartial
方法是在registerObjCNotifiers
调用,在objc
初始化注册通知时就调用了,所以是调用map_images
后调用load_images
read_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_nolock
,由于里面代码比较多,直接看重点
点击进入_read_images
方法,由于方法内代码很多有点无从下手要么一点点读,发现苹果的开发者提供了log
日志
void _read_images(header_info hList, uint32_t hCount, int
totalClasses, int
unoptimizedTotalClasses)
{
... //表示省略部分代码
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
// 条件控制进行一次的加载
if (!doneOnce) { ... }
// 修复预编译阶段的`@selector`的混乱的问题
// 就是不同类中有相同的方法 但是相同的方法地址是不一样的
// Fix up @selector references
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
// 当类中有协议时:`readProtocol`
// Discover protocols. Fix up protocol refs.
for (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.
if (didInitialAttachCategories) { ... }
ts.log("IMAGE TIMES: discover categories");
// 类的加载处理
// Category discovery MUST BE Late to avoid potential races
// when other threads call the new category code befor
// 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
}
根据log
提示整理出主要流程模块
- 条件控制进行一次加载
- 修复预编译阶段的
@selector
的混乱的问题 - 错误混乱的类处理
- 修复重映射一些没有被镜像文件加载进来的类
- 修复一些消息
- 当类中有协议时:
readProtocol
- 修复没有被加载的协议
- 分类的处理
- 类的加载处理
- 没有被处理的类,优化那些被侵犯的类
下面就根据日志的提示重点的单独分析
只加载一次
if(!doneOnce) {
doneOnce = YES; // 加载一次后,就不会在进判断 doneOnce = YES
launchTime = YES;
...
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
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
的混乱
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;
}
}
}
}
因为不同类中可能相同的方法,但是虽然是相同的方法但是地址不同,对那些混乱的方法进行修复。因为方法是存放在类中,每个类中的位置是不一样的,所以方法的地址也就不一样
错误混乱的类处理
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;
}
}
}
在readClass
处加一个断点,运行源码
cls
指向的是一块地址,而newCls
此时还没有赋值,系统随机给我分配一块脏地址,所以有数据,断点向下走一步看下赋值过后什么样的
图中显示readClass
的作用就是把类名和地址关联起来。可能大家还不明白,举个🌰
这边有一套房子现在没有人买,这套不属于任何人,但是房子有地址的那条街那条路多少号。
现在张三买了,那么房产证上有了张三的名字,这套房子就和张三关联起来
现在通过自定义的类在验证下,现在自定义两个类LWPerson
和LWTeacher
。因为
classlist
= _getObjc2ClassList
是通过macho
文件Section
中__objc_classlist
获取。现在看下macho
文件
LWPerson
的地址是0x0000000100004230
,LWTeacher
的地址是0x0000000100004280
和上面的cls
也是对应的
macho
和源码对应起来的,一抹抹一样样,可以说是相当完美。下面探究下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 { ...}
//将关联的类插入到另一张哈希表中
addClassTableEntry(cls);
}
// for future reference: shared cache never contains MH_BUNDLEs
if (headerIsBundle) { ... }
return cls;
}
大家可能会奇怪上面有用到的cls->nonlazyMangledName()
,从哪里来的。就是readClass
中的
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
方法或者静态的实例方法
图中判断自己添加判断的地方没有断住,就是因为LWPerson
是懒加载类。现在给LWPerson
添加load
方法
- 当
LWPerson
变成非懒加载类断点就能断住 nlclslist
简单说下里面调用_getObjc2NonlazyClassList
方法就是从macho中获取非懒加载列表,从macho
中Section
是__objc_nlclslist
获取,简单检验真下realizeClassWithoutSwift
这个方法可能大家有点熟悉,后面会进行详细的探究
__objc_nlclslist
列表中就就一条数据,存放的数据是0x0100004250
,因为IOS
是小端模式从右往左读,__objc_classlist
列表中的第一条数据LWPerson
类,地址是0x0100004250
,所以非懒加载列表数据是LWPerson
类
总结
从dyld
到_objc_init
再到read_images
这整个流程的探究逐渐的串联起来了。许多知识也有点到面,脉络越来越清晰。后面就是非常重要详细的类的加载