美文网首页
iOS底层之类的加载

iOS底层之类的加载

作者: 大橘猪猪侠 | 来源:发表于2020-10-15 22:24 被阅读0次

    在iOS底层中,关于类的加载,在应用程序开始加载时,首先通过dyld链接到动态库objc,从objc中的init方法将类中的属性、方法、协议通过编译成Mach-o格式,而在Mach-o中是通过里面的data()读取其中的ro (干净的内存,由于iOS是运行时,需要在内存中不断插入,修改,因此将干净内存copy到rw中,rwe是脏内存,在wwdc2020有详细介绍)rw,那么他是如何加载的,这是本文探索的重点。

    而在之前探索过dyld与objc的关联,在objc的源码中我们研究过一个很重要的函数_read_images,在这个函数中,有很多事件的处理:

    1: 条件控制进行一次的加载
    2: 修复预编译阶段的 `@selector` 的混乱问题
    3: 错误混乱的类处理 
    4:修复重映射一些没有被镜像文件加载进来的类 
    5: 修复一些消息!
    6: 当我们类里面有协议的时候 : readProtocol 
    7: 修复没有被加载的协议
    8: 分类处理
    9: 类的加载处理
    10 : 没有被处理的类 优化那些被侵犯的类
    

    关于类的加载,我们只需要查看第4点和第10点,首先我们以能够运行的objc4-781源码当作工程来研究。

    KCObjc中有一个Person类,里面的代码:

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

    可以看到在Person.m中,将load方法进行了注释掉,接下来来探究一下
    看下图:

    iShot2020-10-15 19.02.34.png

    看上图可以得出,当系统对类进行加载时,并不会执行3724-3749之间的代码,而当我们将load方法写出来时,程序执行了3724-3749的代码,如下图:

    iShot2020-10-15 19.04.53.png

    那么进来之后,我们继续执行下一步:


    iShot2020-10-15 19.16.06.png

    当我们在3747行打上断点后,发现if (cls->isSwiftStable()) {这个判断中的代码并不会执行,而是直接执行3747行代码,当我们往后继续看代码时(如下图),你会发现,它有多个方法中都有realizeClasses的函数名,那根据之前对方法的慢速查找时,也有着类似的地方。

    iShot2020-10-15 19.18.01.png

    那么接下来我们来看一下realizeClassWithoutSwift方法的实现,继续执行代码:

    iShot2020-10-15 19.24.02.png

    在上图中,将对clsdata()值赋值给ro,在接下来中(看下图),会执行方框中的代码,将ro赋值给rw,设置cls的数据为rw

    iShot2020-10-15 19.29.19.png

    当继续执行程序,突然发现了一个很奇怪的东西,看下图:


    iShot2020-10-15 19.34.11.png

    当上面的clsrw已经赋值了,在我们打印cls的值时,却始终为空;

    那么继续执行程序:


    iShot2020-10-15 19.42.52.png

    就会走到下面的递归程序中,对父类,元类进行递归执行;

    iShot2020-10-15 19.45.14.png

    在上图中,当程序执行到此处时,我们发现在进这个方法时,cls是Person,而在此处编程了地址,这是因为这是元类信息,而这个地址跟我们之前打印的cls的isa地址一摸一样。

    而执行到这个方法最后,有一个很重要的方法:! iShot2020-10-15 20.01.00.png

    下面我们继续进入这个方法去看一下他的源码实现:


    iShot2020-10-15 20.17.26.png

    方框中是自己写判断探索的类为自己创建的类,因为系统的类并不好判断,只能使用我们自己的类去探索,我们现在去看一下class_ro_t是什么东西,先看一下他的结构:

    iShot2020-10-15 20.19.39.png

    上图中,方框中的属性是方法列表。

    以下的代码是我利用控制台对kc_ro的一些输出,可以看到

    (lldb) p kc_ro
    (const class_ro_t *) $0 = 0x00000001000080a8
    (lldb) p $0.baseMethodList
    (method_list_t *const) $1 = 0x00000001000080f0
      Fix-it applied, fixed expression was: 
        $0->baseMethodList
    (lldb) p *$1
    (method_list_t) $2 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 24
        count = 3
        first = {
          name = "kc_instanceMethod2"
          types = 0x0000000100003f9e "v16@0:8"
          imp = 0x0000000100003de0 (KCObjc`-[Person kc_instanceMethod2])
        }
      }
    }
    (lldb) p $2.get(0)
    (method_t) $3 = {
      name = "kc_instanceMethod2"
      types = 0x0000000100003f9e "v16@0:8"
      imp = 0x0000000100003de0 (KCObjc`-[Person kc_instanceMethod2])
    }
    (lldb) p $2.get(1)
    (method_t) $4 = {
      name = "kc_instanceMethod1"
      types = 0x0000000100003f9e "v16@0:8"
      imp = 0x0000000100003e10 (KCObjc`-[Person kc_instanceMethod1])
    }
    (lldb) p $2.get(2)
    (method_t) $5 = {
      name = "kc_instanceMethod3"
      types = 0x0000000100003f9e "v16@0:8"
      imp = 0x0000000100003e40 (KCObjc`-[Person kc_instanceMethod3])
    }
    
    iShot2020-10-15 20.20.46.png

    而继续执行程序,下面的代码会很有意思,在prepareMethodLists方法中有一个fixupMethodList,在这个方法中,会对方法进行排序,如下图:

    iShot2020-10-15 20.22.31.png

    下面去验证一下它是否排序:

    (lldb) p mlist
    (method_list_t *) $6 = 0x00000001000080f0
    (lldb) p *$6
    (method_list_t) $7 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 24
        count = 3
        first = {
          name = "kc_instanceMethod1"
          types = 0x0000000100003f9e "v16@0:8"
          imp = 0x0000000100003e10 (KCObjc`-[Person kc_instanceMethod1])
        }
      }
    }
    (lldb) p $7.get(0)
    (method_t) $8 = {
      name = "kc_instanceMethod1"
      types = 0x0000000100003f9e "v16@0:8"
      imp = 0x0000000100003e10 (KCObjc`-[Person kc_instanceMethod1])
    }
    (lldb) p $7.get(1)
    (method_t) $10 = {
      name = "kc_instanceMethod2"
      types = 0x0000000100003f9e "v16@0:8"
      imp = 0x0000000100003de0 (KCObjc`-[Person kc_instanceMethod2])
    }
    (lldb) p $7.get(2)
    (method_t) $9 = {
      name = "kc_instanceMethod3"
      types = 0x0000000100003f9e "v16@0:8"
      imp = 0x0000000100003e40 (KCObjc`-[Person kc_instanceMethod3])
    }
    

    可以看到方法列表名是排序的;

    继续执行程序,看下图所示:


    iShot2020-10-15 20.26.47.png

    当排序完之后,这个rwe值为空,也就是说,我们探究到了data()->ro->rw->rwe(这一步没执行,值为空)

    而在上面,探究过在类中加入了load方法后,就执行了非那一段非懒加载的代码,那么非懒加载类和懒加载类是什么意思呢?
    就是说方法在需要的时候就加载,不需要的时候就不加载;

    那么结果就是,当有load方法时,他就是非懒加载类,没有load时,就是懒加载类,当有需要让程序提前执行时,就添加load方法,

    而非懒加载类为什么会来,是由于消息转发,请看下图,当没有load方法,执行到realizeClassWithoutSwift方法之后,我们去打印堆栈信息,从dyld_start->_objc_msgSend_uncached->lookUpImpOrForward->realizeClassWithoutSwift,可以得到,这一过程是消息转发的过程:

    iShot2020-10-15 20.36.45.png

    而为什么要这样呢,因为在加载方法的过程中,会有很多代码,对方法进行排序,如果方在main函数之前,那么main的启动会非常慢,那么当你的方法从开始到结束都没调用,那这样运行会耗费内存,而设计懒加载,节省了内存的消耗。

    所以在我们一般开发中,不建议用load

    下面对懒加载类和非懒加载类进行一些总结:

    1: 懒加载类情况 数据加载推迟到第一次消息的时候

    lookUpImpOrForward
    realizeClassMaybeSwiftMaybeRelock
    realizeClassWithoutSwift
    methodizeClass

    2: 非懒加载类情况 map_images的时候 加载所有类数据

    _getObjc2NonlazyClassList
    readClass
    realizeClassWithoutSwift
    methodizeClass

    在上面的探究过程中,漏了对分类的探究,下面来简单的来探究一下分类的加载,在下一篇文章中将会详细介绍分类的加载。

    在main.m中添加分类代码:

    @interface Person (LG)
    
    @property (nonatomic, copy) NSString *cate_name;
    @property (nonatomic, assign) int cate_age;
    
    - (void)cate_instanceMethod1;
    - (void)cate_instanceMethod3;
    - (void)cate_instanceMethod2;
    + (void)cate_sayClassMethod;
    
    @end
    
    @implementation Person (LG)
    
    - (void)cate_instanceMethod1{
        NSLog(@"%s",__func__);
    }
    
    - (void)cate_instanceMethod3{
        NSLog(@"%s",__func__);
    }
    
    - (void)cate_instanceMethod2{
        NSLog(@"%s",__func__);
    }
    
    + (void)cate_sayClassMethod{
        NSLog(@"%s",__func__);
    }
    @end
    

    在终端通过指令clang -rewrite-objc main.m -o main.cpp编译cpp文件。

    看下图,当编译完cpp文件后,找分类的结构,可以看到一个_category_t的结构,接下来去看一下这个结构体的结构:

    struct _category_t {
        const char *name;
        struct _class_t *cls;
        const struct _method_list_t *instance_methods;
        const struct _method_list_t *class_methods;
        const struct _protocol_list_t *protocols;
        const struct _prop_list_t *properties;
    };
    
    iShot2020-10-15 21.32.23.png

    方法代码:函数名就是imp,是一个method_t结构,而分类中的属性没有set,get方法,只能通过关联属性实现set,get方法。

    iShot2020-10-15 21.36.39.png

    那么继续回到objc源码中来,从上面的探索中,分类是在methodizeClass方法中,而在上面我们通过对类方法进行排序的验证之后,继续执行代码,如下图所示:

    iShot2020-10-15 21.44.45.png

    在上图中,程序中有分类,但是这个分类的代码并没有执行,直接跳到1480行代码,unattachedCategories的初始化在runtime,那么继续去看看attachToClass方法的实现:

    iShot2020-10-15 21.52.08.png

    在上图中,我们将程序继续执行,却发现程序直接跳过了1150-1161行,而在这中间有一个重要的方法attachCategories,这里有两个一样的函数调用,一个是实例方法,一个是类方法,那么我们就进入这个方法看一下:

    iShot2020-10-15 21.54.51.png

    上图是一部分代码,其中图片中是比较重要的一部分:

    下图是对一些数据的处理,其中cats_count值为1是因为在前面是一个方法一个方法的进来,而ATTACH_BUFSIZ的值为64,则1374行的作用就是倒叙插入。

    iShot2020-10-15 21.56.46.png

    来看一下mlist的结构,


    iShot2020-10-15 22.01.29.png

    继续执行,当执行完循环后,看下图:
    执行完循环,在1398处打断点,当程序执行到1400处后,rwe有值了;


    iShot2020-10-15 22.08.41.png

    rwe是什么时候开始有值了呢?
    我们往上看代码,如下图:

    iShot2020-10-15 22.11.13.png

    可以看到rew在此处调用了一个方法;
    这个方法对rew进行判断,如果存在,就直接获取,如果不存在,就开辟创建,如下两图:


    iShot2020-10-15 22.11.59.png iShot2020-10-15 22.13.40.png

    rwe是在分类的添加的时候才进行处理,所以在这个时候才会进行初始化。

    在经过一翻查询后,可以得到分类的添加处理在addMethodaddProaddprotocol的时候都会进行处理。

    相关文章

      网友评论

          本文标题:iOS底层之类的加载

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