分类的加载

2020-10-21  本文已影响0人  深圳_你要的昵称

前言

上回分析了类的加载的大致流程,我们知道了懒加载类和非懒加载类的系统调用栈信息,通过断点调试追踪,我们知道了系统在调用realizeClassWithoutSwift去实现类的信息,通过methodizeClass方法,在进行方法列表绑定时,又通过prepareMethodLists对方法列表中的方法进行排序的,然后将类里的方法列表,属性列表、协议列表以及分类信息做一个绑定的过程。


然后分类的信息绑定,如下图:

这个attachToClass就是分类绑定的过程。

attachToClass的大致流程

下面看看attachToClass的源码:

    void attachToClass(Class cls, Class previously, int flags)
    {
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));

        auto &map = get();
        auto it = map.find(previously);

        if (it != map.end()) {
            category_list &list = it->second;
            if (flags & ATTACH_CLASS_AND_METACLASS) {
                int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
                attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
                attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
            } else {
                attachCategories(cls, list.array(), list.count(), flags);
            }
            map.erase(it);
        }
    }

大致理解:

    Type &get() {
        return *reinterpret_cast<Type *>(_storage);
    }
  iterator find(const_arg_type_t<KeyT> Val) {
    BucketT *TheBucket;
    if (LookupBucketFor(Val, TheBucket))
      return makeIterator(TheBucket, getBucketsEnd(), true);
    return end();
  }

find函数可知,map中的元素是BucketT这种类型,我们暂且理解为桶结构模板。如果找到了previously类,则返回其对应的桶结构的索引下标iterator,否则返回end()集合的末尾。
using iterator = DenseMapIterator<KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>; iterator是DenseMapIterator结构体类型👇

至此,我们知道了分类绑定的调用链顺序:

realizeClassWithoutSwift --> methodizeClass --> attachToClass --> attachCategories

绑定的大致过程分析完了,我们知道了how-->是如何加载的,那when-->何时加载到内存的呢?
既然类的加载是在_read_images中完成的,那么分类是否也在其中呢?

回到_read_image


根据注释和log日志,我们在3629到3633行找到了关于分类相关的代码-->load_categories_nolock

load_categories_nolock

以上只是我们根据类的加载,去分析分类何时加载到内存的,并没有去证明这个过程,接下来我们用示例证明分类的加载的时机,是否在_read_image中?

示例断点追踪

  1. 创建分类
#import "LGPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson (LG)

@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, assign) int cate_age;

- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;
- (void)cate_instanceMethod3;
+ (void)cate_classMethod1;

@end

NS_ASSUME_NONNULL_END

#import "LGPerson+LG.h"

@implementation LGPerson (LG)

- (void)cate_instanceMethod1 {
    NSLog(@"%s", __func__);
}

- (void)cate_instanceMethod2 {
    NSLog(@"%s", __func__);
}

- (void)cate_instanceMethod3 {
    NSLog(@"%s", __func__);
}

+ (void)cate_classMethod1 {
    NSLog(@"%s", __func__);
}

@end
  1. 打断点


  2. run运行



断点根本没有进来,看来不是在_read_images里去触发分类的加载时机,那时机到底在哪里?我们直接在load_categories_nolock里打个断点,再看看调用栈:


调用栈是:

load_images-->loadAllCategories-->load_categories_nolock-->attachCategories

小结

以上我们分析了分类加载how 与 when,请看下图👇


attachCategories流程分析

接下来我们具体看看attachCategories是如何将分类的信息绑定到类里面的。

  1. 注入代码,强制断点跳到我们要研究的类LGPerson中:

  2. LGPerson 和分类LG, 添加+load方法-->非懒加载化,可以简化加载的流程


  1. run运行
    断点进入

cat中确实是LG分类,cat的信息是从header_info中而来,而header_info是从machO中读取的。

查看cat中的分类方法:


接着看加载:


attachCategories如法炮制设置断点:

其中mlists是二维数组

插入分类LG的方法列表

分类LG没有声明属性

也没有声明协议


只有一个分类

prepareMethodLists-->fixupMethodList

attachList 共3部分条件判断

(lldb) p array()
(list_array_tt<method_t, method_list_t>::array_t *) $0 = 0x00000001006121a0
(lldb) p $0.lists[0]
(method_list_t *) $2 = 0x0000000100008048
  Fix-it applied, fixed expression was: 
    $0->lists[0]
(lldb) p $2.get(0)
(method_t) $3 = {
  name = "cate_instanceMethod1"
  types = 0x0000000100003e10 "v16@0:8"
  imp = 0x0000000100003a40 (KCObjc`-[LGPerson(LG) cate_instanceMethod1])
}
  Fix-it applied, fixed expression was: 
    $2->get(0)
(lldb) p $2->get(1)
(method_t) $4 = {
  name = "cate_instanceMethod2"
  types = 0x0000000100003e10 "v16@0:8"
  imp = 0x0000000100003a70 (KCObjc`-[LGPerson(LG) cate_instanceMethod2])
}
(lldb) p $2->get(2)
(method_t) $5 = {
  name = "cate_instanceMethod3"
  types = 0x0000000100003e10 "v16@0:8"
  imp = 0x0000000100003aa0 (KCObjc`-[LGPerson(LG) cate_instanceMethod3])
}
(lldb) p $0.lists[0].get(1)
(method_t) $6 = {
  name = "cate_instanceMethod2"
  types = 0x0000000100003e10 "v16@0:8"
  imp = 0x0000000100003a70 (KCObjc`-[LGPerson(LG) cate_instanceMethod2])
}
  Fix-it applied, fixed expression was: 
    $0->lists[0]->get(1)
(lldb) p $0.lists[1]
(method_list_t *) $7 = 0x00000001000081d8
  Fix-it applied, fixed expression was: 
    $0->lists[1]
(lldb) p $7.get(0)
(method_t) $8 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003e10 "v16@0:8"
  imp = 0x0000000100003b40 (KCObjc`-[LGPerson kc_instanceMethod1])
}
  Fix-it applied, fixed expression was: 
    $7->get(0)
(lldb) p $7->get(1)
(method_t) $9 = {
  name = "kc_instanceMethod3"
  types = 0x0000000100003e10 "v16@0:8"
  imp = 0x0000000100003b10 (KCObjc`-[LGPerson kc_instanceMethod3])
}
(lldb) p $7->count
(uint32_t) $10 = 8
(lldb) 

至此,分类的方法就与本类的方法加载到了一起。

以上通过设置判断条件,断点追踪,得到以下结论:

本类和分类均是非懒加载类时,分类方法的加载在load_images中,本类的方法在_read_images中,然后再把分类的方法贴到本类方法列表中,其中分类的方法靠前。

其它情况

根据排列组合,还有以下三种情况:

  1. 主类非懒加载 分类懒加载
  2. 主类懒加载 分类非懒加载
  3. 主类 分类均懒加载

我们稍微改变下示例,声明两个分类LGALGB

@interface LGPerson (LGA)
- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;
@end

@interface LGPerson (LGB)
- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;
@end
1.主类非懒加载 分类懒加载

realizeClassWithoutSwift中观察类cls里的data()数据,发现是class_ro_t结构体类型👇


查看baseMethodLists里面的方法

上图可知,在realizeClassWithoutSwift中,cls的两个分类的方法已经绑定在方法列表中了,再看左侧调用栈信息,可知是在_read_images时,cls的分类就已经加载完毕了。

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

注意:main方法中没有调用LGPerson的任何方法:


直接运行,看看日志记录👇

从日志中查看,没有触发load_categories_nolock方法,最终走的是attachToClass,那么我们在attachToClass中断点看看,只有主类的方法,如下图👇


但是我并没有在main方法中调用关于LGPerson的任何方法,LGPerson本是懒加载类,按道理只有消息发送时才能触发LGPerson类的加载,但是现在确已经加载了。
接着我们的断点继续走,来到attachCategories中:

接着断点走到attachLists

接着来到else第三种情况,上面分析过:先扩容,然后将后加入的分类放在方法列表的前面。

综上,主类懒加载,分类非懒加载时,

  1. 主类会以非懒加载的方式提前加载数据,
  2. 分类方法添加的调用链是:
    realizeClassWithoutSwift --> methodizeClass --> attachToClass --> attachCategories --> attachLists
3.主类 分类均懒加载

依旧main方法中没有调用LGPerson的任何方法,此时类信息和分类信息都不应该加载
运行观察日志:果然没有加载👇


再触发方法,让它们加载

既然是消息发送,那我们去到lookUpImpOrForward中断点查看,如何触发的懒加载

断点锁定了LGPerson,继续走,如下图👇

因为懒加载,类LGPerson肯定未实现,那么会触发realizeClassMaybeSwiftAndLeaveLocked


接着走,来到realizeClassWithoutSwift

后面流程就不用走大家都应该知道。至此,我们锁定了一个新的方法realizeClassMaybeSwiftMaybeRelock,同理,我们在此方法中增设条件

放开所有断点,run运行项目,查看日志:

上图证明了,不论是懒加载的类,还是懒加载的分类,都是在消息发送触发的前提条件下,才会去触发类信息与分类信息的加载。

总结

分类数据的加载时机如下:

上一篇 下一篇

猜你喜欢

热点阅读