二十、app加载流程(四)类的加载(上)

本文由快学吧个人写作,以任何形式转载请表明原文出处

一、资料准备

objc4-818.2

对应mac的版本是11.1。可根据自己的系统版本挑选可以进行调试的源码。

二、思路

既然说到了类的加载,那么必然要知道类的结构,在类的本质这里说过,类是个结构体,叫objc_class,是继承于objc_object的,objc_class结构体内有4个成员,也就是类有四个成员 : isa、superClass、cache、bits。

  1. 上一章map_images映射镜像中,在读取镜像的函数_read_images里面找到了类从镜像被加载到内存的步骤,也就是上一章总结里面的2和8。
  2. 那么类是如何加载到内存中的?类的结构中的数据又是如何加载到内存中的?
  3. 核心函数是 : readClass(加载了类的名称,并和地址关联上)、realizeClassWithoutSwift(加载了类的其他内容)。

三、readClass

1. 找到readClass源码
  1. 在objc4-818.2 ---> 搜索_objc_init(void) ---> map_images ---> map_images_nolock ---> _read_images。
  2. 找到类处理的for循环。找到readClass如下 :
2. 进入readClass源码
  1. 解析都在图中的注释中。
  2. readClass对正常的类做的事情是 : 将类添加到gdb_objc_realized_classes表和allocatedClasses表中,如果类已经在allocatedClasses表中存在,则只添加元类。
  3. 添加到gdb_objc_realized_classes表中是通过addNamedClass函数。
  4. 添加到allocatedClasses表中是通过addClassTableEntry函数。
  5. gdb_objc_realized_classes表的创建是在上一章的四--->1,也就是doneOnce里面创建的。存储的是没有在dyld的共享缓存中存在的类,无论这个类是否实现过。
  6. allocatedClasses表的创建是在上上章objc_init中的runtime_init()创建的。存储的是已经分配过内存的类。
  7. gdb_objc_realized_classes可以看作是allocatedClasses的超集,是包含了allocatedClasses表中的内容的。
  8. 为什么要创建两张表?答 : 方便或者说优化查找。不用每次查找都找大的表(gdb_objc_realized_classes)。已经分配过内存的类可以直接到小表allocatedClasses中查找。
  9. addNamedClass源码 :
  1. addClassTableEntry源码 :
3. readClass源码总结
  1. 将类添加进gdb_objc_realized_classes表中,也就是添加到 :dyld共享缓存中没有的类的表中。
  2. allocatedClasses表中如果没有类,也添加到allocatedClasses表中,这个表是所有分配过内存的类的表。将元类也添加进来。
  3. readClass之后,类有名称和地址,但是类的内容,包括isa、superclass、cache、bits都还没有数据。也就是都还没有加载到内存中。

四、realizeClassWithoutSwift

1. 找到realizeClassWithoutSwift源码
  1. 在objc4-818.2 ---> 搜索_objc_init(void) ---> map_images ---> map_images_nolock ---> _read_images。

  2. 找到实现类的for循环中 :

2. 进入realizeClassWithoutSwift源码
1. 判断类是否存在,判断类是否实现过
2. 将类的rw和ro数据加载进去。
3. 递归实现父类和元类的rw和ro的数据加载
4. 设置类的isa,看是否需要设置成纯净的isa
5. 更新类的父类和元类的数据,修正内存偏移,设置类的实例的大小,设置rw的flags,将子类添加到父类的子类列表里面
6. 修复类的方法、属性、协议的列表的存储。处理分类相关

五、源码调试realizeClassWithoutSwift

源码调试的目的是为了直观的看到,rw和ro到底是在哪一步就加到了cls中的。rwe是不是也在这里加到了cls中。
有关ro,rw,rwe的内容在有关ro和rw这里。

1. 在objc-818.2的KCObjcBuild文件夹下创建自己的类,并在类的.m文件中实现+load

之所以要实现+load是因为realizeClassWithoutSwift的官方注释里面写了,实现的是非懒加载的类。正常的JDMan是懒加载的,添加上+load方法,就会让它的加载时机提前,不必再等到调用时再加载。

2. 需要用的代码,自己写的

这两段代码是为了在调试realizeClassWithoutSwift方法时直接定位到自定义的类,过滤掉系统的类。

代码段1 :

arduino 复制代码
            //mangledName是在objc_class的结构体中找到的,在上面的readClass里面有一
            //个相似的nonlazyMangledName,所以找到了它
            
            const char *qudaodemingzi = cls->mangledName();
            const char *JDManName = "JDMan";
            if (strcmp(qudaodemingzi, JDManName) == 0) {
                printf("找到自己定义的类了");
            }

代码段2 :

scss 复制代码
    //仅仅比上面的代码多了一个从类的ro中取出flags,用来保证探索的不是元类
    //毕竟元类我只能找到类方法,但是类可以通过lldb找到rw和ro中的属性和实例方法
    
    const char *qudaodemingzi = cls->mangledName();
    const char *JDManName = "JDMan";
    if (strcmp(qudaodemingzi, JDManName) == 0) {
        auto jd_ro = (const class_ro_t *)cls->data();
        auto jd_isMeta = jd_ro->flags & RO_META;
        if (!jd_isMeta) {
            printf("找到自己定义的类了");
        }
    }
3. 将两个代码段放到如下位置
  1. read_images源码里面,找到实现懒加载类的源码,将代码段1加入如下位置 :
  1. realizeClassWithoutSwift源码里面,将代码段2加入如下位置 :
4. 断点怎么打

一定要一个断点,一个断点的使用,先断到了自己创建的类,再给别的地方打断点。因为read_images是循环遍历mach-o静态段去加载类,每一个非懒加载类都会走到realizeClassWithoutSwift。直接打上所有的断点,走到断点上的时候,cls就不一定是不是自己创建的类了。

所以先断点如下 :

5. 断点调试
  1. 运行项目,断到如上面的图中3824行的位置。找到自定义的JDMan类
  1. 再添加断点,添加到realizeClassWithoutSwift(cls, nil)位置。让断点跳到这里。
  1. 进入realizeClassWithoutSwift函数,在自己写的代码段2的位置打上断点。然后让断点跳到这里。
  1. 然后点stepOver,跳断点,看能不能执行到如下位置,如果可以,证明现在的cls就是自定义的JDMan类,不是JDMan元类。
  1. 向下走断点,可以看下ro是什么,这个ro是mach-o中的ro,还没有加载到内存中,也没有放入到类的rw中。断点走到如下位置,lldb看ro。
  1. 向下走断点,会跳过下面的if,进入到else。先走到如下图位置。具体内容看图。
  1. 再向下走断点,会发现rw的内存空间已经分配好了,rw是一个崭新的rw,没有任何的数据存在里面。

不确定的话,可以用lldb看看。lldb调试如下 :

  1. 继续向下走断点。过了rw->set_ro(ro)之后,rw的ro就被设置好了。

此时的rw的ro有数据了。自己内部的函数,例如methods()也可以从ro中获取到数据了。可以lldb试一下从rw拿到数据,如下图 :

为什么说methods()函数是从ro中获取到数据?还能从哪里获取?通过上面的源码rw->set_ro(ro);,进入到set_ro()的源码。

看那个判断if(v.is)这里的,类型是rwe的话,才会走这里。现在的rwe还没有值呢。可以验证rwe此时是没有值的,如下图操作 :

  1. 断点还是在这个位置,不要移动。看cls中的rw和ro有值吗?

cls的ro和rw是没有值的。因为rw还没有被赋值给cls的rw。赋值是在下面的一步 :cls->setData(rw)

  1. 移动断点到如下图所示,再看cls的rw和ro,已经有了数据,说明cls->setData(rw)这一步就已经给cls的rw和ro赋值 :

看cls的rw中的数据 :这里我只看了methods,属性其实也已经出来了,也可以看,没有截图。

看cls的ro中的数据 :

6. 总结

由上面的代码调试可以总结出,在cls->setData(rw);之后,类的rw和ro就已经有了数据了。

六、有关methodizeClass放在下一章

methodizeClass内容有关分类,放在分类之后。

相关推荐
若水无华2 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"2 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂3 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T3 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20253 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz3 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频