iOS底层分析之类的加载(上)

847 阅读9分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

前面几篇文章,我们认识了类的本质,属性、协议等存放的位置,以及通过dyld链接,将Mach-O加载到内存里来。那么类的各种信息又是如何加载的?这就是本次课题需要探索的内容- - - 类的加载

通过上一篇文章应用程序加载流程, 我们知道dyld跟objc是通过_dyld_objc_notify_register函数进行关联的,objc里调用_dyld_objc_notify_register的函数是_objc_init,那么_objc_init里又做什么事情呢?

void _objc_init(void)
{
    ...
    // 环境变量初始化
    environ_init();
    // 设置析构函数
    tls_init();
    // C++函数静态函数初始化
    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

void environ_init(void) 
{
    ...
    if (PrintHelp  ||  PrintOptions) {
        ...
        //将这段for代码块复制一份到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);
        }
    }
    ...
}

略作改动:

void environ_init(void) 
{
    ...
    for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
        const option_t *opt = &Settings[i];
        // 去掉了前面的if判断
        _objc_inform("%s: %s", opt->env, opt->help);
        _objc_inform("%s is set", opt->env);
    }
    
    if (PrintHelp  ||  PrintOptions) {
        ...
    }
    ...
}

打印:

image.png 图片上控制台打印都是一些环境变量,就拿OBJC_PRINT_LOAD_METHODS来配置环境变量:

image.png

image.png 运行工程:

image.png OBJC_PRINT_LOAD_METHODS环境变量的配置,可以在控制台打印所有调用到load方法的地方,这样方便我们在做项目优化的时候,快速定位到load方法的位置。针对项目不大的情况,其实全局搜索也能定位到load方法,消耗的时间也不多,但是全局搜索搜不到一些写好的库文件里的load方法

当然,除了控制台打印,我们也可以选择将它在终端打印显示,输入
$export OBJC_HELP=1

tls_init

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
}

进入pthread_key_init_np

/* setup destructor function for static key as it is not created with pthread_key_create() */
// 大概意思是,针对非pthread_key_create()创建的key键,设置析构函数
int       pthread_key_init_np(int, void (*)(void *));

拓展什么是析构函数
析构函数是特殊的类成员函数,它用来完成对象被删除前的一些清理工作,也就是专门的扫尾工作。

static_init

/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors, 
* so we have to do it ourselves.
**********************************************************************/
//注释可以得知,主要是做一些静态初始化操作,运行C++静态构造函数
static void static_init()
{
    ...
}

runtime_init

void runtime_init(void)

{
    objc::unattachedCategories.init(32);
    // 全局搜索allocatedClasses可以看到注释,
    // 大概意思是初始化“已分配的所有类(和元类)的表”
    objc::allocatedClasses.init();
}

exception_init

/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
//初始化libobjc异常,由map_images函数调起
void exception_init(void)
{
    ...
}

举个例子,数组越界错误:

- (IBAction)logArray{
    NSArray *a = @[@"张三",@"李四",@"王五"];
    NSLog(@"打印----%@",a[4]);
}

image.png objc源码工程搜索_objc_terminate

static void _objc_terminate(void)
{
    ...
    @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)();
        }
    ...
}

我们发现,在catch异常的时候会先调用uncaught_handler,这个是用来告诉上层代码,这里报错了。

那么,如何设置uncaught_handler呢? objc源码全局搜索它:

objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    ...
    uncaught_handler = fn;
    ...
}

发现uncaught_handler的值来自于objc_setUncaughtExceptionHandler函数的fn参数,也就是说我们可以通过调用objc_setUncaughtExceptionHandler函数来给uncaught_handler赋值。

Foundation里面提供了一个NSSetUncaughtExceptionHandler函数,可以设置一个顶层异常处理函数。

我们可以调用NSSetUncaughtExceptionHandler来接收底层反馈的异常信息:

// NdUncaughtExceptionHandler.h文件
#import <Foundation/Foundation.h>

void uncaughtExceptionHandler(NSException * exception);

@interface NdUncaughtExceptionHandler : NSObject
@end
// NdUncaughtExceptionHandler.m文件
@implementation NdUncaughtExceptionHandler
void uncaughtExceptionHandler(NSException * exception){
    //异常的堆栈信息
    NSArray *stackArray = [exception callStackSymbols];
    //出现异常的原因
    NSString *reason = [exception reason];
    //异常名称
    NSString *name = [exception name];
    //拼接异常信息
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason: %@\nException name: %@\nException call stack:%@\n", name, reason, stackArray];
    NSLog(@"捕获异常----%@",exceptionInfo);
}
// AppDelegate.m文件里 部分代码
#import "NdUncaughtExceptionHandler.h"
...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 这样就可以在uncaughtExceptionHandler函数里,自己处理异常
    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
    return YES;
}

打个断点,运行看下效果:

image.png

通过这种方式,我们可以针对一些线上奔溃,做一个奔溃日志信息的收集。

接下来继续分析_objc_init

void _objc_init(void)
{
    ...
    _imp_implementationWithBlock_init();
    /** 第一个参数带&表示它是一个指针传递,与map_images同步发生变化
        这样设计的目的是,map_images内部是做了镜像文件的映射,
        这个过程比较耗时,一旦中间有某一步出现问题,会导致整个程序发生问题,所以需要同步设置。
        _dyld_objc_notify_register一旦调用,map_images就会传递给dyld里的
    */
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

_imp_implementationWithBlock_init做了什么?

/// Initialize the trampoline machinery. Normally this does nothing, as
/// everything is initialized lazily, but for certain processes we eagerly load
/// the trampolines dylib.
/**
    启动蹦床。正常情况下,这不起任何作用,就像
    一切都是惰性初始化的,但对于某些进程,我们急切地加载
    蹦床运动。
*/
void
_imp_implementationWithBlock_init(void)
{
    /**
        在某些进程中急切地加载libobjc-trampolines.dylib。一些
    程序(最著名的是早期版本的嵌入铬)启用高度限制的沙箱配置文件
    阻止对那个动态库的访问。如果有什么事imp_实现WithBlock
    (正如AppKit已经开始做的那样),然后我们将尝试加载时崩溃。
    在这里加载将在沙箱之前设置它配置文件已启用并阻止它。
    这修复了EA的起源(rdar://problem/50813789)
    和蒸汽(rdar://problem/55286131)
    */
    if (__progname &&
        (strcmp(__progname, "QtWebEngineProcess") == 0 ||
         strcmp(__progname, "Steam Helper") == 0)) {
        Trampolines.Initialize();
    }
}

_dyld_objc_notify_register做了什么?

_dyld_objc_notify_register内部调用了map_imagesload_images

&表示它是一个指针传递,为什么第一个参数带&呢?

因为map_images这个函数重要性级别很高;
我们知道,_dyld_objc_notify_register一旦调用,参数map_images就会传递给dyld里的sNotifyObjCMapped

// 代码来自 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);
}

// 进入registerObjCNotifiers
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
    sNotifyObjCMapped = mapped;
    sNotifyObjCInit = init;
    sNotifyObjCUnmapped = unmapped;
    try {
        notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
    }
    ...
}
// 进入notifyBatchPartial
static void notifyBatchPartial(dyld_image_states state, bool orLater, dyld_image_state_change_handler onlyHandler, bool preflightOnly, bool onlyObjCMappedNotification)
{
    ...
    // 这里调用了registerObjCNotifiers函数的第一个参数
    (*sNotifyObjCMapped)(objcImageCount, paths, mhs);
    ...
}

我们再来看objc源码里map_images函数:

//处理dyld映射到的给定图像。
void map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{...}

map_images这个过程比较耗时,一旦中间有某一步出现问题,会导致整个程序发生问题,所以需要同步设置(sNotifyObjCMapped要跟map_images同步)。




map_images干了什么?

进入map_images

void map_images(unsigned count, const char * const paths[],

           const struct mach_header * const mhdrs[])
{
    ...
    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[])
{
    // 代码很长。。。我们只关心跟images有关系的部分
    ...
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    ...
}

进入_read_images
大致流程如下

    // 1、************ 条件控制进行一次加载  ************
    if (!doneOnce){...}
    
    // 2、************ 修复@selector混乱问题  ************
    static size_t UnfixedSelectors;
    ...
    ts.log("IMAGE TIMES: fix up selector references");
    
    // 3、************ 修复类的混乱问题  ************
    // 开始遍历头文件,进行类与元类的读取操作并标记(旧类改动后会生成新的类,并重映射到新的类上)
    for (EACH_HEADER){...}
    ts.log("IMAGE TIMES: discover classes");
    
    // 4、************ 修复重映射没有被镜像文件加载进来的 类  ************
    if (!noClassesRemapped()){...}
    ts.log("IMAGE TIMES: remap classes");
    
    // 5、************ 修复一些消息  ************
    for (EACH_HEADER){...}
    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
    
    // 6、************ readProtocol读取协议  ************
    for (EACH_HEADER){...}
    ts.log("IMAGE TIMES: discover protocols");
    
    // 7、************ 修复一些协议  ************
    for (EACH_HEADER){...}
    ts.log("IMAGE TIMES: fix up @protocol references");
    
    // 8、************ load_categories_nolock分类的处理  ************
    if (didInitialAttachCategories){…}
    ts.log("IMAGE TIMES: discover categories");

    // 9、************ 类的加载处理  ************
    for (EACH_HEADER){…}
    ts.log("IMAGE TIMES: realize non-lazy classes");

     // 10、************ 实现未来类解析  ************
    if (resolvedFutureClasses){…}
    ts.log("IMAGE TIMES: realize future classes");
    
    // 剩下的就是一些打印

下面开始针对性的分析_read_images

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    ...
    static bool doneOnce;
    ...
    if (!doneOnce) {
        doneOnce = YES;
        ...
        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是一个存放非共享缓存里的类名的列表,无论这些类是否已经被实现。
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
    }
    
    // 修复selector(SEL地址=段地址+偏移量,每次运行地址偏移量都是不一样的)
    static size_t UnfixedSelectors;
    {
        ...
        // Mach-O里获取SEL地址
        SEL *sels = _getObjc2SelectorRefs(hi, &count);
        UnfixedSelectors += count;
        for (i = 0; i < count; i++) {
            const char *name = sel_cname(sels[i]);
            // dyld里获取SEL的真正地址,这块的探索放到后面“sel_registerNameNoLock探索”
            SEL sel = sel_registerNameNoLock(name, isBundle);
            // 以dyld地址为参考,如果不一样,就修改
            if (sels[i] != sel) {
                sels[i] = sel;
            }
        }
    }
    ...
    // 修复类
    for (EACH_HEADER) {
    for (i = 0; i < count; i++) {
        Class cls = (Class)classlist[i];
        Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
    }
    ...
}

进入readClass


readClass内部代码很多,为了搞清楚mangledName是什么,我们这边printf打印一下。

image.png 我们发现mangledName打印的是类名,我们只需要研究我们自己的类Direction
代码略作改动:

image.png 接下来,我们给printf那一行打上断点,以便后面的操作都是针对Direction类, 然后再通过断点一步步走,看它的内部代码走向:

IMB_TTW5Jk.GIF

那些没有进入的代码块我们不需要去关注,代码简化之后:


Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->nonlazyMangledName();
    ...
    cls->fixupBackwardDeployingStableSwift();
    Class replacing = nil;
    ...
    else{
        if (mangledName) { //some Swift generic classes can lazily generate their names
            // 将name添加到非元类映射表
            addNamedClass(cls, mangledName, replacing);
        }
        ...
        /**
            将类添加到所有类的那张表里;
            如果第二个参数为true,则元类也加入到allocatedClasses表。
        */
        addClassTableEntry(cls);
    }
    ...
    return cls;
}

针对自定义的普通类,readClass函数是将类、元类添加到实现表。

sel_registerNameNoLock探索

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    ...
    SEL sel = sel_registerNameNoLock(name, isBundle);
    ...
}

进入sel_registerNameNoLock

SEL sel_registerNameNoLock(const char *name, bool copy) {
    return __sel_registerName(name, 0, copy);  // NO lock, maybe copy
}

进入__sel_registerName

static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
{
    ...
    // name非空判断
    if (!name) return (SEL)0;
    // 根据name查找SEL
    result = search_builtins(name);
    if (result) return result;
    ...
    // 根据name找不到SEL才会进入这行代码,意思是把name插入到namedSelectors
    //objc_allocateClassPair表
    auto it = namedSelectors.get().insert(name);
    ...
    // 返回带地址的SEL
    return (SEL)*it.first;
}

进入search_builtins:(看看内部大概流程)

static SEL search_builtins(const char *name)
{
    ...
    if (SEL result = (SEL)_dyld_get_objc_selector(name))
    ...
}

// 进入_dyld_get_objc_selector,发现这是一个dyld的方法
extern const char* _dyld_get_objc_selector(const char* selName);

打开dyld源码搜索_dyld_get_objc_selector

const char* _dyld_get_objc_selector(const char* selName){
    // Check the shared cache table if it exists.
    // 如果类已经存在,就去共享缓存查找。
    ...
    // 因为系统写的方法是在共享缓存里的,我们自己写的方法不在共享缓存里面,
    // 所以需要再走一遍dyld3的_dyld_get_objc_selector函数
    if ( gUseDyld3 )
        _dyld_get_objc_selector
    ...   
}

进入dyld3_dyld_get_objc_selector函数:

const char* _dyld_get_objc_selector(const char* selName){
    ...
    return gAllImages.getObjCSelector(selName);
}

进入getObjCSelector:

const char* AllImages::getObjCSelector(const char *selName) const {
    ...
    return _objcSelectorHashTable->getString(selName, _objcSelectorHashTableImages.array());
}

走到这里,发现不能进入_objcSelectorHashTable,也不能进入getString,改为全局搜索_objcSelectorHashTable

image.png 发现_objcSelectorHashTable被赋值为selectorHashTable,而selectorHashTable是ObjCSelectorOpt类型,所以进入ObjCSelectorOpt

输入法有毛病,截图的时候突然间不能打中文了~ image.png

image.png

// Get a string if it has an entry in the table
const char* getString(const char* selName, const Array<uintptr_t>& baseAddresses) const;

进入getString

const char* ObjCStringTable::getString(const char* selName, const Array<uintptr_t>& baseAddresses) const {
    // 读取当前段地址
    uintptr_t sectionBaseAddress = baseAddresses[imageAndOffset.imageIndex];
    // sel地址 = 段地址+二进制文件偏移量
    const char* value = (const char*)(sectionBaseAddress + imageAndOffset.imageOffset);
    // strcmp是C语言里,用来比较两个参数是否相等的函数
    if (!strcmp(selName, value))
        return value;
    // 不等就返回空
    return nullptr;
}

看到这里,我们也可以有所感知:SEL不仅仅是一个字符串,它还带有地址。

sel_registerNameNoLock返回一个SEL,它地址=段地址+二进制文件偏移量。

下篇预告

本文,从dyld链接到objc_init,分析了objc_init内部做了哪些事情:

  • 环境变量初始化
  • 设置析构函数
  • C++函数静态函数初始化
  • 两张表初始化
  • 异常信息初始化
  • 缓存条件初始化
  • _dyld_objc_notify_register

到目前为止,我们只看到类、元类被加载到表里,那么类的ro、rw又是在哪里加载的呢? 探究内容将会放到下一篇章:
iOS底层分析之类的加载(中)
----更新时间:2021-8-04

代码

百度网盘
链接: pan.baidu.com/s/1iDuSZcqY… 密码: r5g0
(包含dyld源码、objc4-818.2源码、类的加载原理Demo)