iOS底层探索(十一)类的加载
iOS底层探索(十一)类的加载
通过上一篇文章iOS底层探索(十) 应用程序加载我们可以了解到应用程序加载的过程,那么类的加载是在什么时候进行的呢?
通过上一篇文章我们了解到dyld
加载镜像文件时是通过(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
方法获取镜像文件的真实地址的。
那么sNotifyObjCInit
指针地址又是什么时候传进来的呢?
通过搜索发现sNotifyObjCInit
是在registerObjCNotifiers()
方法中进行赋值的,而registerObjCNotifiers()
方法又是在_dyld_objc_notify_register()
方法中调用,通过传进来的init
地址,回调函数,最终加载镜像文件
那么_dyld_objc_notify_register
方法是在什么时候调用,给aNotifyObjCInit
赋值的呢?
_objc_init 初始化方法
查看objc
源码可知可查找到_objc_init
方法,为类加载的入口。
/**
* _objc_init
* 引导程序初始化,用dyld注册我们的镜像文件通知程序
* 库初始化之前由libSystem调用
*/
void _objc_init(void) {
// 判断是否已进行初始化
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme推迟初始化,直到找到objc使用的镜像文件
// 读取影响运行时的环境变量。如果需要,还可以打印环境变量
environ_init();
// 关于线程key的绑定-比如线程数据的析构函数
tls_init();
// 运行C++静态构造函数。在dyld调用我们的静态构造函数之前,libc调用_objc_init()函数。
static_init();
// runtime运行时环境初始化,里面主要是unattachedCategories,allocatedClasses后面会分析
runtime_init();
// 初始化libobjc的异常处理系统,由map_images()调用
exception_init();
// 缓存条件初始化
cache_init();
// 启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载trampolines dylib
_imp_implementationWithBlock_init();
/// 注意:仅供objc运行时使用
/// 当映射,取消映射和初始化objc镜像时将调用注册处理程序
/// dyld将使用包含objc-image-info的镜像数组调用“映射”函数
/// 那些是dilib的镜像将自动增加引用计数,因此objc不再需要在它们上调用dlopen()来防止它们被卸载。
/// 在调用_dyld_objc_notify_register()期间,dyld还将调用"映射"函数。
/// 在以后的任何dlopen()调用期间,dyld还将调用“映射”函数。
/// 当dyld初始化镜像文件时,dyld将调用“init”函数,这是objc在镜像文件中调用任何 +load方法的时候
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
由上面代码可知_objc_init
中的工作流程
- 判断是否进行过初始化
-
environ_init
初始化环境 tls_init
-
static_init
C++静态构造函数 -
runtime_init
runtime初始化 -
exception_init
异常系统初始化 -
cache_init
缓存系统初始化 _imp_implementationWithBlock_init
-
_dyld_objc_notify_register
镜像映射初始化
接下来对方法进行一个一个的分析
environ_init
环境初始化,我们先来看一下简化后的代码结构
void emviron_init(void) {
if (issetugid()) {...}
bool PrintHelp = false;
bool PrintOptions = false;
bool maybeMallocDebugging = false;
// Scan environ[] directly instead of calling getenv() a lot.
// This optimizes the case where none are set.
for (char **p = *_NSGetEnviron(); *p != nil; p++) {...}
// Special case: enable some autorelease pool debugging
// when some malloc debugging is enabled
// and OBJC_DEBUG_POOL_ALLOCATION is not set to something other than NO.
if (maybeMallocDebugging) {...}
// Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
if (PrintHelp || PrintOptions) {...}
}
然后我们利用断点进行查看该方法中有哪些代码块是执行了的,哪些没有执行,没有执行的代码可以忽略不看,只看执行的代码
运行后通过断点可知执行了for (char **p = *_NSGetEnviron(); *p != nil; p++)
中的代码。
-
for (char **p = *_NSGetEnviron(); *p != nil; p++)
通过注释可知该代码在进行environ
进行扫码,这样可以优化未设置的情况,源码如下
for (char **p = *_NSGetEnviron(); *p != nil; p++) {
if (0 == strncmp(*p, "Malloc", 6) || 0 == strncmp(*p, "DYLD", 4) ||
0 == strncmp(*p, "NSZombiesEnabled", 16))
{
maybeMallocDebugging = true;
}
if (0 != strncmp(*p, "OBJC_", 5)) continue;
if (0 == strncmp(*p, "OBJC_HELP=", 10)) {
PrintHelp = true;
continue;
}
if (0 == strncmp(*p, "OBJC_PRINT_OPTIONS=", 19)) {
PrintOptions = true;
continue;
}
const char *value = strchr(*p, '=');
if (!*value) continue;
value++;
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
if ((size_t)(value - *p) == 1+opt->envlen &&
0 == strncmp(*p, opt->env, opt->envlen))
{
// 当设置环境变量并且设置的值为"YES"时执行
*opt->var = (0 == strcmp(value, "YES"));
break;
}
}
}
- 强行打印环境变量,将
if (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);
}
运行,查看打印结果,输出的结果比较多,我们只截取一部分进行查看。
我鼠标选中的变量为优化后的isa
,我们知道在isa
中存在nonpointer
字段,iOS底层探索(三) isa详解可详细查看,我们分别打印配置该环境变量与不配置环境变量,对象的区别。
- 我们先进行环境变量的配置,在环境变量
Environment Varibales
中增加环境变量OBJC_DISABLE_NONPOINTER_ISA
值为YES
,在main
函数中创建对象,并使用断点拦截。具体如下图
重新运行后打印isa
由打印结果可知,当前isa
的最后一位为0
,即未做优化的isa
- 将该环境变量删除,再重新运行并打印
isa
由打印结果可知,当前isa
的最后一位为1
,即已做优化的isa
总结: -
environ_init
函数用于读取运行时的环境变量,如果需要,还可以打印环境变量帮助
tls_init
关于线程key的绑定,比如线程数据的析构函数
暂不展开分析,有兴趣的自己去看看
static_init
运行C++静态构造函数。在dyld调用我们的静态构造函数之前,libc
会调用_objc_init()
,因此我们必须自己做
暂不展开分析,有兴趣的自己去看看
runtime_init
runtime运行时环境初始化,里面主要是unattachedCategories
, allocatedClasses
后面会分析
暂不展开分析,有兴趣的自己去看看
exception_init
初始化libobjc
的异常处理系统,方法源码如下:
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
说到异常就能引发出我们常遇到的问题crash
,相信大家都遇到过相同的问题,比如数组越界、类型不匹配、方法不存在等等一些闪退问题。但是crash
是什么呢?
异常捕获
在iOS中引发崩溃的代码本质上就两类
- C/C++语言层面的错误。属于比较底层的错误,比如野指针、除零、内存访问异常等等,
- 系统未捕获异常(Uncaught Exception),iOS下面最常见的就是OC的NSException,比如数组越界等等。
因此我们可以了解到iOS系统的大致执行,为上层iOS代码以及底层C/C++代码。如下图所示。
- 在iOS系统中,上层代码与底层代码几乎同时运行。并且上层代码会编译底层代码进行运行
- 当底层代码执行时发生
Excaption
异常时,底层代码会通过信号机制进行一个错误信号的抛出(signal
或者是sigaction
),并且阻断底层代码的运行,即crash
.如果我们通过设定一个回调函数,当抛出异常时可以让我们自己去处理该错误。
这个过程在objc
源码中可以查看到。即_objc_terminate
方法
/*
* 未捕获的异常回调实现
* 1. 检查是否存在活动异常
* 2. 如果是,检查是否是Objective-C异常
* 3. 如果是,请使用该对象调用我们注册的回调函数
* 4. 最后,调用设置的回调函数
**/
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) {
// 这是一个objc对象,如果设置了,则进行调用
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
// It's not an objc object. Continue to C++ terminate.
(*old_terminate)();
}
}
}
由上面的代码可知,使用uncaught_handler
方法进行异常信号进行回调。,查看uncaught_handler
方法,并搜索相关信息。、
// 给uncaught_handler设置默认方法
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_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;
}
未捕获异常处理
- 创建未捕获异常的类并创建捕获方法,代码如下,其中
NSSetUncaughtExceptionHandler
等于objc_setUncaughtExceptionHandler
方法
// 创建捕获异常的类方法,并注册方法
+ (void)installUncaughtSignalExceptionHandler{
// objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
}
/// Exception,异常处理方法
void LGExceptionHandlers(NSException *exception) {
NSLog(@"%s",__func__);
}
- 在
AppDelegate
中的didFinishLaunchingWithOptions
方法中进行方法注册
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[LGUncaughtExceptionHandle installUncaughtSignalExceptionHandler];
return YES;
}
- 在
ViewController
中手动做一个数组越界
的异常。
- (IBAction)exceptionAction:(id)sender {
self.dataArray = @[];
NSLog(@"%@",self.dataArray[100]);
}
- 运行代码后执行,我们可以看到,数组越界的异常出现在我们设置的方法
LGExceptionHandlers
里了。
这么做有什么好处呢?
设置了该方法后,所有的未捕获异常都会执行到该方法中,我们可以利用这个机制进行线上应用的异常收集,方便我们的crash
的监测、修改
常见的Exception Type & Exception Code
- Exception Type
-
EXC_BAD_ACCESS
,此类型是我们最常碰到的crash
,通常用于访问了不可访问的内存导致。一般EXC_BAD_ACCESS
后面的()
内还会带有补充信息。-
SIGSEGV
:通常由于重复释放对象导致,这种类型在切换了ARC
模式后已经很少见到了。 -
SIGABRT
:收到Abort
信号退出,通常Foundation
库中的容器为了保护状态正常会做一些检测,例如插入nil
到数组中等会遇到此类错误 -
SEGV
:(Segmentation Violation),代表无效内存地址,比如空指针、未初始化指针、栈溢出等。 -
SIGBUS
:总线错误,与SIGSEGV
不同的是,SIGSEGV
访问的是无效地址
,而SIGBUS
访问的是有效地址
,但总线访问异常
,如地址对齐问题 -
SIGILL
:尝试执行非法的指令,可能不被识别或者没有权限。
-
-
EXC_BAD_INSTRUCTION
:此类异常通常由于线程执行非法指令导致 -
EXC_ARITHMETIC
: 除零错误会抛出此类异常
-
- Exception Code
-
0xbaaaaaad
: 此种类型的log意味着该Crash log并非一个真正的Crash,它仅仅只是包含了整个系统某一时刻的运行状态
。通常可以通过同时按Home键和音量键,可能由于用户不小心触发. -
0xbad22222
: 当VOIP程序
在后台
太过频繁
的**时,系统可能会终止此类程序. -
0x8badf00d
: 程序启动或者恢复时间过长被watch dog终止 -
0xc00010ff
: 程序执行大量耗费CPU和GPU的运算,导致设备过热
,触发系统过热保护被系统终止. -
0xdead10cc
: 程序退到后台
时还占用系统资源,如通讯录被系统终止. -
0xdeadfa11
: 程序无响应用户强制关闭.
-
cache_init
缓存条件初始化
暂不展开分析
_imp_implementationWithBlock_init
启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载trampolines dylib
.
暂不展开分析
_dyld_objc_notify_register
注册dyld通知回调。该方法是dyld
方法,源码如下
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_register
方法可知,该方法中只有一个registerObjCNotifiers
方法,查看该方法。
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());
}
}
}
由上方代码可知
-
sNotifyObjCMapped
即为_objc_init
方法中_dyld_objc_notify_register
中的第一个参数&map_images
,映射镜像文件 -
sNotifyObjCInit
即为第二个参数load_images
,加载镜像文件。 -
sNotifyObjCUnmapped
即为第三个参数unmap_image
在dyld
中的内容我们在iOS底层探索 应用程序加载已经了解过相应的内容,而现在需要做的是需要弄懂map_images
、load_images
、unmap_image
三个方法即可
map_images
管理文件和动态库中所有的符号,如class
、Protocol
、selector
、category
等
源码如下:
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
方法,并通过断点方方式,将不执行的代码屏蔽掉,简化源码如下
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;
// 如有必要,请执行首次初始化,该函数在普通库初始化程序之前调用。
if (firstTime) {...}
if (PrintImages) {...}
// 查找所有带有Objective-C元数据的镜像文件
hCount = 0;
// 数级,根据总数改变大小
int totalClasses = 0;
int unoptimizedTotalClasses = 0;
// 代码块,作用域
{...}
if (firstTime) {...}
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
// 一切设置完成后,调用镜像加载功能
for (auto func : loadImageFuncs) {...}
}
根据上部分的简化源码可知,firstTime
变量的改变中间位置是该方法的核心,而_read_images
方法为firstTime
变量改变区间的核心。那么接下来的重点为_read_images
方法
_read_images
进入到该方法,查看源码,先整体分析,简化源码如下
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();
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
// 1:
if (!doneOnce) {...}
// 2: Fix up @selector references
static size_t UnfixedSelectors;
{...}
ts.log("IMAGE TIMES: fix up selector references");
// 3: Discover classes. Fix up unresolved future classes. Mark bundle classes.
bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
for (EACH_HEADER) {...}
// 4: 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
// 5: Fix up old objc_msgSend_fixup call sites
for (EACH_HEADER) {...}
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
bool cacheSupportsProtocolRoots = sharedCacheSupportsProtocolRoots();
// 6: Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {...}
ts.log("IMAGE TIMES: discover protocols");
// 7: 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");
// 8: 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) {...}
ts.log("IMAGE TIMES: discover categories");
// 9: 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");
// 10: Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) {...}
ts.log("IMAGE TIMES: realize future classes");
if (DebugNonFragileIvars) {...}
// Print preoptimization statistics
if (PrintPreopt) {...}
#undef EACH_HEADER
}
上部流程分析
- 1:条件控制进行一次的加载
- 2:修复预编译阶段的
@selector
的混乱问题 - 3:错误混乱的类处理
- 4:修复重映射一些没有被镜像文件加载进来额类
- 5:修复一些消息
- 6:当我们类里面有协议的时候:
readProtocol
- 7:修复没有被加载的协议
- 8:分类处理
- 9:类的加载处理
- 10:没有被处理的类,优化那些被侵犯的类
接下来通过断点,仅将执行的代码进行细节分析
2:中的代码块,源码如下:
// Fix up @selector references
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->hasPreoptimizedSelectors()) continue;
bool isBundle = hi->isBundle();
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;
}
}
}
}
将代码运行后,断点分析,运行结果如下:
按理来说,两个方法名相同,那么两个方法的地址应该是相同,但是为什么会出现这种情况呢?
如上图所示,在我们整个系统中会有多个框架如Foundation
、CoreFoundation
等,当每个框架都有一个class
方法时,在执行该方法时,需要将方法平移到程序出口
的位置进行执行,那么在Foundation
框架中的class
方法,则为0
, 在CoreFoundation
框架中的class
方法则为0 + Foundation大小
。因此,地址不同,方法需要进行平移调整。
3: 中的代码块,源码如下
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
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;
}
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) {...}
}
}
该代码中将进行readClass
的读取。在readClass
的前后进行断点打印,结果如下:
发现该方法是对类进行名字赋值。
查看readClass方法源码,根据断点进行简化,源码如下:
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->mangledName();
if (missingWeakSuperclass(cls)) {...}
cls->fixupBackwardDeployingStableSwift();
Class replacing = nil;
if (Class newCls = popFutureNamedClass(mangledName)) {...}
if (headerIsPreoptimized && !replacing) {...} else {
// 添加类名,
addNamedClass(cls, mangledName, replacing);
addClassTableEntry(cls);
}
// for future reference: shared cache never contains MH_BUNDLEs
if (headerIsBundle) {
cls->data()->flags |= RO_FROM_BUNDLE;
cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
}
return cls;
}
查看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 {
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());
}
该方法为将类地址与名字进行映射,并插入内存。
查看mangledName
方法源码,如下:
const char *mangledName() {
// fixme can't assert locks here
ASSERT(this);
if (isRealized() || isFuture()) {
return data()->ro()->name;
} else {
return ((const class_ro_t *)data())->name;
}
}
该方法为读取类的名字
上一篇: DMHS软件介绍和安装
下一篇: 达梦数据库开启SQL日志记录