美文网首页
类的加载(下)

类的加载(下)

作者: 猿人 | 来源:发表于2020-10-20 22:57 被阅读0次

    在上一篇文章类的加载(上)了解了到了_read_images大概流程,也详细讲解了readClass方法 技能回顾

    map_images函数实现类加载处理标红.jpg
    下面讲解 标红区域 因为讲的是类的加载,当类的加载了解清楚了 其他步骤 也就清晰可见

    懒加载类和非懒加载类

    首先我们看类加载处理这里的源码

    类的加载处理源码注释
    在源码注释里我们很清晰的看到了一句话 Realize non-lazy classes (for +load methods and static instances) 翻译过来 就是 实现非懒加载类(当实现了+load方法 或者 static instances)❓
    带着疑问 我们修改源码 在 类的加载(上)我们曾在readClass里面也进行了修改 这次我们在 下面继续断点 并 修改源码目的针对性研究
    截屏2020-10-20 下午1.31.55.png
    非懒加载类环境流程

    将LGPerson里的项目 添加上+load 运行 按照源码所说 此时它为非懒加载类


    截屏2020-10-20 下午1.35.40.png

    项目断在readClass 并继续过断点


    截屏2020-10-20 下午1.37.20.png
    截屏2020-10-20 下午1.40.48.png

    我们看到确实来到了修改源码的断点 并即将进入 realizeClassWithoutSwift 函数 下面我们进行研究

    realizeClassWithoutSwift

    源码注释此方法的含义


    截屏2020-10-20 下午1.47.10.png

    解读

    • 首次对类cls进行初始化,
    • 包括分配读写数据。
    • 不执行任何快速端初始化。
    • 返回类的真实类结构。

    源码查看及解读 (由于代码太长分段解读)


    截屏2020-10-20 下午1.57.22.png
    第一步:读取data数据
    • 当前类的判空 是否已经实现 的判断
    • 未实现也不为nil 读取 class的data数据 并强转为 (class_ro_t*)类型 获取 ro
    • 获取元类判断条件
    • 如果是未来的类 ,此时 rw 数据 已经准备好 直接读取 data() 获取 rw ro
    • 正常的类 分配可写入的类数据。
      1. 申请开辟 <class_rw_t>数据结构 rw模板
      2. 根据ro设置rw 从ro中copy到rw中
      3. 将cls的data赋值为rw形式
    第二步:递归调用 realizeClassWithoutSwift 完善 继承链
    截屏2020-10-20 下午2.29.45.png
    • 递归调用 realizeClassWithoutSwift设置父类、元类
      注: isa找到根元类之后,根元类的isa是指向自己,并不会返回nil,所以有以下递归终止条件,其目的是保证类只加载一次
      1.如果类不存在,则返回nil

      2.如果类已经实现,则直接返回cls 截屏2020-10-20 下午2.38.53.png
      1. remapClass 如cls不存在,则返回nil 截屏2020-10-20 下午2.41.06.png
    • 设置父类和元类的isa指向

    • 通过addSubclass 和 addRootClass设置父子的双向链表指向关系,即父类中可以找到子类,子类中可以找到父类

    第三步:通过 methodizeClass 方法化类

    源码注释此方法的含义



    截屏2020-10-20 下午2.49.43.png
    • 修复cls的方法列表、协议列表和属性列表。
    • 附加任何突出类别。

    源码查看及解读


    截屏2020-10-20 下午3.28.43.png
    • 在realizeClassWithoutSwift 我们已经拿到了 ro rw 没见到 rwe的踪影
    • method_list 方法处理
      注:在这里 我们想起了在 消息发送流程的慢速查找流程,其中查找算法是二分查找,说明sel-imp是有序排列,那么如何排序的呢?
      就是在这里
      截屏2020-10-20 下午3.38.54.png
      它内部是通过fixupMethodList方法排序
      截屏2020-10-20 下午3.41.01.png
      进入fixupMethodList源码实现,是根据selector address排序
      截屏2020-10-20 下午3.43.11.png
    • 无论是methods 还是properties 还是protocols 我么发现都去调用了一个方法attachLists添加上针对的代码 并断点到LGPerson
      截屏2020-10-20 下午4.05.32.png

    结果发现 rwe 为NULL 并未运行其 rwe为真的判断 为什么 留疑问

    • 继续过断点来到了 attachToClass


      截屏2020-10-20 下午4.11.02.png
      截屏2020-10-20 下午4.19.50.png

      在methodlist方法主要是将分类添加到主类中 当前环境没有分类所以下面都没有运行

    懒加载类环境流程

    👆上面的流程我们都是在实现了load的情况下进行跟进的下面👇我们来屏蔽load使其变为懒加载环境


    截屏2020-10-20 下午10.23.29.png

    并在初始化LGPerson代码断言 运行


    截屏2020-10-20 下午10.29.57.png
    发现 在main函数之前调用的 mapimages -> _read_images -> readClass
    在次向下走一步断点
    截屏2020-10-20 下午10.43.28.png

    进入到methodizeClass里查看调用栈


    截屏2020-10-20 下午10.48.33.png
    这不正是我们之前学习的消息慢速转发流程lookImpOrForward发起的调用嘛

    总结

    • 什么是懒加载类 和非懒加载类
      截屏2020-10-20 下午10.51.39.png
    • ro rw rwe 定义及为什么存在
      1. app在使用类时,是需要在磁盘中app的二进制文件中读取类的信息,二进制文件中的类存储了类的元类、父类、flags和方法缓存 那么类的额外信息(name、方法、协议和实例变量等)存储在class_ro_t中

      2. class_ro_t简称 ro: read only 干净的内存空间 只读 又称为 clean memory

      3. class_rw_t简称 rw: read write 意义: 由于ios有运行时 会不断的插入 添加 删除 也就是 增删改 查 频繁 对当前 这块内存操作比较严重 为了防止对原始数据的破坏,所以就有了 rw 用于读写编写程序。 drity memory 在进程运行时发生更改的内存。类一经使用运行时就会分配一个额外的内存,那么这个内存变成了drity memory。但是在实际应用中,类的使用量只是10%,这样就在rw中造成了内存浪费,所以苹果就把rw中方法、协议和实例变量等放到了class_rw_ext_t中。

      4. class_rw_ext_t简称 rwe: read write ext,用于运行时存储类的方法、协议和实例变量等信息。


        class ro - rw - rwe.jpg

    分类的加载流程

    attachToClass

    在methodlist方法主要是将分类添加到主类中,其源码实现如下


    截屏2020-10-20 下午11.05.34.png
    • 找到一个分类进来一次,即一个一个加载分类,不要混乱
    • 当主类没有实现load, 分类开始加载、迫使主类加载,会走到if流程
      下面研究 attachCategories

    attachCategories

    截屏2020-10-21 下午4.58.58.png

    源码注释解析

    1. 将方法列表、属性和协议从类别附加到类中。
      假设所有cats中的类别都已加载并按加载顺序排序,
      先有最古老的类别。

    2. 只有少数类在启动期间有超过64个类别。
      这使用了一个小堆栈,并避免malloc。

      类别必须按正确的顺序添加,也就是从后到前。为了完成分块操作,我们从前向后迭代cats_list,向后构建本地缓冲区,并在块上调用attachLists。attachLists突出显示的
      列表,因此最终结果按照预期的顺序。

    截屏2020-10-21 下午5.09.28.png

    在这里我们看到了 rwe 赋值

    截屏2020-10-21 下午5.19.08.png
    • 判断rwe是否存在,存在直接取值不存在 则根据ro开辟。


      截屏2020-10-21 下午5.26.46.png

    点进去我们也看到了 attachLists方法 从0-1的过程也存在下面进行分析它

    attachLists

    截屏2020-10-21 下午5.40.21.png

    经过分析 我们可以看到 attachLists 方法存在三种情况 下面为 rwe 方法列表 0 -1的过程


    rwe方法列表 0-1

    回到上面断点处 我们打印 rwe


    截屏2020-10-21 下午6.24.57.png

    此时没有走下面的时候 rwe的 methods 正是 本类也就是ro->baseMethods 数据的 copy
    我们继续向下走


    截屏2020-10-22 上午10.51.07.png

    画图表示 这个算法


    category存入 64个空间的 mlists
    1. 先存进之前开辟好的64个空间的栈里
    2. mlists + ATTACH_BUFSIZ - mcount 获取mlists的首地址+ 64 - 有几个分类 最终等于 这几个分类的真实地址。抹掉没用到的。
    3. 排序
    4. 存入到rwe的methods中.
      1). 从上面的0-1 过程 就是 开始创建 rwe 将本类 ro中的baseMethods存入rwe中 此时 attachlists 是 0-1 是一个 一纬数组。
      2). 这次再进入 就是一个 1对多 的过程。


      list ->many Lists

    在下面是 多对多的逻辑示意图


    截屏2020-10-22 下午1.15.14.png

    总结

    • readClass 主要是读取类,即此时的类仅有地址+名称,还没有data数据
    • 懒加载类 未实现load的的类 数据加载推迟到第一次消息的时候
    • 非懒加载类 map_images的时候 加载所有的类数据
    • methodizeClass 两种情况都会调用 一个是main函数之前 一个是消息发送之慢速查找。
    • 在mehodizeClass里 方法序列化 为了 在慢速查找中的二分查找, 有附加分类 方法 ,主要是将 rwe的初始化(从ro中获取主类初始值)、将主类的 methods、 properties、 protocols 等添加到 rwe中, 其帖进去的方法核心为 attachLists函数算法

    类的加载源码解析分析图

    类加载处理.jpg

    上面我们已经大概理解了类是如何从Mach-0 加载到内存中,下面我们来详细了解 分类 如何加载到类中,以及 类 + 分类搭配使用情况

    分类的本质

    通过 clang 以及 苹果官方文档 及 objc源码 我们都可以了解到分类的本质就是一个结构体 截屏2020-10-22 下午6.15.17.png
    • instanceMethods: 实例方法列表
    • classMethods: 类方法列表
    • protocols: 协议表
    • instanceProperties: 实例属性 表
    • _classProperties : 类属性表 ; 拓: @property(class,nonatomic,copy) NSString * classPropertie
    • methodsForMeta() : 获取方法列表 判断是否是元类 还是 类。(因为我们知道 实例方法在类中,类方法在元类中)
    • propertiesForMeta() 获取元类或者类的属性 表
    • protocolsForMeta() 获取 元类 或者 类的 协议表 (从这里可以看出协议只存在类对象中)

    分类的调用时机

    上面的流程分析了 attachCategories的源码干了点什么事情它主要是 是否存才 rwe 不存在 初始化rwe 及 将分类的信息 帖到 rwe中 下面是它的流程
    realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories
    上面支线我们已经跑通 下面来全局搜索 attachCategories 看哪里还用到了它经过我们的反推有了以下流程
    load_images -> loadAllCategories -> load_categories_nolock -> attachCategories
    示意图


    截屏2020-10-22 下午6.29.28.png
    • realizeClassWithoutSwift 方法 (已知)
      懒加载类的时候 是 由 lookUpImpOrForward 发起的调用
      非懒加载类的时候 是由 map_images 发起的

    • attachCategories 方法

      1. 首先必要条件是研究的这个类存在分类(已知)
      2. attachToClass 是由realizeClassWithoutSwift掉起的 什么情况下才会掉起 attachCategories ?
      3. 既然 有懒加载类 和 非懒加载 区别 那么 分类是不是也有?
      4. 通过反推查看objc源码 得知 load_images 也可以掉起 attachCategories?什么时机掉起的?
    • 带着上面已知和未知的问题 就引出了下面要讲解的 类 和 分类搭配到的加载

    类和分类搭配加载

    首先配置环境主类 LGPerson

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LGPerson : NSObject
    @property (nonatomic, copy) NSString *kc_name;
    @property (nonatomic, assign) int kc_age;
    - (void)kc_instanceMethod1;
    - (void)kc_instanceMethod3;
    - (void)kc_instanceMethod2;
    + (void)kc_sayClassMethod;
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "LGPerson.h"
    @implementation LGPerson
    + (void)load{
        
    }
    
    - (void)kc_instanceMethod3{
        NSLog(@"%s",__func__);
    }
    
    - (void)kc_instanceMethod1{
        NSLog(@"%s",__func__);
    }
    
    - (void)kc_instanceMethod2{
        NSLog(@"%s",__func__);
    }
    + (void)kc_sayClassMethod{
        NSLog(@"%s",__func__);
    }
    @end
    

    分类1 LGPerson+LGA

    #import "LGPerson.h"
    NS_ASSUME_NONNULL_BEGIN
    @interface LGPerson (LGA)
    - (void)cateA_1;
    - (void)cateA_2;
    - (void)cateA_3;
    @end
    NS_ASSUME_NONNULL_END
    #import "LGPerson+LGA.h"
    @implementation LGPerson (LGA)
    + (void)load{
        
    }
    - (void)kc_instanceMethod1{
        NSLog(@"%s",__func__);
    }
    - (void)cateA_2{
        NSLog(@"%s",__func__);
    }
    - (void)cateA_1{
        NSLog(@"%s",__func__);
    }
    - (void)cateA_3{
        NSLog(@"%s",__func__);
    }
    @end
    

    分类2 LGPerson+LGB

    #import "LGPerson.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LGPerson (LGB)
    - (void)cateB_1;
    - (void)cateB_2;
    - (void)cateB_3;
    @end
    
    NS_ASSUME_NONNULL_EN
    #import "LGPerson+LGB.h"
    
    @implementation LGPerson (LGB)
    + (void)load{
        
    }
    
    - (void)kc_instanceMethod1{
        NSLog(@"%s",__func__);
    }
    
    - (void)cateB_2{
        NSLog(@"%s",__func__);
    }
    - (void)cateB_1{
        NSLog(@"%s",__func__);
    }
    - (void)cateB_3{
        NSLog(@"%s",__func__);
    }
    
    @end
    

    1.非懒加载类 + 非懒加载分类

    • 首先 在 realizeClassWithoutSwift 写下针对性研究LGPerson类的代码 并在里断点 看其调用堆栈


      截屏2020-10-23 下午12.56.46.png
    • 继续 在methodizeClass 写下针对性代码 并在里断点 看其调用堆栈


      截屏2020-10-23 下午1.03.22.png
    • 继续 在attachToClass 写下针对性代码 并在里断点 看其调用堆栈
    截屏2020-10-23 下午1.09.56.png
    • 在下面打上新的断点 并 在 attachCategories里到打下断点


      截屏2020-10-23 下午1.18.36.png
      截屏2020-10-23 下午1.20.39.png

      过断点 发现 上面我们标红区域并没有走 但是确实也来到了 attachCategories方法 确实也是LGPerson 我们仔细看堆栈情况


      截屏2020-10-23 下午1.26.31.png
      (1). 此时发现 map_image掉起的流程已经结束 并没有调用 attachCategories。流程: map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass ->attachToClass ->结束
      (2).而是由 load_image发起的调用流程:load_image -> loadAllCategories ->load_categories_nolock -> attachCategories
      (3).此时 rwe 有了值 我们bt 打印其 methods 发现其正是 对 ro->methods的数据拷贝 对于extAllocifNeeded()我们上面 也已经详细分析过了。

    在下面继续断点跟踪


    截屏2020-10-23 下午2.07.18.png

    1.发现 cats_count =1 此时循环一次 分类的名字 为LGA 将分类中的方法 存入mists 的最后一位 上面有对此详细的解答

    1. 将分类的方法 进行排序 并将 分类的方法存入到 rwe中 通过 attachLists方法 上面我们也有对attachLists算法的详细解答

    我们断过上面 来到 1399 1400 方法 并将 1367 和1399 1400 断点取消


    截屏2020-10-23 下午2.21.37.png

    继续过断点操作 发现此时断点又断回了attachCategories 中的针对性代码区域


    截屏2020-10-23 下午2.25.40.png
    继续打开 1367 1399 1400 向下走
    截屏2020-10-23 下午2.30.11.png

    1.发现 cats_count =1 此时循环一次 分类的名字 为LGB 将分类中的方法 存入mists 的最后一位

    1. 将分类的方法 进行排序 并将 分类的方法存入到 rwe中 通过 attachLists方法

    继续取消 1367 和1399 1400 断点 向下走 发现又回到了 realizeClassWithoutSwift


    截屏2020-10-23 下午2.42.20.png

    断点到针对性代码下面


    截屏2020-10-23 下午2.46.18.png

    继续过断点 发现再此处 cls->isRealized() 判断了一下是否实现 实现返回cls 并没有向下执行 又回到了 realizeClassWithoutSwift方法对性代码里


    截屏2020-10-23 下午2.49.29.png
    继续过断点 从判断 cls->isRealized() 直接返回cls 整个流程跑完了程序进入了main函数
    截屏2020-10-23 下午2.53.34.png

    注:删掉其他断点留下针对性调试的断点 我们下面还会用哦
    总结:非懒加载类+ 非懒加载分类 : 首先map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass ->attachToClass ->结束
    在这条支线上并未对 分类进行什么操作 只是完成了类的加载
    然后发起 load_image函数的回调 -> loadAllCategories ->load_categories_nolock -> attachCategories (在这里有多次调用有几个非懒分类会调用几次)将分类的数据贴到 rwe中,并再次发起了 realizeClassWithoutSwift(有几个非懒分类会调用几次)的调用 来判断类是否已经实现。

    2.非懒加载类 + 懒加载分类

    • 下面我们将源码中 所有分类的load都去掉 使分类变为懒加载概念 并运行 来到 realizeClassWithoutSwift 此时是由 map_images发起的调用


      截屏2020-10-23 下午3.49.37.png
    • 继续过断点 来到methodizeClass


      截屏2020-10-23 下午3.51.04.png
    • 继续过断点 来到attachToClass


      截屏2020-10-23 下午3.54.36.png
    • 将下面断点打开 并运行 发现并没有调用 attachCategories 中断


      截屏2020-10-23 下午3.58.46.png

      总结: 非懒加载类 + 懒加载分类 map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass ->attachToClass ->结束

    3.懒加载类 + 懒加载分类

    • 下面将主类和分类里所有的load方法全部去掉 使其变为所谓的懒加载 运行 来到 realizeClassWithoutSwift


      截屏2020-10-23 下午4.04.30.png

      发现此时的 realizeClassWithoutSwift 是由消息慢速查找流程里的lookUpImpOrForward掉起的。这说明在第一次消息 调用时候加载数据

    • 我们继续向下走 来到methodizeClass 方法


      截屏2020-10-23 下午4.11.13.png
    • 继续向下走 来到attachToClass 方法


      截屏2020-10-23 下午4.12.39.png
    • 打开下图所示断点 看是否可以进去调用 attachCategories


      截屏2020-10-23 下午4.14.27.png
    • 发现并未进去 此时运行完毕

    总结流程: lookUpImpOrForward -> realizeClassMaybeSwiftMaybeRelock ->realizeClassWithoutSwift->methodizeClass->走完

    4.懒加载类 + 非懒加载分类 (经测试 一个类拥有多个分类的情况下只要有大于等于两个分类实现load就会走下面流程)

    • 下面将主类去掉 load ,分类全部加上load 我们再次分析
      运行发现奔溃 去掉 针对性代码的元类判断 我们根据断点打印查看当前是否为 LGPerson


      截屏2020-10-23 下午4.27.26.png
    • 继续运行 来到 realizeClassWithoutSwift


      截屏2020-10-23 下午4.29.32.png

      我们看调用栈 realizeClassWithoutSwift 是由 load_images发起的

    • 继续运行来到 methodizeClass


      截屏2020-10-23 下午4.34.27.png
    • 继续运行来到 attachToClass


      截屏2020-10-23 下午4.36.45.png
    • 打开下面断点看其是否可以进去


      截屏2020-10-23 下午4.38.22.png
    • 继续运行 它进来了


      截屏2020-10-23 下午4.39.34.png

    *继续过断点 来到了 attachCategories


    截屏2020-10-23 下午4.40.52.png
    • 继续向下走


      截屏2020-10-23 下午4.43.31.png

      (1) .此时 cats_count = 2 因为两个分类 (还记得 非懒加载类+非懒加载分类 时 cats_count是=1 而是 调用了两遍 attachCategories方法,也就是 调用一次 初始化一个64位的栈 并将方法 分类方法存入此64位的最后一位)
      (2) .此时 for循环2遍 拿到 两个分类的 mlist 存入 mlists[64 - ++mcount ] 先进来的放在最后面 第二次进来的放在倒数第二未
      (3).分类方法排序prepareMethodLists
      (4). 通过attachLists方法 将mlists插入 rwe的方法列表

    总结

    1. 懒加载类 + 非懒加载分类 调用 load_image函数的回调 -> prepare_load_methods ->realizeClassWithoutSwift ->methodizeClass -> attachToClass -> 并进入到 分类附加方法里 调用->attachCategories -> for循环 分类个数次数 ; 分类方法mlist 存入 mlists[64 - ++mcount ] 先进来的放在最后面 第二次进来的放在倒数第二 -> 1.将mlists方法 序列化 ;2.通过attachLists算法将 分类的方法 贴到rwe之中 -> realizeClassWithoutSwift 判断是否实现了cls实现返回 进入 main函数

    懒加载类 + 非懒加载分类 + 懒加载分类(这种情况多个分类的情况下 有且只有一个分类为非懒加载才会走下面)

    这种情况当由主类没有实现load 分类 两个其中的一个实现load 另一个不实现

    • 运行发现断到realizeClassWithoutSwift是由 map_images掉起的


      截屏2020-10-23 下午5.23.48.png
    • 过断点来到 methodizeClass


      截屏2020-10-23 下午5.26.35.png
    • 过断点来到 attachToClass


      截屏2020-10-23 下午5.27.29.png
    • 继续向下过断点 看是否会进入到下边


      截屏2020-10-23 下午5.28.41.png

    *继续向下过断点 发现并未进入到 下方 attachCategories 取消断点继续运行 进入到了main

    截屏2020-10-23 下午5.32.53.png

    总结:懒加载类 + 非懒加载分类 + 懒加载分类 流程 map_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ->进入main并未发现 attachCategories的调用

    最后总结

    我们将这几种情况的调用流程拿出来
    1.非懒加载类 + 非懒加载分类
    map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass
    load_image -> loadAllCategories ->load_categories_nolock -> attachCategories
    2.非懒加载类 + 懒加载分类
    map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass
    3.懒加载类 + 懒加载分类
    lookUpImpOrForward -> realizeClassMaybeSwiftMaybeRelock ->realizeClassWithoutSwift->methodizeClass
    4.懒加载类 + 非懒加载分类(多个分类大于等于两个实现load流程)
    load_image -> prepare_load_methods ->realizeClassWithoutSwift ->methodizeClass -> attachToClass ->attachCategories
    5.懒加载类 + 非懒加载分类 + 懒加载分类(多个分类有且只有一个分类实现load)
    map_images ->_read_images()-> realizeClassWithoutSwift -> methodizeClass

    可见load的非必须不要随意用 ,因为 在main函数之前会做很多很多事情。


    截屏2020-10-23 下午5.43.43.png

    相关文章

      网友评论

          本文标题:类的加载(下)

          本文链接:https://www.haomeiwen.com/subject/wmbrmktx.html