iOS底层探究-----类的加载 上

765 阅读3分钟

前言

通过前三篇文章,了解了程序的加载流程,dyld_objc_init的对接关系,还有load_images的执行情况,还有dyld的迭代变化。

dyld链接的是镜像文件images。根据程序加载流程知道,images已经映射到应用程序里面了,但是映射过来只是对应的库,没有转变成相应的内存。我们工程,通过编译后,会形成machO(可执行文件),但是如何加载进入内存的?就是我们接下来要探索的。

准备资源

进入正文

objc源码中,当工程运行起来时,会执行_objc_init,然后就是相应的注册_dyld_objc_notify_register,如下图: 09BF7D57-C8C9-4FB5-8760-23EEEC163D43.png 就是_dyld_objc_notify_register(&map_images, load_images, unmap_image)这句代码,起到了承接作用,用简图描述下: 未命名文件-2.png 未命名文件-4.png

  • map_images:管理可执行文件中和动态库中所有的符号,完成class、selector、protocol、category的加载;
  • load_images:加载执行load方法。

分析_objc_init()里面的方法

environ_init()

读取影响运⾏时的环境变量,在源码中稍微做一些修改,就能直接打印信息,如下图: 9884DFD0-2084-4790-82AA-F47C402BF396.png

运行后,可以获得打印结果:

44B537BF-3F71-4D45-A8E1-DEBCF82D7050.png 打印的都是objc的相应信息。当然,还可以通过控制台,进行设置,来调整打印情况,如:

  • 是否对isa进行优化处理OBJC_DISABLE_NONPOINTER_ISA
  • 是否打印load方法OBJC_PRINT_LOAD_METHODS; 设置方法:Product --> Scheme --> Edit Scheme,就能打开设置框,在设置框里面:Run --> Arguments --> Environment Variables,在Environment Variables添加。

比如,先打印下LGPerson类的isa指针:

82484FA8-3EC2-4419-85F5-71D44305A619.png

从打印结果看,尾数是1,代表着已经开启了指针优化处理,接着就关掉isa指针优化处理,就是在设置框里面,设置OBJC_DISABLE_NONPOINTER_ISAYES,如下图:

DAA5D7C6-D493-4521-96B5-A795D120A367.png

再运行工程,通过lldb调试,打印isa指针的二进制:

595E7C41-445D-42BD-9D4A-9ED0BF6E6291.png

此时的尾数就是0,代表着已经关闭了指针优化处理。

同样的方法,可以通过设置OBJC_PRINT_LOAD_METHODS,来输出调用load方法的类,如下图的打印结果: 4E611ADA-0BCA-41C9-A28A-18F19BFF0385.png

tls_init()

关于线程key的绑定-⽐如每线程数据的析构函数,如下图: 9516AE68-6691-43CB-9A38-B8D5AC345005.png

static_init()

运⾏C++静态构造函数。在dyld调⽤我们的静态构造函数之前,libc 会调⽤ _objc_init(),因此我们必须⾃⼰做,如下图: 4CD29C3C-613E-4C2E-93B4-D62E30DD1313.png

runtime_init()

其中,runtime运⾏时环境初始化,初始化两张表:

  • unattachedCategories.init(32) 分类表的初始化;
  • allocatedClasses.init() 内存中类表的创建; 26813C31-9129-477A-8AD6-C712D739F055.png

exception_init()

初始化libobjc的异常处理系统

3D41B9AB-ABEC-49BB-A6A8-32C7F45CD082.png

image.png

当出现一个异常,会判断是否为objc异常,如果是objc异常会执行回调函数uncaught_handler。全局搜索uncaught_handler,找到回调函数设置的方法。

D978B044-A8AE-4C3D-A054-C6E9B4272DF3.png

OC层,可以通过调用方法NSSetUncaughtExceptionHandler设置回调函数,回调函数会被赋值给uncaught_handler

cache_t::init()

缓存条件初始化

B713485D-2D8A-4E31-97DC-AA1C9C99DAC5.png

_imp_implementationWithBlock_init()

启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib

31A7F950-BA52-45E4-ADDC-C9542D649E90.png

_dyld_objc_notify_register()

程序加载流程一文中,就讲解了,当程序加载到后面,会执行_objc_init函数,然后就会执行_dyld_objc_notify_register(),进行注册,相当于一个桥梁,将三个方法注册到dyld中去: B9320D34-DF24-42A9-B59C-87597BAD70B6.png

在这个函数里面,这三个方法是:

  • map_images:管理可执行文件中和动态库中所有的符号,完成classselectorprotocolcategory的加载;
  • load_images:加载执行load方法。
  • unmap_imagedyldimage移除时,会触发该函数。

_read_images()分析

整体分析

map_images()

处理由dyld映射的给定镜像文件。 C6BFD9BC-4B05-4E9A-AC61-653066AE697A.png

map_images_nolock()

01FC2698-DA0C-45E6-9F32-A9EE584B73CE.png

  • 流程:map_images() --> map_images_nolock() --> _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:修复预编译阶段的`@selector`的混乱问题
    //因为在每个镜像文件,同名的方法的位置是不相同的,所以要局部处理
    // Fix up @selector references
    // sel 名字 + 地址
    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();
    
    //✅3:错误混乱的类处理
    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.
    //✅4:修复重映射⼀些没有被镜像⽂件加载进来的类
    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

    //✅6:当我们类⾥⾯有协议的时候:readProtocol
    // 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");

    // 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
    
    //✅8:分类处理
    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 before
    // this thread finishes its fixups.

    // +load handled by prepare_load_methods()

    // Realize non-lazy classes (for +load methods and static instances)
    
    //✅9:类的加载处理
    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: 没有被处理的类 优化那些被侵犯的类; 很显然89是核心。也就是load_categories_nolockrealizeClassWithoutSwift

局部详细解析

doneOnce 分析

87C142CA-5F19-49A6-9F49-7506D391198A.png

  • 小对象类型的处理。

  • 创建一张类的总表,这个表包含所有的类,目的是方便快捷查找类。

  • 表的大小也遵循负载因子,这里 namedClassesSize = totalClasses * 4 / 3相当于是负载因子3/4的逆过程。namedClassesSize相当于总容量,totalClasses 相当于要占用的空间。

  • 根据runtime_init()函数里面的两张表,如下图: 26813C31-9129-477A-8AD6-C712D739F055.pnggdb_objc_realized_classes的比较:

  • gdb_objc_realized_classes 是一张总表,无论类是否实例化。

  • allocatedClasses 包含的是所有 allocated 的类和元类。 所以 gdb_objc_realized_classes 应该包含 allocatedClassesdoneOnce的作用相当于是创建类的总表。

UnfixedSelectors 分析

修复预编译阶段的 @selector 的混乱问题,如下图:

EBC8E252-E2B9-4061-89A5-A865FACC8FF2.png

因为sel = 名字 + 地址,在每个镜像文件,有同名的方法,但是这些方法位置是不相同的,所以要局部处理,可以通过打断点调试:

87CED4CC-59C9-44BE-AAE7-A7BD45588D59.png 从调试的结果,可以看出,同样类的 retain 方法,但是地址却不一样。但是最终以 dyld 所加载的真实地址为准。

未命名文件-2.png

如上图所示,在我们整个系统中会有多库,如果每个库都有一个retain方法,那么在执行该方法时,需要将方法平移到程序出口的位置进行执行,那么在A库中的retain方法的地址相当于首地址, 在B库中的retain方法的地址需要平移A库的地址大小。因此,地址不同,方法需要进行平移调整。

readClass 错误混乱的类处理(核心重点)

在此部分会初始化类的名称。类已移动但未删除,对错误混乱的类进行处理,比如有片空间里面存储类,当这片空间被移动后,原始的类就要被干掉,如果没有被完全干掉,就会有残留,变得混乱,出现野指针,到了后面,才会被干掉。源码如下:

1C77B9A7-91EC-4647-8776-52278015C1EF.png

通过_getObjc2ClassList,从可执行文件machO中获取类列表,然后就能对类进行处理。可以通过调试来看下情况,如下图:

image.png

当没有执行readClass函数,此时cls还只是一个地址 0x00007fff889de040 ,当执行了readClass函数之后,cls 就变成了 __NSStackBlock__

这说明在 readClass 函数中,应该是做了 类名地址 相关的一些处理,接下来对 readClass 进行分析。

进入readClass中查看其实现:

B07F3E1D-DBAB-4803-B3BC-4B25BE31AECD.png

添加的断点①断点②是为下文做准备,我们自己先创建了一个 LGPerson 类,为了更清楚看清readClass里面做的事情,再自行添加printf("%s -KC - %s\n",__func__,mangledName);,执行打印,如下图:

D35CCC5D-AEBA-4BAD-91A3-08157F3DE480.png

能够找到我们自己创建的类 LGPerson

当然我们也可以过滤掉其他类的打印,只打印 LGPerson类,如下图:

DE837800-0854-4F4C-A08F-93084A0360A6.png

可以直接打印到对应的LGPerson类。可能会比较迷惑啊,这有什么用?在这里可以单独对某个类进行处理,方便跟踪。

接着断点往下走,这里就能用到刚刚设置的断点①断点②了,如下图:

25192383-A1E3-47BB-A648-FFE19F382C25.png

之所以这么做,查看了一些资料,说是会走3365处这个if判断块的代码,但是实际上是没有执行的。继续跟踪代码,程序会运行到addNamedClass(),通过该方法,将类名添加到已命名的非元类映射(关联类信息,加入总表 gdb_objc_realized_classes),如下图:

5CECA88D-7E9E-4403-A6BA-54975EBE4568.png

  • cls类加入 gdb_objc_realized_classes 总表中。总表是在 doneOnce 中创建的。
  • 执行addNamedClass函数后,地址进行关联了。其中,核心逻辑是 NXMapInsert 处理的。也就是插入总表的时候进行的关联。以 MapPair(key-value) 的形式进行关联。

5270BEA0-60A1-4709-A6C7-CB6A7B1B5C4B.png

接着断点往下跟踪,就执行 addClassTableEntry 函数,如下图:

D2C4D383-4855-434F-83FF-195DD42B9A3C.png

  • 将类和元类插入 allocatedClasses 表中。这张表是在 runtime_init 中创建的。

remapClasses

502A4585-E5C0-4257-AE74-B5AC27499E1E.png

  • 通过 noClassesRemapped 方法判断是否有类引用 (_objc_classrefs) 需要进行重映射
  • 接着就是对类进行重新映射,读取的是 macho 中的数据 __objc_classrefs__objc_superrefs 。最终调用 remapClassRef 进行重新映射。

objc_msgSend_fixup

724B6992-0825-42D9-97EB-D6EF6B12A854.png

  • 修复 sel 的调用,比如我的第二篇博客里面,写的苹果关于 allochook 操作, allocimp 改为直接调用 objc_alloc ,而不是走 alloc 的实现。当然正常情况下不会走这个逻辑,在 llvm 阶段已经处理了。

我们再看下 fixupMessageRef 的源码,就很熟悉了,如下图:

0F757E42-27A2-469E-8EAA-3FFDC794F899.png

discover categories

794BFFAD-687D-43A8-9370-190C40E9E54B.png

根据注释说明,不会进入这个逻辑(即使实现了分类的+ load也不会进入)。分类的加载必须在load_images之后。

realize non-lazy classes

非懒加载类的处理,如下图:

2CA1B847-F245-4E5A-B985-0C9A611F7FCD.png

  • 一般情况下自己实现的类是不会进入这个逻辑的(除非实现了+ load方法)。
  • 根据注释可以看到只有非懒加载类会进入这个逻辑,nlclslist 就是获取非懒加载类列表。通过 macho__objc_nlclslist 获取。实现了 +load 方法的类会出现在 __objc_nlclslist 中。
  • 核心就是 realizeClassWithoutSwift 的初始化逻辑了。这个方法在之前的消息慢速查找流程遇见过了。

非懒加载类分为三种情况:

1.本类实现了+ load方法。

2.子类实现了+ load方法。(因为子类初始化会连带着初始化父类的)

3.分类实现了+load方法。(这里包括自己的分类以及子类的分类)

看下 machO 文件里面对应的 __objc_nlclslist 信息,如下图:

image.png

  • 这就说明了尽量避免在+ load方法中进行逻辑处理。整个过程是一个连锁反映。添加+load方法的类就会出现在__objc_nlclslist中。

为什么有懒加载和非懒加载类的区别?

因为苹果系统是按需分配的,在启动过程中初始化的类越少,那么启动速度就越快。

现在已经清楚了非懒加载类的实例化入口,那么懒加载类是在哪里实例化的呢?

既然要实例化肯定要在调用realizeClassWithoutSwift,在其中打个调试断点,把load方法去掉:

image.png

从堆栈信息可以看到,在调用 alloc 的时候进行慢速消息查找的时候实例化的。那就直接调用一个类方法,发现堆栈的信息中,执行的步骤是一样的。那么就说明了在类进行第一次发送消息的时候进行的实例化。

  • 对于非懒加载类,实现了+load方法(子类/分类/自己),类就会提前加载,为+ load的调用做准备。
  • 对于懒加载类,是在第一次消息发送objc_msgSend,进行lookUpImpOrForward消息慢速查找的时候进行初始化的。

懒加载和非懒加载对比:

  • 懒加载类:数据加载推迟到第一次发送消息的时候。 lookUpImpOrForward --> realizeClassMaybeSwiftMaybeRelock --> realizeClassWithoutSwift --> methodizeClass

  • 非懒加载类:map_images的时候加载所有类数据。 readClass --> _getObjc2NonlazyClassList --> realizeClassWithoutSwift --> methodizeClass