iOS Objective-C 分类的加载

iOS Objective-C 分类的加载

前言

在我的另一篇文章iOS 应用的加载objc篇中分析了在objc协议sel等的加载,但是在文章中并没有详细的分析分类是如何加载的,那么分类是如何加载的呢?在本文我们会进行详细的分析。

1. 通过clang查看分类在底层的实现

我们打开一个新的objc4-779.1工程,新建一个LGTeacher的类,并创建一个LGTeacher+Test的分类。

LGTeacher+Test.m源码:

#import "LGTeacher+Test.h"

@implementation LGTeacher (Test)

+ (void)load{
    NSLog(@"分类 load");
}

- (void)setCate_p1:(NSString *)cate_p1{
    
    NSLog(@"%s",__func__);
}

- (NSString *)cate_p1{
    return @"cate_p1";
}

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

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

@end

通过clang编译我们的LGTeacher+Test.m

clang -rewrite-objc LGTeacher+Test.m -o category.cpp

我们查看category.cpp这个文件,可以找到分类被编译后的结果:

static struct _category_t _OBJC_$_CATEGORY_LGTeacher_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "LGTeacher",
    0, // &OBJC_CLASS_$_LGTeacher,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_Test,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_Test,
    0,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LGTeacher_$_Test,
};

我们可以看到LGTeacher_+_Test这个分类在底层实现实际是一个category_t的结构体。我们还可以看出我们的分类是存储在Mach-O__Data段的__objc_const节中的。

根据结构体内部的信息我们可以看出三行重要的代码:

  • _OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_Test
  • _OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_Test
  • _OBJC_$_PROP_LIST_LGTeacher_$_Test

以上三行分别对应着我的对象方法类方法和*属性

1.1 分类的定义

上面我们提到category_t,那么它到底是什么样的结构中呢,我们来到libobjc源码中进行查看其真正的定义。(PS:C++去掉下划线)

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

category_t的定义和刚才我们clang的结果我们可以得到如下结论:

  • name:是分类所关联类的名字
  • clsclang后这个值是0,但是我们根据其注释可知这是我们的类对象,只是在编译期这个值不存在。
  • instanceMethods:分类存储的对象方法
  • classMethods:分类存储的类方法
  • protocols:分类实现的协议
  • instanceProperties:分类定义的实例属性,不过我们一般在分类中添加属性都是通过关联对象来实现的
  • _classProperties:分类所定义的类属性,根据注释我们知道这个内容不一定一直在磁盘上保存。可能是私有属性,不一定一直都存在。
  • 下面还有是否是元类的一些处理

2. 分类的加载

我们在分析的加载的时候会涉及到懒加载类非懒加载类的区别。那么分类的加载又是怎么样的呢?是否也有懒加载非懒加载的区别呢?而且分类是依附于类的,类存在才会存在分类,那么类的懒加载非懒加载会不会对分类有什么影响呢?

其实一开始就是写了个分类,并没有考虑是否是懒加载,然后在_read_images函数中的Discover categories.段中添加下图红色框框中的代码,但是没到断点处,所以才有了上面那段的想法。

Discover categories.jpg

2.1 没有实现load的分类

2.1.1 类为懒加载的时候

此处就是都没实现+load方法,这在我们开发中也是比较常见的,只写业务逻辑,不需要通过+load方法进行提前加载。

那么此时分类在何处进行加载呢?根据我们上面刚开始时得到的结果,该分类的加载肯定不会走_read_images函数,在类的加载的时候我们关于懒加载类的加载是在第一次发送消息的时候,那么分类呢?首先想到的是我们在类加载的时候methodizeClass函数中的Attach categories

Attach categories.jpg

我们发现上图中的第二个断点是不会来到的,这个我们在应用的加载objc这篇文章中详细的分析了关于Attach categoriesif 分支 不会调用的原因。

此时我们查看lldb发现分类中的方法已经存在于ro中,如下图所示:

ro.jpg

虽然我们看到分类中的方法已经在ro中了,但是我们还是来看看if分支下面那行代码吧:

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);
        }
    }

attachToClass函数中主要还是通过attachCategories函数将获取到的分类列表进行附加操作,但是区分了元类和类。

那么attachCategories函数都做了什么工作呢?

attachCategories 源码:


// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rw = cls->data();

    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                rw->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rw->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rw->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rw->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rw->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rw->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

根据注释我们就可以知道该函数是将方法列表、属性和协议从分类附加到类上,如果所有分类都附加完毕则后来居上,后面附加的排在第一个。

我们分别在这两个函数开始处添加断点,发现来到methodizeClass的断点后并不会走attachCategories函数,到了attachToClass函数中也会因为if (it != map.end())的不成立直接跳过。

结论:

所以说对于懒加载的分类和懒加载的类同时使用时,分类也是在编译期间直接存储在了Mach-O中,然后在类加载的时候直接与类一起加载了。

2.1.2 类为非懒加载的时候

如果类是非懒加载,那么类就是走_read_images里面的流程,首先还是来点methodizeClass函数,沿用上面的断点和添加的信息。

此时运行我们的程序结果跟上面一模一样,这里就不上图了,因为都是一样的,不管是通过发送消息来到methodizeClass函数还是通过_read_images来到methodizeClass最终都会来到methodizeClass。最终也会调用attachToClass函数,但是依然不会调用attachCategories函数。

结论:

所以说,类是非懒加载的还是懒加载的都不会影响分类在编译期写入到Mach-O中,通过类加载的时候直接读入ro

2.2 实现了load的分类

虽然在我们的开发中一般很少在分类中去写+load方法。但是我们还是来看看在分类中实现+load方法的时候,分类是怎么加载的。

2.2.1 类为懒加载的时候

还是上面的断点,我们运行一下我们的程序,这时候断点首先来到了_read_images函数中的Discover categories.

Discover categories.jpg

此时我们通过控制台查看我们的rorw内部并没有什么数据。

这个时候对于懒加载类,并不是由发送消息触发的类的加载,所以说懒加载类的加载也会由它的非懒加载类触发。

我们添加如下图所示的断点,看看会不会在这里附加我们的分类,然后并不会,因为在这里类还没有被实现,所以类不存在分类还没有被附加,元类也一样。所以说对于非懒加载分类懒加载类组合的时候,分类不是在这里加载的。

Discover categories 2.jpg

我们过掉这些断点,再次来到了methodizeClass函数中。

15995512557473.jpg

此时我们的ro中依旧没有数据,那么我们首先看看调用堆栈。

调用堆栈.jpg

调用堆栈中我们可以看到prepare_load_methods函数,那么这个函数做了些什么事情呢?

prepare_load_methods 源码:

        void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

由此函数我们可以知道在methodizeClass的断点是由prepare_load_methods中的realizeClassWithoutSwift(cls, nil);这行代码调用过去的,我们也知道realizeClassWithoutSwift函数是加载我们的OC类的。 在prepare_load_methods函数中:

  • 首先定义变量和加锁就没什么可说的了
  • 然后通过_getObjc2NonlazyClassList函数获取非懒加载的类
  • 遍历这些非懒加载的类,进行重新映射
  • 接下来就是通过_getObjc2NonlazyCategoryList获取非懒加载的分类列表
  • 遍历这些非懒加载的分类,因为分类依附于类所以在这里要提前实现类,如果类实现了也会在realizeClassWithoutSwift函数中返回。
  • 调用add_category_to_loadable_list

add_category_to_loadable_list 源码:

/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(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));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

译 : 分类们的父类(依附的类)存在,并且该分类已附加到其类中。 在其依附的类建立连接并调用其自己的+ load方法之后,将该类别安排为+ load。(PS: 没太明白啥意思)

小结:

  1. 对于存在的非懒加载分类的情况会通过prepare_load_methods函数调用realizeClassWithoutSwift进行类的加载
  2. 类的加载以及分类的处理会通过realizeClassWithoutSwift函数进一步调用methodizeClass进一步处理
  3. methodizeClass的处理我们在上面以及看过

接下来所以我们可以大胆测猜想,这次肯定会调用到attachCategories函数,我们可以直接来到attachCategories函数中进行查看。

attachCategories.jpg

attachCategories执行完毕后我们的ro中虽然没有数据,但是在rw中已经有了我们分类的方法,因为roreadonly,在类加载的时候从Mach-O中读取出来的,既然此时分类的数据不在编译期写在了Mach-O中,也就不会存在于ro中。

总结:

所以说非懒加载分类懒加载类组合时,通过调用load_images->prepare_load_methods->realizeClassWithoutSwift->methodizeClass->objc::unattachedCategories.attachToClass->attachCategories协议方法属性附加到类上。

2.2.2 类为非懒加载的时候

此时就是在分类和类中都实现+load方法,对于非懒加载类我们已经很熟悉了,在_read_images里面进行加载,我们沿用上面的断点:

首先还是会来到_read_images中的Discover categories处,但是处理还是与懒加载类的时候一样,所以说对于非懒加载的分类,才会通过_getObjc2CategoryList或者_getObjc2CategoryList2取出分类列表,但是由于类还未实现都不会进行进一步的处理。在objc4-779.1Discover categories的代码在Realize non-lazy classes前,所以Discover categories这段代码貌似没什么用处,在objc4-756.2中这两段代码顺序相反,就会有作用。

我们跳过这些断点来到methodizeClass中:

methodizeClass.jpg

在断点处我们分类中的信息同样不在rorw中。 同样我们来到attachCategories函数中:

attachCategories.jpg

attachCategories函数执行完毕后,我们发现我们的分类方法已经在rwmethods中。至此分类已经加载到了内存。

所以说非懒加载分类非懒加载类组合时,我们的分类在类加载的时候就顺便加载了,流程为map_images->_read_images->realizeClassWithoutSwift->methodizeClass->objc::unattachedCategories.attachToClass->attachCategories

3. 总结

关于分类的加载非常的绕,因为我们要考虑类的加载方式,现在总结如下:

  • 懒加载分类 + 懒加载类

直接通过编译打包生成Mach-O时将分类信息写入,与类一起加载

  • 懒加载分类 + 非懒加载类

直接通过编译打包生成Mach-O时将分类信息写入,与类一起加载

  • 非懒加载分类 + 懒加载类

通过load_images->prepare_load_methods进一步加载。

  • 非懒加载分类 + 非懒加载类

通过map_images->_read_images在类加载的时候加载

注: 分类的加载并不像我们想象的那样在_read_imagesDiscover categories 进行加载,而是根据不同的组合有更多种的加载方式。至此关于Objective-C 分类的加载就分析完毕了,流程比较繁琐,文章排版一般,因为分析的时候太繁琐了,就一步一步的罗列了,有的地方也没上图说明,如有问题欢迎指正。

你可能感兴趣的:(iOS Objective-C 分类的加载)