在上一篇 OC 底层探索 17 - 类的加载 (上) 中对类的名称, data, 方法, 属性, 协议的注入完成了分析. 还留下了一个问题就是类中分类的加载
本文调试源码 objc4-818.2, 所以结论也仅限于该版本.
二, 分类的加载
书接上文, 在 methodizeClass 中发现了 attachToClass 这个方法中对分类方法进行了处理.
前提:
- @implementation HRTest
- @property(nonatomic, copy)NSString *name;
- -(void)sayHappy;
- +(void)load{
- }
- @end
- @implementation HRTest (cate1)
- -(void)sayHappy;
- -(void)sayHappyCate1;
- +(void)load{
- }
- @end
类和分类中都实现了 + load 方法, 后续会用到.
1, 分类的加载时机
- static void methodizeClass(Class cls, Class previously)
- {
- ...
- objc::unattachedCategories.attachToClass(cls, cls,
- isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
- }
- void attachToClass(Class cls, Class previously, int flags)
- {
- 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);
- }
- }
根据断点调试发现, 就发现该方法没有被调用过.
可是根据观察这个方法 attachCategories 就是完成分类的加载的, 所以在 attachCategories 增加断点查看.
通过堆栈可以看到执行流程: load_images - loadAllCategories -
load_categories_nolock
- attachCategories.
所有分类的加载是在 map_images 之后的 load_images 里被调起的, 真的是这样吗? 记得在文章的开始有提过一个前提, 是类, 分类中都实现了 + load 方法, 如果没有实现这个方法呢?
1.1 类, 分类都不实现 + load
我们知道如果类中不实现 load 方法, 则该类是一个非懒加载类, 类的加载时机推迟到第一次消息调用. 那个分类的加载时机是什么时候呢?
断点设置在 methodizeClass, 因为 attachCategories 不会被调用;
堆栈信息看到起点是在
类第一次消息发送时
;
在类从 mach-o 中读出 ro 时,
类, 分类的方法都已经保存在 ro 里了
;
1.2 类, 分类的 4 类情况
类 | 分类 | 类加载情况 | 分类加载情况 |
---|---|---|---|
load | load | 类在 map_image 加载 | 分类在 load_image 加载 |
load | 类在 map_image 加载 | 分类方法已经通过 mach-o 读取到 ro 里 | |
load | 类被标记为非懒加载类,在 map_image 加载 | 分类方法已经通过 mach-o 读取到 ro 里 | |
类在第一次消息转发时加载 | 分类方法已经通过 mach-o 读取到 ro 里 |
2, 分类的加载
只有在类, 分类都实现 + load 方法才会执行, 其他情况都是直接从 mach-o 中读取出来的.
- static void load_categories_nolock(header_info *hi) {
- size_t count;
- // 所有分类进行循环
- auto processCatlist = [&](category_t * const *catlist) {
- for (unsigned i = 0; i <count; i++) {
- if (cls->isRealized()) {
- attachCategories(cls, &lc, 1, ATTACH_EXISTING);
- }
- }
- ...
- }
- static void
- attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count...) {
- // 脏内存是 wwdc2020 提出的一种类的内存优化
- // extAllocIfNeeded 调用该方法之后才生成 rwe.
- // 分类, addmethod,addprotocol,addproperty 四种情况下才会产生 rwe 脏内存
- auto rwe = cls->data()->extAllocIfNeeded();
- // 根据调试 cats_count = 1, 该循环只执行一次, 该类的其他分类是通过
- for (uint32_t i = 0; i <cats_count; i++) {
- auto& entry = cats_list[i];
- // 拿出分类中的方法
- method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
- if (mlist) {
- // 把分类放到最后一位 64 号位置, 猜测方便查询
- mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
- fromBundle |= entry.hi->isBundle();
- }
- // 属性, 协议的方法注入就省略了
- ....
- }
- if (mcount> 0) {
- // 依旧进行排序
- prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
- NO, fromBundle, __func__);
- // 插入方法列表
- rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
- }
- }
通过该方法完成分类方法, 属性, 协议的
获取, 排序, 插入
;
分类中属性是不自动生成 set,get 方法;
3, 分类方法的插入
在 OC 底层探索 17 - 类的加载 (上) 已经提到过该方法的一种情况, 事实上该方法有 3 种情况.
- void attachLists(List* const * addedLists, uint32_t addedCount) {
- if (hasArray()) {
- // many lists -> many lists
- uint32_t oldCount = array()->count;
- uint32_t newCount = oldCount + addedCount;
- // 数组进行扩容
- array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
- newArray->count = newCount;
- array()->count = newCount;
- // 旧数组元素从后往前插
- for (int i = oldCount - 1; i>= 0; i--)
- newArray->lists[i + addedCount] = array()->lists[i];
- // 新数组元素从前往后插
- for (unsigned i = 0; i <addedCount; i++)
- newArray->lists[i] = addedLists[i];
- free(array());
- setArray(newArray);
- }
- else if (!list && addedCount == 1) {
- // 0 lists -> 1 list
- list = addedLists[0];
- }
- else {
- // 1 list -> many lists
- Ptr<List> oldList = list;
- uint32_t oldCount = oldList ? 1 : 0;
- uint32_t newCount = oldCount + addedCount;
- // 数组进行扩容
- setArray((array_t *)malloc(array_t::byteSize(newCount)));
- array()->count = newCount;
- // 把旧数组当做一个元素放到 lists 最后一位
- if (oldList) array()->lists[addedCount] = oldList;
- // 把新数组从头依次放入
- for (unsigned i = 0; i <addedCount; i++)
- array()->lists[i] = addedLists[i];
- }
- }
当分类方法首次注入时会走到
1 list -> many lists
这里, 这就是导致类中
methodsList 有时会是一个二维数组的原因
.
会把分类的新方法放入到新数组的最开头,
所有重写类中的方法并没有被替换
, 而是插入到了最前方.
这就是为什么在方法查找 (lookupImp) 时从后往前进行查询的
.
当第二个分类方法进行注入时, 将数组进行扩容, 然后把
新的方法从头依次插入
.
三, load_images(非懒加载类)
map_images 完成后, 还记得在_objc_init - _dyld_objc_notify_register(&map_images, load_images, unmap_image); 中的 load_images 吗?
- void
- load_images(const char *path __unused, const struct mach_header *mh)
- {
- if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
- didInitialAttachCategories = true;
- // 非懒加载类的分类中实现 load 方法后, 通过该方法完成分类的加载.
- loadAllCategories();
- }
- // Discover load methods
- {
- // 准备所有 load 方法
- prepare_load_methods((const headerType *)mh);
- }
- // Call +load methods (without runtimeLock - re-entrant)
- // 调用所有 load 方法
- call_load_methods();
- }
1,prepare_load_methods 准备所有 load 方法
- void prepare_load_methods(const headerType *mhdr)
- {
- size_t count, i;
- classref_t const *classlist =
- _getObjc2NonlazyClassList(mhdr, &count);
- for (i = 0; i <count; i++) {
- schedule_class_load(remapClass(classlist[i]));
- }
- // 获取所有实现 load 方法的分类
- category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
- // 所有分类的 load 方法都会被加载
- for (i = 0; i < count; i++) {
- category_t *cat = categorylist[i];
- Class cls = remapClass(cat->cls);
- // 非懒加载分类迫使主类完成加载
- realizeClassWithoutSwift(cls, nil);
- add_category_to_loadable_list(cat);
- }
- }
完成了类的 load 获取, 同时也完成了
分类 load 方法的获取
;
即使类是一个懒加载类, 在获取非懒加载分类的 load 方法时
迫使主类完成加载
;
多个分类的 load 方法都会被添加
- 1.1 add_class_to_loadable_list(类)
- static void schedule_class_load(Class cls)
- {
- // Ensure superclass-first ordering
- schedule_class_load(cls->getSuperclass());
- }
- void add_class_to_loadable_list(Class cls)
- {
- IMP method;
- method = cls->getLoadMethod();
- // 在数组到达上限时, 完成数组的扩容
- if (loadable_classes_used == loadable_classes_allocated) {
- loadable_classes_allocated = loadable_classes_allocated*2 + 16;
- loadable_classes = (struct loadable_class *)
- // 数组的扩容
- realloc(loadable_classes,
- loadable_classes_allocated *
- sizeof(struct loadable_class));
- }
- // 将 load 方法和对应的类放入 loadable_classes
- loadable_classes[loadable_classes_used].cls = cls;
- loadable_classes[loadable_classes_used].method = method;
- loadable_classes_used++;
- }
将类和 load 方法添加到数组 loadable_classes 中;
在数组达到上限后再进行扩容操作, 尽可能的节省内存;
- 1.2 add_category_to_loadable_list(分类)
- void add_category_to_loadable_list(Category cat)
- {
- IMP method;
- method = _category_getLoadMethod(cat);
- // 扩容
- if (loadable_categories_used == loadable_categories_allocated) {
- loadable_categories_allocated = loadable_categories_allocated*2 + 16;
- loadable_categories = (struct loadable_category *)
- realloc(loadable_categories,
- loadable_categories_allocated *
- sizeof(struct loadable_category));
- }
- // 将分类 load 方法加入 loadable_categories
- loadable_categories[loadable_categories_used].cat = cat;
- loadable_categories[loadable_categories_used].method = method;
- loadable_categories_used++;
- }
方法原理都是一样的;
将分类的 load 方法加入 loadable_categories
2,prepare_load_methods 调用所有 load 方法
- void call_load_methods(void)
- {
- bool more_categories;
- // 当前循环值执行一次
- do {
- // 1. Repeatedly call class +loads until there aren't any more
- // 通过完成类所有 load 方法的调用
- // 当前循环值执行一次
- while (loadable_classes_used> 0) {
- call_class_loads();
- }
- // 2. Call category +loads ONCE
- // 完成所有分类所有 load 方法的调用
- more_categories = call_category_loads();
- // 3. Run more +loads if there are classes OR more untried categories
- } while (loadable_classes_used> 0 || more_categories);
- }
类的 load 方法和分类的 load 方法都会被调用, 而且是
类的 load 方法先被调用
- .
- 2.1 call_class_loads
- static void call_class_loads(void)
- {
- int i;
- // Detach current loadable list.
- struct loadable_class *classes = loadable_classes;
- int used = loadable_classes_used;
- loadable_classes = nil;
- loadable_classes_allocated = 0;
- // 直接设置为 0, 外层就不会继续循环
- loadable_classes_used = 0;
- // Call all +loads for the detached list.
- for (i = 0; i < used; i++) {
- Class cls = classes[i].cls;
- load_method_t load_method = (load_method_t)classes[i].method;
- if (!cls) continue;
- // 通过函数指针调用
- (*load_method)(cls, @selector(load));
- }
- // 释放 class
- if (classes) free(classes);
- }
call_category_loads 是类似的就是不赘述了.
通过函数指针完成 load 方法的调用
面试题
题: 如果类和分类有同名方法, 调用会调用哪个方法?
答: 两种情况:
如果是普通方法, 则会调用分类中的重名方法
如果是 + load 方法, 则先调用类中的 + load, 在依次调用分类的 load.
总结
类的加载 - 分类的加载 - load 方法调用后, 加载一个类所有的工作都已经完成了, 等待后续使用.
来源: http://www.jianshu.com/p/7a3a1e37f6be