温故而知新 - ObjC Category 实现原理

摘要

Category 主要作用是为已有的类,添加方法、属性、协议。

其实现原理,一方面,在编译时期,会生成 category_t 及相关结构体。

另一方面,在运行时期,会将这些方法、属性、协议添加到类之中。

category_t 结构

// objc4-750.1/runtime/objc-runtime-new.h
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);
};

比较特殊地,它并非继承 objc_object。

而且,当中并没有 ivars,这也解释了,为什么 Category 不能添加变量。

插个题外话,其中的 _classProperties 比较少见,查了下,才发现是为配合 Swift,Xcode8 推出的新特性。

语法如下:

@property (class) NSString *someString;

编译时期

  1. 编译时期,不同 Category,会被编译成不同的 category_t 结构体。
  2. 而 Category 中的方法、属性、协议等列表,也会被编译成不同的结构体,存于 category_t 之中。
  3. Category 的相关信息最终会在 Mach-O 中有所记录。

具体的,先来个简单代码,看看会编译成什么。

@implementation Cat (JD)
- (void)run {
    NSLog(@"run");
}
@end

转化成 c++ 代码

clang -x objective-c -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0  Cat+JD.m

可看到 run 方法被转换成以下静态函数

// @implementation Cat (JD)

static void _I_Cat_JD_run(Cat * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_mv_t42fx9pj0mn9k96m8y1rwwd40000gn_T_Cat_JD_0bdbd6_mi_0);
}
// @end

创建了名为 _OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD_method_list_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_Cat_$_JD __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Cat_JD_run}}
};

创建名为 _OBJC_$_CATEGORY_Cat_$_JD_category_t (即 category_t) 结构体。

static struct _category_t _OBJC_$_CATEGORY_Cat_$_JD __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
    "Cat",
    0, // &OBJC_CLASS_$_Cat,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD,
    0,
    0,
    0,
};

与原来的类关联

static void OBJC_CATEGORY_SETUP_$_Cat_$_JD(void) {
    _OBJC_$_CATEGORY_Cat_$_JD.cls = &OBJC_CLASS_$_Cat;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
    (void *)&OBJC_CATEGORY_SETUP_$_Cat_$_JD,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_Cat_$_JD,
};

源码中有不少 section ("__DATA,__objc_const") 这样的代码,其实就是 Mach-O 的相关存储位置,会在加载时用到。

运行时期

Objc 初始化时,从 Mach-O 中拿到编译时期产生的 category_t 数组。

再做了以下操作:

  1. 将 category_t 中的实例属性、实例方法、协议添加到 class 之中。
  2. 将类属性、类方法、协议添加到 metaClass 中。

添加之后, Category 方法、属性、协议都会在原列表的前面。

来看看具体实现。

_read_images()

从 ObjC 初始化方法 _objc_init() 开始,会调用 _read_images()

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
    ...
    // _objc_init->map_images->map_images_nolock->_read_images->realizeClass
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_read_images() 之中,既给 class 添加了实例方法、协议、实例属性,也给 metaClass 添加了类方法、协议、类属性。

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
    ...
    // Discover categories.
    for (EACH_HEADER) {
        // 获取
        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);

            ...

            // Process this category.
            // First, register the category with its target class.
            // Then, rebuild the class's method lists (etc) if
            // the class is realized.
            bool classExists = NO;

            // 实例方法、协议、实例属性 -> class
            if (cat->instanceMethods ||  cat->protocols
                ||  cat->instanceProperties)
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                ...
            }

            // 类方法、协议、类属性 -> class
            if (cat->classMethods  ||  cat->protocols
                ||  (hasClassProperties && cat->_classProperties))
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                ...
            }
        }
    }

    ...
}

attachCategories()

而添加的具体逻辑,并不在 remethodizeClass() 之中,而在 attachCategories()。

其调用堆栈

20210330-2322.png

attachCategories() 核心代码如下。

可以看到,后加载到的 category 信息,会先添加。

// 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, category_list *cats, bool flush_caches)
{
    ...

    // 逆序添加
    // 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;
    while (i--) {
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 省略了属性、协议 ...
    }

    auto rw = cls->data();

    ...
    //  remethodizeClass() 调用的,需要更新缓存; methodizeClass() 则不需要
    if (flush_caches  &&  mcount> 0) flushCaches(cls);

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

    // 省略了协议 ...
}

list_array_tt::attachLists()

添加列表的逻辑在 list_array_tt::attachLists() 之中

从中可看到,会保留类原来的列表,只是将其顺序往后移动了,所以调用时,会优先调用分类方法。

// objc4-750.1/runtime/objc-runtime-new.h

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;

    if (hasArray()) {
        // many lists -> many lists
        uint32_t oldCount = array()->count;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
        memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
    }
    else {
        // 1 list -> many lists
        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;
        if (oldList) array()->lists[addedCount] = oldList;
        memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));
    }
}

many lists -> many lists 当中,关键的 2 行代码

memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));

其实也就是将 Category 里的列表,移动到原列表之前,翻译过来,可用一张图表示。

20210331-1800.png

小结

======== 编译时期 ========

不同 Category,会被编译成不同的 category_t 结构体。

包括其中的方法、属性、协议等列表,也被编译成结构体,存于 category_t 之中。

所有这些信息最终会在 Mach-O 中有所记录。

======== 运行时期 ========

Objc 初始化时,从 Mach-O 中拿到编译时期产生的 category_t 数组。

再做了以下操作:

  1. 将 category_t 中的实例属性、实例方法、协议添加到 class 之中。
  2. 将类属性、类方法、协议添加到 metaClass 中。

添加之后, Category 方法、属性、协议列表都会在前面。

疑问

上面有提到 Category 无法添加变量的原因是 category_t 中无 ivars。

那为什么不在 category_t 中也增加 ivars 呢?

个人见解如下:

结合 objc_class 的结构来看,变量列表存在于 class_ro_t 中,是不可变的。

而变量其实是存在于对象内存布局之中,Category 是给类添加东西,不应影响对象。

struct class_ro_t {
     // 变量列表
     const ivar_list_t * ivars;
 }

但不知正确与否,麻烦知道的同学告知一声,感激不尽!

感谢

深入理解 Objective-C:Category

Apple Developer Category

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容