类的加载(上)

369 阅读8分钟

iOS 底层原理 文章汇总

我们初步探索了dyld和objc的关联,也因此引出将类加载到内存中,最关键的就是两个函数map_imagesload_images

  • map_images:主要是管理文件中和动态库中的所有符号,即class、protocol、selector、category等
  • load_images加载执行load方法

静态库、动态库以及自己写的代码通过编译会生成mach-o形式的可执行文件,再从Mach-O中读取到内存。

1320655-972db2887d6a745f.png

map_images:加载镜像文件到内存

map_images方法的主要作用就是将Mach-O可执行文件中的类信息加载到内存

  • 进入map_images的源码

image.png

  • 进入map_images_nolock源码,其关键代码是_read_images

image.png

_read_images 源码实现

_read_images主要是加载类信息(即类、分类、协议等)内存,进入_read_images源码实现,主要分为以下几部分:

  • 条件控制进行的一次加载
  • 修复预编译阶段的@selector的混乱问题
  • 错误混乱的类处理
  • 修复重映射一些没有被镜像文件加载进来的类
  • 修复一些消息
  • 当类里面有协议时:readProtocol 读取协议
  • 修复没有被加载的协议
  • 分类处理
  • 类的加载处理
  • 没有被处理的类,优化那些被侵犯的类
1、条件控制进行的一次加载

在doneOnce流程中通过NXCreateMapTable创建表,即创建一张类的哈希表 gdb_objc_realized_classes,目的是为了方便快捷地查找类

image.png

查看gdb_objc_realized_classes的注释说明,这张哈希表用于存储不在dyld共享缓存中且已命名的类,无论类是否实现

image.png

2.修复预编译阶段的@selector的混乱问题

主要是通过_getObjc2SelectorRefs拿到Mach_O中的静态段__objc_selrefs,遍历列表调用sel_registerNameNoLock将SEL添加到namedSelectors哈希表中

image.png

3、错误混乱的类处理

主要是从Mach-O中取出所有类,在遍历进行处理。当某些类已经被移动了,但未被删除的时候,就会在这里进行处理

image.png

  • 通过断点调试,在未执行readClass方法前,cls只是一个地址
  • 在执行完readClass方法之后,newCls是一个类的名称

image.png

所以到了这一步,类的信息目前仅仅存储了地址+名称

4、修复重映射一些没有被镜像文件加载进来的类

主要是将未映射的Class 和Super Class进行重映射,其中

  • _getObjc2ClassRefs是获取Mach-O中的静态段__objc_classrefs即类的引用
  • _getObjc2SuperRefs是获取Mach-O中的静态段__objc_superrefs即父类的引用
  • 通过注释可以得知,被remapClassRef的类都是懒加载的类,所以最初经过调试时,这部分代码是没有执行的

image.png

5、修复一些消息

主要是通过_getObjc2MessageRefs获取Mach-O的静态段__objc_msgrefs`,并遍历通过fixupMessageRef将函数指针进行注册,并fix为新的函数指针

image.png

6、当类里面有协议时:readProtocol 读取协议

image.png

7、修复没有被加载的协议

image.png

8、分类处理

分类的加载,会在后面的文章中介绍...

image.png

9、类的加载处理

主要是实现类的加载处理,实现非懒加载类

  • 通过_getObjc2NonlazyClassList获取Mach-O的静态段__objc_nlclslist非懒加载类表
  • 通过addClassTableEntry将非懒加载类插入类表,存储到内存,如果已经添加就不会载添加,需要确保整个结构都被添加
  • 通过realizeClassWithoutSwift实现当前的类,因为前面3中的readClass读取到内存的仅仅只有地址+名称,类的data数据并没有加载出来

image.png

10、没有被处理的类,优化那些被侵犯的类

主要是实现没有被处理的类,优化被侵犯的类

image.png

接下来 我们需要重点关注一下readClass以及realizeClassWithoutSwift这两个方法。

readClass:读取类

readClass主要是去读取类,在未执行readClass之前,cls只是一个地址,在执行完readClass之后,返回的newCls已经有了名称

  • 为了让自己定义的LGPerson类进来,我们开readClass源码里面写了一些判断条件
   // 如果想自定义的类进入,自己加一个判断
    const char *LGPersonName = "LGPerson";
    if (strcmp(mangledName, LGPersonName) == 0) {
        auto kc_ro = (const class_ro_t *)cls->data();
        printf("%s -- 研究重点--%s\n", __func__,mangledName);
    }
  • readClass源码实现如下,关键代码是addNamedClass和addClassTableEntry

image.png

通过源码实现,主要分为以下几步:

  • 通过mangledName获取类的名字,其中mangledName方法的源码实现如下(todo:有变化

image.png

其中nonlazyMangledName实现如下

image.png

其中installMangledNameForLazilyNamedClass如下

image.png

  • 当前类的父类中若有丢失的weak-linked类,则返回nil

  • 判断是不是后期需要处理的类,在正常情况下,不会走到popFutureNamedClass,因为这是专门针对未来待处理的类的操作,也可以通过断点调试,可知不会走到if流程里面,因此也不会对ro、rw进行操作

  • 通过addNamedClass将当前类添加到已经创建好的gdb_objc_realized_classes哈希表,该表用于存放所有类

image.png

通过addClassTableEntry,将初始化的类添加到allocatedClasses表,这个表在dyld与objc的关联文章中提及过,是在_objc_init中的runtime_init就创建了allocatedClasses

image.png

总结:  readClass的主要作用就是将Mach-O中的类读取到内存,即插入表中,但是目前的类仅有两个信息:地址以及名称,而mach-O的其中的data数据还未读取出来

realizeClassWithoutSwift:实现类

realizeClassWithoutSwift方法中有ro、rw的相关操作,这个方法在消息流程的慢速查找中有所提及,方法路径为:慢速查找(lookUpImpOrForward) -->realizeClassMaybeSwiftAndLeaveLocked --> realizeClassMaybeSwiftMaybeRelock --> realizeClassWithoutSwift(实现类)

realizeClassWithoutSwift方法主要作用是实现类,将类的data数据加载到内存中,主要有以下几部分操作:

  • 读取data数据,并设置rorw
  • 递归调用realizeClassWithoutSwift完善继承链
  • 通过methodizeClass方法化类
【第一步】:读取data数据

读取class的data数据,并将其强转为ro,以及rw初始化和ro拷贝一份到rw中的ro

  • ro 表示 readOnly,即只读,其在编译时就已经确定了内存,包含类名称、方法、协议和实例变量的信息,由于是只读的,所以属于Clean Memory,而Clean Memory是指加载后不会发生更改的内存

  • rw 表示 readWrite,即可读可写,由于其动态性,可能会往类中添加属性、方法、添加协议,在最新的2020的WWDC的对内存优化的说明Advancements in the Objective-C runtime - WWDC 2020 - Videos - Apple Developer中,提到rw,其实在rw中只有10%的类真正的更改了它们的方法,所以有了rwe,即类的额外信息。对于那些确实需要额外信息的类,可以分配rwe扩展记录中的一个,并将其滑入类中供其使用。其中rw就属于dirty memory,而 dirty memory是指在进程运行时会发生更改的内存类结构一经使用就会变成 ditry memory,因为运行时会向它写入新数据,例如 创建一个新的方法缓存,并从类中指向它

image.png

【第二步】递归调用 realizeClassWithoutSwift 完善 继承链

递归调用realizeClassWithoutSwift完善继承链,并设置当前类、父类、元类的rw

  • 递归调用 realizeClassWithoutSwift设置父类、元类
  • 设置父类和元类的isa指向
  • 通过addSubclass 和 addRootClass设置父子的双向链表指向关系,即父类中可以找到子类子类中可以找到父类

image.png image.png image.png

这里有一个问题,realizeClassWithoutSwift递归调用时,isa找到根元类之后, 但根元类的isa指向自己,不会返回nil,所以有以下的递归终止条件,其目的是保证类只加载一次

  • 在realizeClassWithoutSwift中,如果类不存在,则返回nil,如果类已经实现,则直接返回cls。 所以根元类的isa虽然指向自己,但此时根元类已经实现,直接返回cls递归终止

image.png

  • 在remapClass方法中,如果cls不存在,则直接返回nil

image.png

【第三步】通过 methodizeClass 方法化类

通过methodizeClass方法,从ro中读取方法列表(包括分类中的方法)、属性列表、协议列表赋值给rw,并返回cls

image.png

断点调试 realizeClassWithoutSwift

  • 在LGPerson中重写+load函数
// 变成懒加载类,方便调试
+ (void)load { 

}

realizeClassWithoutSwift方法中增加自定义逻辑

image.png

运行程序,让LGPerson进到_read_Images里面

image.png

跳转到下一个断点,进入realizeClassWithoutSwift, 并且此时的类就是LGPerson

image.png

通过x/4gx cls ,查看cls内存情况,此时bits值为0

image.png

这里我们需要去查看set_ro的源码实现

image.png

通过源码可知ro的获取主要分两种情况:有没有运行时

  • 如果有运行时,从rw中读取
  • 反之,如果没有运行时,从ro中读取

methodizeClass:方法化类

其中methodizeClass的源码实现如下,主要分为几部分:

  • 将属性列表、方法列表、协议列表等贴到rwe中
  • 附加分类中的方法(将在后面的文章中进行解释说明)

image.png

方法如何排序
  • 进入prepareMethodLists的源码实现,其内部是通过fixupMethodList方法排序

image.png

  • 通过fixupMethodList找到SortBySELAddress

image.png

  • 在SortBySELAddress方法里面return lhs.name < rhs.name ,我们知道方法的排序是根据method_t里面的name的地址进行排序

image.png

attachLists

通过rwe->methods.attachLists(&list, 1),从list中添加方法列表到rwe的methods中

image.png

attachLists的源码如下:

  • attachLists方法主要是将分类的数据加载到rwe

image.png

1320655-03bc350b6030537e.png

懒加载类和非懒加载类

区别: 非懒加载类实现了+load方法,而懒加载类没有

懒加载类和非懒加载类的数据加载时机如下图所示:

1320655-ab76dd544c54515e.png