iOS-底层原理 13 分类的本质和加载过程

740

在日常开发中经常会使用到分类,对分类的了解也仅限于使用。但是对它的底层实现还不是那么了解,本文主要根据源码探索分类。

分类的介绍

Category是Objective-C 2.0之后添加的语言特性,分类、类别其实都是指的Category。Category的主要作用是为已经存在的类添加方法。
Objective-C 中的 Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。

分类的使用

  • 声明私有方法
  • 分解体积庞大的类文件
  • 把Framework私有方法公开
  • 模拟多继承(另外可以模拟多继承的还有protocol)

分类的特点

  • 1.分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性 ;
  • 2.分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量,会编译通过,但是引用变量会报错;
  • 3.如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为 分类 > 本类 > 父类
  • 4.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。
  • 5.运行时决议
  • 6.同名分类方法生效取决于编译顺序
  • 7.名字相同的分类会引起编译报错

分类的本质

使用clang 编译.m文件得到如下代码所示,可以得到.cpp的内容。

clang -rewrite-objc main.m -o main.cpp

编译后的.cpp部分内容如下所示。说明分类是一个_category_t类型的结构体。

static struct /_method_list_t/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_good __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"studyMusic", "v16@0:8", (void *)_I_NSObject_good_studyMusic}}
};

static struct /_method_list_t/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_good __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"studyEnglish", "v16@0:8", (void*)_C_NSObject_good_studyEnglish}}
 };
    
static struct /_prop_list_t/ {
     unsigned int entsize;  // sizeof(struct _prop_t)
     unsigned int count_of_properties;
     struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_NSObject_$_good **__attribute__** ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"goodSize","T@\"NSString\",&,N"}}
};

extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_NSObject;

static struct _category_t _OBJC_$_CATEGORY_NSObject_$_good __attribute__*((used, section ("__DATA,__objc_const"))) = 
{
"NSObject",
0, // &OBJC_CLASS_$_NSObject,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_good,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_good,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_NSObject_$_good,
};

在objc源码中查看分类:

struct category_t {
    const char *name;//名称
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;//实例方法列表
    WrappedPtr<method_list_t, PtrauthStrip> 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;
    }
};

通过编译的中间层代码.cpp文件于objc4底层源码比较的到结论:

  1. name代表的是分类名字。
  2. 分类不区分类方法于实例方法,本质类加载过程分类属于插入数据,实例方法插入到类中,类方法插入到元类中,所以自己并不会有分元类。
  3. 分类的属性并不会自动生成getter、setter方法,这在.cpp文件中可以得知

分类的加载流程

分类的加载方法load_categories_nolock

根据注释可以看出实现分类的加载主要是由 attachCategoriesattachToClass实现

static void load_categories_nolock(header_info *hi) {
{
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    size_t count;
    
    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            locstamped_category_t lc{cat, hi};
            if (!cls) {
                continue;
            }
            
            // Process this category.
            if (cls->isStubClass()) {
                // 类在初始化之前不知道它们的元类,
                // 所以我们必须向类本身添加带有类方法或属性的类别。
                // methodizeClass()将找到它们,并根据需要将它们添加到元类中。
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {
                //注册分类到类信息中。
                //如果类已实现,则重建该类的方法列表
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) {
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }
                
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {

                    if (cls->ISA()->isRealized()) {
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };
    
    processCatlist(hi->catlist(&count));
    processCatlist(hi->catlist2(&count));
}

attachCategories

将方法列表、属性和协议从类别附加到类。 假设cats中的categories都加载了,并按加载顺序排序,最旧的类别在先。

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

   /**
     * 只有少数班级在发布时有超过 64 个类别。
     * 这使用了一点堆栈,并避免了 malloc。
     * 类别必须以正确的顺序添加,即从后到前。为了通过分块来做到这一点,我们迭代cats_list
     * 从前到后,向后构建本地缓冲区,并在块上调用 attachLists。 attachLists 将列表放在前面,因此最终结果按预期顺序排列。
     */
    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 rwe = cls->data()->extAllocIfNeeded();
    
    const char *mangledName = cls->nonlazyMangledName();
    if (strcmp(mangledName, "FFPerson") == 0)
    {
        if (!isMeta) {
            printf("%s -FFPerson....\n",__func__);
        }
    }

    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, __func__);
                //倒叙插入
                rwe->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) {
                rwe->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) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

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

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

常见问题

Q:分类的对象方法类方法都存在哪里?

一个类的所有分类的 对象方法放在类对象中,所有分类的类方法存放在元类中

Q:分类的方法是如何添加到类对象方法列表中的?

大概流程

  • 1.通过Runtime加载某个类的所有Category数据

  • 2.把所有Category的方法、属性、协议数据,合并到一个大数组中

  • 3.后面参与编译的Category数据,会在数组的前面

  • 4.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

Q:Category的实现原理

  • Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
  • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

Q:Category和Class Extension的区别是什么?

  • Class Extension在编译的时候,它的数据就已经包含在类信息中
  • Category是在运行时,才会将数据合并到类信息中

Q:+load方法调用顺序?

1. 先调用类的+load方法
  • 1.1按照编译先后顺序调用(先编译,先调用)
  • 1.2先调用父类的+load再调用子类的+load
2. 再调用分类的+load方法
  • 2.1按照编译先后顺序调用(先编译,先调用)
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
    call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();
  • 每个类、分类的+load,在程序运行过程中只调用一次,只有在加载类时候调用一次
  • 不存在分类的+load方法覆盖类的+load方法

Q:load、initialize的调用顺序?

1.load
  • 1> 先调用类的load
    a) 先编译的类,优先调用load
    b) 调用子类的load之前,会先调用父类的load
  • 2> 再调用分类的load
    a) 先编译的分类,优先调用load
2.initialize

1> 先初始化父类
2> 再初始化子类(可能最终调用的是父类的initialize方法)