前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OC底层探索18 - 类的加载(下)OC底层探索18 - 类的加载(下)

OC底层探索18 - 类的加载(下)OC底层探索18 - 类的加载(下)

作者头像
用户8893176
发布2021-08-09 11:14:22
3350
发布2021-08-09 11:14:22
举报
文章被收录于专栏:小黑娃Henry

在上一篇OC底层探索17 - 类的加载(上)中对类的名称、data、方法、属性、协议的注入完成了分析。还留下了一个问题就是类中分类的加载

本文调试源码objc4-818.2,所以结论也仅限于该版本。

二、 分类的加载

书接上文,在methodizeClass中发现了attachToClass这个方法中对分类方法进行了处理。

前提:
代码语言:javascript
复制
@implementation HRTest
@property(nonatomic, copy)NSString *name;
-(void)sayHappy;
+(void)load{ }
@end

@implementation HRTest (cate1)
-(void)sayHappy;
-(void)sayHappyCate1;
+(void)load{ }
@end
  • 类和分类中都实现了+load方法,后续会用到。

1、分类的加载时机

代码语言:javascript
复制
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方法,则该类是一个懒加载类,类的加载时机推迟到第一次消息调用。那个分类的加载时机是什么时候呢?

  1. 断点设置在methodizeClass,因为attachCategories不会被调用;
  2. 堆栈信息看到起点是在类第一次消息发送时;
  3. 在类从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中读取出来的。

代码语言:javascript
复制
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);
    }
}
  • 通过该方法完成分类方法、属性、协议的获取、排序、插入
  • 分类中属性是不自动生成setget方法;

3、分类方法的插入

OC底层探索17 - 类的加载(上)已经提到过该方法的一种情况,事实上该方法有3种情况.

代码语言:javascript
复制
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吗?

代码语言:javascript
复制
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方法

代码语言:javascript
复制
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(类)
代码语言:javascript
复制
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(分类)
代码语言:javascript
复制
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方法

代码语言:javascript
复制
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
代码语言:javascript
复制
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方法的调用
面试题

题:如果类和分类有同名方法,调用会调用哪个方法?

答:两种情况:

  1. 如果是普通方法,则会调用分类中的重名方法
  2. 如果是+load方法,则先调用类中的+load,在依次调用分类的load.

总结

类的加载-分类的加载-load方法调用后,加载一个类所有的工作都已经完成了,等待后续使用。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/6/24 下,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 二、 分类的加载
    • 前提:
      • 1、分类的加载时机
        • 1.1 类、分类都不实现+load
        • 1.2 类、分类的4类情况
      • 2、分类的加载
        • 3、分类方法的插入
        • 三、 load_images(非懒加载类)
          • 1、prepare_load_methods 准备所有load方法
            • 1.1 add_class_to_loadable_list(类)
            • 1.2 add_category_to_loadable_list(分类)
          • 2、prepare_load_methods 调用所有load方法
            • 2.1 call_class_loads
            • 面试题
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档