runtime(2)Category

声明:本文仅供记忆使用,并不适合新手小白观看

category应用场景

  • 把类的不同实现方法分开到不同的文件里。
  • 声明私有方法。
  • 模拟多继承。
  • framework 私有方法公开化。

category的实质

  • Category 的本质就是 _category_t 结构体 类型
1
2
3
4
5
6
7
8
9
struct category_t {
const char *name; // 类名
classref_t cls; // 类,在运行时阶段通过 clasee_name(类名)对应到类对象
struct method_list_t *instanceMethods; // Category 中所有添加的对象方法列表
struct method_list_t *classMethods; // Category 中所有添加的类方法列表
struct protocol_list_t *protocols; // Category 中实现的所有协议列表
struct property_list_t *instanceProperties; // Category 中添加的所有属性
};

Category的大致调用过程

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

  2. 把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面

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

    objc-os.mm

    _objc_init
    map_images
    map_images_nolock

    objc-runtime-new.mm

    _read_images
    remethodizeClass 重建类的方法列表
    attachCategories 为类添加未依附的分类
    attachLists
    realloc、memmove、 memcpy

Category的核心方法

  1. _read_images 方法,忽略 _read_images 方法中其他与本文无关的代码,得到如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 获取镜像中的分类数组
    category_t **catlist =
    _getObjc2CategoryList(hi, &count);
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    // 遍历分类数组
    for (i = 0; i < count; i++) {
    category_t *cat = catlist[i];
    Class cls = remapClass(cat->cls);
    // 处理这个分类
    // 首先,使用目标类注册当前分类
    // 然后,如果实现了这个类,重建类的方法列表
    bool classExists = NO;
    if (cat->instanceMethods || cat->protocols
    || cat->instanceProperties)
    {
    addUnattachedCategoryForClass(cat, cls, hi);
    if (cls->isRealized()) {
    remethodizeClass(cls);
    classExists = YES;
    }
    }

    if (cat->classMethods || cat->protocols
    || (hasClassProperties && cat->_classProperties))
    {
    addUnattachedCategoryForClass(cat, cls->ISA(), hi);
    if (cls->ISA()->isRealized()) {
    remethodizeClass(cls->ISA());
    }
    }
    }

    主要用到了两个方法:
    addUnattachedCategoryForClass(cat, cls, hi); 为类添加未依附的分类
    remethodizeClass(cls); 重建类的方法列表
    通过这两个方法达到了两个目的:
    把 Category(分类) 的对象方法、协议、属性添加到类上;
    把 Category(分类) 的类方法、协议添加到类的 metaclass 上。

  2. addUnattachedCategoryForClass(cat, cls, hi); 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader) {
    runtimeLock.assertLocked();

    // 取得存储所有未依附分类的列表:cats
    NXMapTable *cats = unattachedCategories();
    category_list *list;
    // 从 cats 列表中找到 cls 对应的未依附分类的列表:list
    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
    list = (category_list *)
    calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
    list = (category_list *)
    realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    // 将新增的分类 cat 添加 list 中
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    // 将新生成的 list 添加重新插入 cats 中,会覆盖旧的 list
    NXMapInsert(cats, cls, list);
    }

    addUnattachedCategoryForClass(cat, cls, hi); 的执行过程可以参考代码注释。执行完这个方法之后,系统会将当前分类 cat 放到该类 cls 对应的未依附分类的列表 list 中。这句话有点拗口,简而言之,就是:把类和分类做了一个关联映射。

    实际上真正起到添加加载作用的是下边的 remethodizeClass(cls); 方法。

  3. remethodizeClass(cls); 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    static void remethodizeClass(Class cls)
    {
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // 取得 cls 类的未依附分类的列表:cats
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
    // 将未依附分类的列表 cats 附加到 cls 类上
    attachCategories(cls, cats, true /*flush caches*/);
    free(cats);
    }
    }

    remethodizeClass(cls); 方法主要就做了一件事:调用 attachCategories(cls, cats, true); 方法将未依附分类的列表 cats 附加到 cls 类上。所以,我们就再来看看 attachCategories(cls, cats, true); 方法。

  4. attachCategories(cls, cats, true); 方法
    我发誓这是本文中加载 Category(分类)过程的最后一段代码。不过也是最为核心的一段代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    static void 
    attachCategories(Class cls, category_list *cats, bool flush_caches)
    {
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // 创建方法列表、属性列表、协议列表,用来存储分类的方法、属性、协议
    method_list_t **mlists = (method_list_t **)
    malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
    malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
    malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0; // 记录方法的数量
    int propcount = 0; // 记录属性的数量
    int protocount = 0; // 记录协议的数量
    int i = cats->count; // 从分类数组最后开始遍历,保证先取的是最新的分类
    bool fromBundle = NO; // 记录是否是从 bundle 中取的
    while (i--) { // 从后往前依次遍历
    auto& entry = cats->list[i]; // 取出当前分类

    // 取出分类中的方法列表。如果是元类,取得的是类方法列表;否则取得的是对象方法列表
    method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
    if (mlist) {
    mlists[mcount++] = mlist; // 将方法列表放入 mlists 方法列表数组中
    fromBundle |= entry.hi->isBundle(); // 分类的头部信息中存储了是否是 bundle,将其记住
    }

    // 取出分类中的属性列表,如果是元类,取得的是 nil
    property_list_t *proplist =
    entry.cat->propertiesForMeta(isMeta, entry.hi);
    if (proplist) {
    proplists[propcount++] = proplist;
    }

    // 取出分类中遵循的协议列表
    protocol_list_t *protolist = entry.cat->protocols;
    if (protolist) {
    protolists[protocount++] = protolist;
    }
    }

    // 取出当前类 cls 的 class_rw_t 数据
    auto rw = cls->data();

    // 存储方法、属性、协议数组到 rw 中
    // 准备方法列表 mlists 中的方法
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    // 将新方法列表添加到 rw 中的方法列表中
    rw->methods.attachLists(mlists, mcount);
    // 释放方法列表 mlists
    free(mlists);
    // 清除 cls 的缓存列表
    if (flush_caches && mcount > 0) flushCaches(cls);

    // 将新属性列表添加到 rw 中的属性列表中
    rw->properties.attachLists(proplists, propcount);
    // 释放属性列表
    free(proplists);

    // 将新协议列表添加到 rw 中的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    // 释放协议列表
    free(protolists);
    }

    attachCategories(cls, cats, true); 方法的注释中可以看出这个方法就是存储分类的方法、属性、协议的核心代码。

    但是需要注意一些细节问题:
    Category(分类)的方法、属性、协议只是添加到原有类上,并没有将原有类的方法、属性、协议进行完全替换。
    举个例子说明就是:假设原有类拥有 MethodA方法,分类也拥有 MethodA 方法,那么加载完分类之后,类的方法列表中会拥有两个 MethodA方法。
    Category(分类)的方法、属性、协议会被添加到原有类的方法列表、属性列表、协议列表的最前面,而原有类的方法、属性、协议则被移动到了列表后面。
    因为在运行时查找方法的时候是顺着方法列表的顺序依次查找的,所以 Category(分类)的方法会先被搜索到,然后直接执行,而原有类的方法则不被执行。这也是 Category(分类)中的方法会覆盖掉原有类的方法的最直接原因。