iOS 底层原理:类的加载原理上

592 阅读7分钟

前言

上一篇 iOS 底层原理:应用程序加载 中,主要分析了dyld的加载流程,今天将主要对objc_initread_images进行分析,来探索类的加载。

准备工作

一、objc_init 分析

进入objc_init函数:

image.png

  • environ_init():读取影响运行时的环境变量,如果需要,还可以打印环境变量帮助。
  • tls_init():关于线程key的绑定 - 比如每线程数据的析构函数,后面会分析。
  • static_init():运行C++静态构造函数。在dyld调用我们的静态构造函数之前,libc 会调用_objc_init(),因此我们必须自己做。
  • runtime_init()runtime运行时环境初始化,里面主要是:unattachedCategoriesallocatedClasses,后面会详细分析。
  • exception_init():初始化libobjc的异常处理系统。
  • cache_init():缓存条件初始化。
  • _imp_implementationWithBlock_init:启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib

environ_init 分析

进入environ_init函数:

image.png

  • 如上图,红圈圈住部分在PrintHelpPrintOptions条件下,可以打印环境变量help。我们把这段代码复制出来,去掉它的限制条件,打印一下看看能打印出什么。

1. 源码打印环境变量

将代码复制到上边,并运行项目:

image.png

  • 可以看到很多的环境变量被打印出来,其中OBJC_DISABLE_NONPOINTER_ISA也是我们比较熟悉的,nonpointer isa的首位是1

2. OBJC_DISABLE_NONPOINTER_ISA 环境变量

用代码来回顾下nonpointer isa这块的内容:

image.png

  • SSLPerson是我们普通创建的类,它的isa首位就是1

我们向工程添加一下OBJC_DISABLE_NONPOINTER_ISA环境变量:

image.png

再次运行项目:

image.png

  • 设置了OBJC_DISABLE_NONPOINTER_ISA以后,isa的首位就变成了0SSLPerson就不是nonpointer isa了。

3. OBJC_PRINT_LOAD_METHODS 环境变量

我们再添加一个OBJC_PRINT_LOAD_METHODS的环境变量,并运行程序:

image.png

  • 可以看到,所有的load方法都被打印了出来。

4. export OBJC_HELP=1打印环境变量

通过源码打印环境变量其实是不太方便,下面通过终端打印环境变量:

image.png

  • 进入工程目录后,通过export OBJC_HELP=1命令,在终端也打印出了环境变量,这样的方式要快捷方便一些。

static_init 分析

先来添加些代码,在_objc_init所属文件中添加C++函数,打印objcFunc。在SSLPerson类中添加load方法,打印load。在main.m中添加C++函数,打印sslFunc。然后运行程序:

image.png

  • 根据结果,_objc_init文件中的objcFunc先打印,其次SSLPerson中的load打印,最后main.m中的sslFunc打印。
  • _objc_init文件中的C++函数最先调用,那么具体是在哪儿调用的呢,接下来进行分析。

static_init中打断点,运行程序:

image.png

  • 根据运行结果可以得出,_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.mViewController.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函数。

运行程序:

image.png

  • 异常代码真的走到了这里,证明我们的想法应该是对的,下面再查看下堆栈情况。

image.png

  • 查看堆栈,发现确实是通过_objc_terminate调用过来的。

_dyld_objc_notify_register 分析

image.png

  • 接下来将以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是方法名,但也是有内存地址的,这块的代码会将同名的方法进行分配修复,下面断点调试进行验证。

image.png

  • 根据打印结果,selsels[i]的方法名都是retain,但是他们的地址分别是0x00007fff7bc038500x00000001004b2c5b,为什么要把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;
            }
        }
    }
}

断点调试:

image.png

  • 3632行时,打印cls的值是0x00000001004d06a0,这是一个地址。在3634行时,打印cls的值是OS_object,已经变成了类名。
  • 这说明在readClass函数中,应该是做了地址相关的一些处理,接下来对readClass进行分析。

五、readClass 分析

进入readClass

image.png

在函数中添加代码,将所有的方法打印:

image.png

  • 所有的方法都被打印了出来,在最后也看到了我们自己创建的类SSLPerson,接下来添加代码,判断是SSLPerson类时,断点断住进行调试。

添加条件控制代码和断点:

image.png

运行程序:

image.png

  • 断点来到这里后,继续在下面添加断点。

33573384处添加断点:

image.png

断点continue

image.png

  • 断点竟然没有进入3357行,这段代码网上有非常多的文章在分析,说这个时候会进入,其实是不对的。

向下单步调试:

image.png

  • addNamedClass函数会将类名地址进行关联,然后将cls添加到哈希表中。

继续向下走:

image.png

断点进入addClassTableEntry函数:

image.png

  • 可以看到这里将MetaClass也添加到了哈希表中。

继续向下走:

image.png

  • 最终走到了return cls,这个函数已经结束,但是并没有rorw的相关操作,rorw的相关内容将在下一篇进行探索。

有问题可以评论区多多交流,点个赞支持一下吧!!😄😄😄