Category底层实现分析1 - runtime合并分类信息

2018年08月29日

  • 在3.8.2里补上objc_object结构的解析,说明它与objc_class在成员变量结构上是相似的

2018年08月26日

  • 添加参考使用源码版本说明

注:分析参考 MJ底层原理班 内容,本着自己学习原则记录

本文使用的源码为objc4-723

1 Category 是什么

1.1 语言特性

  • category是Objective-C 2.0之后添加的语言特性

1.2 就是对象

  1. 从文件类型上观察,与普通类一样,具有.h 和.m文件


  2. 从文件内容上看,与普通类一样,具有声明和实现编码格式

  • Person
/// Person
@interface Person : NSObject
@end

/// Person.m
@implementation Person
@end
  • Person+Test1
/// Person+Test1.h 
@interface Person (Test1)
@end

/// Person+Test1.m
@implementation Person (Test1)
@end
  1. 将Person+Test1.m转为C++代码
  • 执行命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test1.m,然后获得Person+Test1.cpp文件

  • 通过struct _category_t类型的结构体来表示 Person+Test1

    Person+Test1

struct _category_t
  • struct _category_t包含的成员变量有
    1)、类的名字(name)
    2)、类(cls)
    3)、category中所有给类添加的实例方法的列表(instance_methods)
    4)、category中所有添加的类方法的列表(class_methods)
    5)、category实现的所有协议的列表(protocols)
    6)、category中添加的所有属性(properties)

结合之前对NSObject的本质探究的描述,它们都是通过结构体形式来表现,所以可以说明Category本质上也是一个对象

1.3 应用场景

  1. category的主要作用是为已经存在的类添加方法。
    除此之外,apple还推荐了category的另外两个使用场景
  • 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,
    a)可以减少单个文件的体积
    b)可以把不同的功能组织到不同的category里
    c)可以由多个开发者共同完成一个类
    d)可以按需加载想要的category 等等。
  • 声明私有方法
  1. 不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了category的其他几个使用场景:
  • 模拟多继承
  • 把framework的私有方法公开

REF

2 Category 在 runtime 源码中的 struct _category_t 实现

  • 基本与通过命令行获取的类型描述差不多


    struct _category_t

3 程序运行时,runtime 对 Category 的处理逻辑探究

runtime 会将Category 的instanceMethodscalssMethodsprotocols等信息合并到class-objectmeta-class

在 runtime 源码中查找初始化类时对 Category 的处理逻辑

3.1 通过runtime源码,找到文件 objc-os.mm 文件

3.2 在里面找到void _objc_init(void)方法,它是OC 的 runtime 启动初始化方法

void _objc_init(void)

3.3 搜索map_images方法,可获得map_imagesload_imagesunmap_image等方法实现

`map_images`、`load_images`、`unmap_image`等方法实现

3.4 对比发现

load_images方法与+load 的实现相关
unmap_image方法则与map_images相反
所以先继续探究map_images函数的实现,其中调用了函数map_images_nolock

3.5 map_images_nolock内调用了_read_images函数,且其中一个参数是 totalClasses也就是处理所有类对象相关动作函数(省略了其他源码)

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
// ... 其他行为代码

    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

// ... 其他行为代码

3.6 _read_images中对 Category 处理的逻辑部分(省略了其他源码)

/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked 
* list beginning with headerList. 
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    /// ... 其他行为代码

    size_t count;

    // 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); // 获取该分类对应的类

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // 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;
            /// 处理类对象
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            { // 该分类的结构体中实例方法或协议或实例属性中,其中一个成员变量有值即进入对应的 rebuild 环节
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls); /// 重点,重建类对象的方法列表
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            /// 处理元类对象
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA()); /// 重点,重建元类对象的方法列表
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

    ts.log("IMAGE TIMES: discover categories");

    // Category discovery MUST BE LAST to avoid potential races 
    // when other threads call the new category code before 
    // this thread finishes its fixups.

    /// ... 其他行为代码

3.7 static void remethodizeClass(Class cls)函数实现,其中调用函数attachCategories

static void remethodizeClass(Class cls)

3.8 attachCategories函数实现

注意其中的 while 循环,这是一个倒序组装逻辑,通过 i-- 倒序遍历过程,在组合 mlists 时,会将最后编译的分类方法列表信息添加至 mlists 数组的最前面

3.8.1 最后会将从分类中收集的对象方法列表(mlists) 、属性信息列表(proplists)、协议信息列表(protolists)拼接到类的 rw 结构中的对应列表内。

3.8.2 auto rw = cls->data();的理解:

实例对象、类对象、元类对象的基本内存结构如下,其中类对象和元类对象的本质结构都是struct objc_class类型 而 instance 对象的本质结构是 struct objc_object

引子 MJ 底层课课件

struct objc_class类型内存结构如下:(省略了其他源码)

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    /// ... 其他行为代码
}

objc_object类型内存结构如下:(省略了其他源码)

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    /// ... 其他行为代码
}

而在objc_object结构中的 isa_t isa;, 这个 isa_t 结构如下:它内部包含一个 Class cls 成员变量,而 Class 定义是 typedef struct objc_class *Class;从此也就是说明objc_object具有上述objc_class的内存结构

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

    /// ... 其他行为代码
}

所以 auto rw = cls->data(); 获取得到的 rw 类型即使struct class_rw_t 类型,其结构如下(省略了其他源码),也说明了 rw->methodsrw->propertiesrw->protocols等操作原理。

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
    /// ... 其他行为代码
}

基本关系如下图所示:


图片引自 MJ 底层课课件

attachLists()(拼接从分类中获取的实例方法、属性、协议等列表)的实现方式如下:

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

3.9 memmove 和 memcpy 的使用区别

* 先使用 memmove 对原类的方法列表元素移动到扩容数组最后面,而不用 memcpy 的目的是保证准确性,如果使用 memcpy 的话,一旦dst 和 src 出现重叠情况则无法保证准确性
* 后面使用 memcpy 则无需考虑重叠问题,因为该操作是两个列表对象的复制操作,没有重叠风险。

4 runtime 处理 Category 后的结论

  1. 分类中的方法、属性、协议等信息会通过 runtime 机制,被附加到对应类的类对象和元类对象结构中。
  2. 因为倒序组装所有分类列表和向前附加的动作(将分类的方法等插入到类对象列表前面),这就会导致:
    • 分类与类,优先调用分类的方法
    • 分类与分类,优先调用后编译的分类方法

5 相关题

5.1 Category的使用场合是什么?

  • 参考第1点

5.2 Category的实现原理

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

4.3 Category和Class Extension的区别是什么?

  • Category 具有@implementation 而 Class Extension 没有
  • Class Extension 基本目的是私有化(一般写在.m文件中的)属性、方法、成员变量、协议等信息
  • 必须有一个类的源码才能为一个类添加Extension,所以一般情况下你无法为系统的类比如NSString添加extension(因为Extension只能写在类的.h或.m中),但是可以通过写在 Category 中间接导入
  • Class Extension在编译的时候,它的数据就已经包含在类信息中
  • Category 是在运行时,才会将数据合并到类信息中

文/Jacob_LJ(简书作者)
PS:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载需联系作者获得授权,并注明出处,所有打赏均归本人所有!

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

推荐阅读更多精彩内容