前言
上一篇 iOS 底层原理:应用程序加载 中,主要分析了dyld的加载流程,今天将主要对objc_init、read_images进行分析,来探索类的加载。
准备工作
一、objc_init 分析
进入objc_init函数:
environ_init():读取影响运行时的环境变量,如果需要,还可以打印环境变量帮助。tls_init():关于线程key的绑定 - 比如每线程数据的析构函数,后面会分析。static_init():运行C++静态构造函数。在dyld调用我们的静态构造函数之前,libc会调用_objc_init(),因此我们必须自己做。runtime_init():runtime运行时环境初始化,里面主要是:unattachedCategories,allocatedClasses,后面会详细分析。exception_init():初始化libobjc的异常处理系统。cache_init():缓存条件初始化。_imp_implementationWithBlock_init:启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib。
environ_init 分析
进入environ_init函数:
- 如上图,红圈圈住部分在
PrintHelp、PrintOptions条件下,可以打印环境变量和help。我们把这段代码复制出来,去掉它的限制条件,打印一下看看能打印出什么。
1. 源码打印环境变量
将代码复制到上边,并运行项目:
- 可以看到很多的环境变量被打印出来,其中
OBJC_DISABLE_NONPOINTER_ISA也是我们比较熟悉的,nonpointer isa的首位是1。
2. OBJC_DISABLE_NONPOINTER_ISA 环境变量
用代码来回顾下nonpointer isa这块的内容:
SSLPerson是我们普通创建的类,它的isa首位就是1。
我们向工程添加一下OBJC_DISABLE_NONPOINTER_ISA环境变量:
再次运行项目:
- 设置了
OBJC_DISABLE_NONPOINTER_ISA以后,isa的首位就变成了0,SSLPerson就不是nonpointer isa了。
3. OBJC_PRINT_LOAD_METHODS 环境变量
我们再添加一个OBJC_PRINT_LOAD_METHODS的环境变量,并运行程序:
- 可以看到,所有的
load方法都被打印了出来。
4. export OBJC_HELP=1打印环境变量
通过源码打印环境变量其实是不太方便,下面通过终端打印环境变量:
- 进入工程目录后,通过
export OBJC_HELP=1命令,在终端也打印出了环境变量,这样的方式要快捷方便一些。
static_init 分析
先来添加些代码,在_objc_init所属文件中添加C++函数,打印objcFunc。在SSLPerson类中添加load方法,打印load。在main.m中添加C++函数,打印sslFunc。然后运行程序:
- 根据结果,
_objc_init文件中的objcFunc先打印,其次SSLPerson中的load打印,最后main.m中的sslFunc打印。 _objc_init文件中的C++函数最先调用,那么具体是在哪儿调用的呢,接下来进行分析。
在static_init中打断点,运行程序:
- 根据运行结果可以得出,
_objc_init文件中的objcFunc打印,是由662行的inits[i]();这段代码执行,那么初始化就是通过getLibobjcInitializers来完成。
runtime_init 简要分析
进入runtime_init:
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
unattachedCategories是创建的分类的表。allocatedClasses是创建的类的表,而且是开辟了的类。
exception_init 分析
1. exception_init 源码分析
先看下exception_init的函数实现:
void exception_init(void)
{
old_terminate = std::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)();
}
}
}
- 可以看到系统对异常做了捕获,捕获到异常时会调用
(*uncaught_handler)((id)e)这段代码,接下来再看看uncaught_handler在哪儿里被赋了值。
全局搜索uncaught_handler:
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_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被赋了初始值_objc_default_uncaught_exception_handler,也可以通过objc_setUncaughtExceptionHandler函数进行赋值。 - 我们自己通过函数给
uncaught_handler赋值,那么有异常发生时我们就可以监听到了。 - 系统针对上层以提供了函数
NSSetUncaughtExceptionHandler(),接下来我们通过代码来操作下。
2. NSSetUncaughtExceptionHandler 异常监控
创建iOS工程,在AppDelegate.m和ViewController.m中添加代码:
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler(&SSLExceptionHandlers);
return YES;
}
// Exception
void SSLExceptionHandlers(NSException *exception) {
NSLog(@"发生异常了");
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *dataArray = @[@"Hank",@"CC",@"Kody",@"Cooci",@"Cat"];
NSLog(@"%@",dataArray[5]);
}
@end
ViewController中,是一段数组越界的代码。AppDelegate中,通过NSSetUncaughtExceptionHandler设置了异常回调函数为我们自定义的SSLExceptionHandlers函数。
运行程序:
- 异常代码真的走到了这里,证明我们的想法应该是对的,下面再查看下堆栈情况。
- 查看堆栈,发现确实是通过
_objc_terminate调用过来的。
_dyld_objc_notify_register 分析
- 接下来将以
map_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函数:
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
...
// 初始化准备
if (firstTime) {
preopt_init();
}
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
- 这个函数的重点是
_read_images,接下来进行分析。
三、read_images 主体流程
进入read_images函数:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
// 1: 条件控制进行第一次的加载
if (!doneOnce) {...}
// 2: 修复预编译阶段的 `@selector` 的混乱问题
static size_t UnfixedSelectors;{...}
// 3: 错误混乱的类处理
for (EACH_HEADER) {...}
// 4:修复重映射一些没有被镜像文件加载进来的 类
if (!noClassesRemapped()) {...}
// 5: 修复一些消息!
for (EACH_HEADER) {...}
// 6: 当我们类里面有协议的时候 : readProtocol
for (EACH_HEADER) {...}
// 7: 修复没有被加载的协议
for (EACH_HEADER) {...}
// 8: 分类处理
if (didInitialAttachCategories) {...}
// 9: 类的加载处理
for (EACH_HEADER) {...}
// 10 : 没有被处理的类 优化那些被侵犯的类
if (resolvedFutureClasses){...}
}
四、readClass 核心重点引入
针对read_images函数,我们做一些细节的分析。
条件控制进行第一次的加载
查看实现代码:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
// 1: 条件控制进行一次的加载
if (!doneOnce) {
// 小对象类型的一些处理
initializeTaggedPointerObfuscator();
// namedClasses
// 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);
}
}
NXCreateMapTable是创建了一张类的表,这张表是总表,不管有没有开辟过内存。- 关于
4 / 3的解释,3 / 4一般用来做负载因子,我们在 OC 类原理探索:cache 的结构分析 中cache扩容时有分析过。这里的原理也差不多,只不过正好反过来了,比如我们实际要用的个数为66,那么我们就要申请66 * 4 / 3 = 88大小的个数。
修复预编译阶段的 @selector 的混乱问题
查看实现代码:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
// 2: 修复预编译阶段的 `@selector` 的混乱问题
// 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;
}
}
}
}
}
SEL是方法名,但也是有内存地址的,这块的代码会将同名的方法进行分配修复,下面断点调试进行验证。
- 根据打印结果,
sel和sels[i]的方法名都是retain,但是他们的地址分别是0x00007fff7bc03850和0x00000001004b2c5b,为什么要把sel赋值给sels[i]呢。 sels[i]的取值是在_getObjc2SelectorRefs表中拿出来的,表里的数据是从MachO中读取出来的,MachO有相对位移地址和偏移地址。sel是在dyld读取出来的,dyld是链接整个程序的,我们要以dyld为准,当SEL不一样时进行重定向。
错误混乱的类处理 readClass引入
查看实现代码:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
// 3: 错误混乱的类处理
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) {
// 有些类被删除了,但没有被删除干净就会造成混乱
// 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;
}
}
}
}
断点调试:
- 在
3632行时,打印cls的值是0x00000001004d06a0,这是一个地址。在3634行时,打印cls的值是OS_object,已经变成了类名。 - 这说明在
readClass函数中,应该是做了类和地址相关的一些处理,接下来对readClass进行分析。
五、readClass 分析
进入readClass:
在函数中添加代码,将所有的方法打印:
- 所有的方法都被打印了出来,在最后也看到了我们自己创建的类
SSLPerson,接下来添加代码,判断是SSLPerson类时,断点断住进行调试。
添加条件控制代码和断点:
运行程序:
- 断点来到这里后,继续在下面添加断点。
在3357和3384处添加断点:
断点continue:
- 断点竟然没有进入
3357行,这段代码网上有非常多的文章在分析,说这个时候会进入,其实是不对的。
向下单步调试:
addNamedClass函数会将类名和地址进行关联,然后将cls添加到哈希表中。
继续向下走:
断点进入addClassTableEntry函数:
- 可以看到这里将
MetaClass也添加到了哈希表中。
继续向下走:
- 最终走到了
return cls,这个函数已经结束,但是并没有ro和rw的相关操作,ro和rw的相关内容将在下一篇进行探索。
有问题可以评论区多多交流,点个赞支持一下吧!!😄😄😄