二十、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内容有关分类,放在分类之后。

相关推荐
ii_best4 小时前
ios按键精灵自动化的脚本教程:自动点赞功能的实现
运维·ios·自动化
app开发工程师V帅15 小时前
iOS 苹果开发者账号: 查看和添加设备UUID 及设备数量
ios
CodeCreator181815 小时前
iOS AccentColor 和 Color Set
ios
iOS民工16 小时前
iOS keychain
ios
m0_7482389219 小时前
webgis入门实战案例——智慧校园
开发语言·ios·swift
Legendary_0081 天前
LDR6020在iPad一体式键盘的创新应用
ios·计算机外设·ipad
/**书香门第*/1 天前
Laya ios接入goole广告,搭建环境 1
ios
wakangda2 天前
React Native 集成 iOS 原生功能
react native·ios·cocoa
几维安全2 天前
如何保护你的 iOS 应用免受逆向工程攻击
macos·objective-c·cocoa
crasowas2 天前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store