Category的本质<一>

Category的本质<二>load,initialize方法
Category的本质<三>关联对象

一 写在开头

Category大家应该用过,它主要是用在为对象的不同类型的功能分块,比如说人这个对象,我们可以为其创建三个分类,分别对应学习,工作,休息。
下面创建了一个Person类和两个Person类的分类。分别是Person+Test和Person+Eat。这三个类中各有一个方法。

//Person类
- (void)run;
- (void)run{
    
    NSLog(@"run");
}
//Person+Test分类
- (void)test;
- (void)test{
    
    NSLog(@"test");
}
//Person+Eat分类
- (void)eat;
- (void)eat{
    
    NSLog(@"eat");
}

当我们需要使用这些分类的时候只需要引入这些分类的头文件即可:

#import "Person+Test.h"
#import "Person+Eat.h"

Person *person = [[Person alloc] init];
 [person run];
 [person test];
 [person eat];

我们都知道,函数调用的本质是消息机制。[person run]的本质就是objc_mgs(person, @selector(run)),这个很好理解,由于对象方法是存放在类对象中的,所以向person对象发送消息就是通过person对象的isa指针找到其类对象,然后在类对象中找到这个对象方法。
[person test][person run]都是调用分类的对象方法,本质应该一样。[person test]的本质就是objc_mgs(person, @selector(test)),给实例对象发送消息,person对象通过自己的isa指针找到类对象,然后在自己的类对象中查找这个实例方法,那么问题来了,person类对象中有没有存储分类中的这个对象方法呢?Person+Test这个分类会不会有自己的分类的类对象,将分类的对象方法存储在这个类对象中呢?

我们要清楚的一点是每个类只有一个类对象,不管这个类有没有分类。所以分类中的对象方法也就存储在Person类的类对象中。后面我们会通过源码证实这一点。

二 底层结构

我们在第一部分讲了,分类中的对象方法和类方法最终会合并到类中,分类中的对象方法合并到类的类对象中,分类中的类方法合并到类的元类对象中。那么这个合并是什么时候发生的呢?是在编译器编译器就帮我们合并好了吗?实际上是在运行期,进行的合并。
下面我们通过将Objective-c的代码转化为c++的源码窥探一下Category的底层结构。我们在命令行进入到存放Person+Test.m这个文件的文件夹中,然后在命令行输入clang -rewrite-objc Person+Test.m,这样Person+Test.m这个文件就被转化为了c++的源码Person+Test.cpp。
我们打开这个.cpp文件,由于这个文件非常长,所以我们直接拖到最下面,找到_category_t这个结构体。这个结构体就是每一个分类的结构:

struct _category_t {
    const char *name; //类名
    struct _class_t *cls;
    const struct _method_list_t *instance_methods; //对象方法列表
    const struct _method_list_t *class_methods; //实例方法列表
    const struct _protocol_list_t *protocols;  //协议列表
    const struct _prop_list_t *properties;  //属性列表
};

我们接着往下找到这个结构体的初始化:

static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
    0,
    0,
};

通过结构体名称_OBJC_$_CATEGORY_Person_$_Test我们可以知道这是Person+Test这个分类的初始化。类名对应的是"Person",对象方法列表这个结构体对应的是&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,类方法列表这个结构体对应的是&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,其余的初始化都是空。
然后我们找到&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test这个结构体:

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_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_Test_test}}
};

可以看到这个结构体中包含一个对象方法test,这正是Person+Test这个分类中的对象方法。
然后我们再找到&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test这个结构体:

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_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_Person_Test_test2}}
};

同样可以看到这个结构体,它包含一个类方法test2,这个同样是Person+Test中的类方法。

三 利用runtime进行合并

由于整个合并的过程是通过runtime进行实现的,所以我们要了解这个过程就要通过查看runtime源码去了解。下面是查看runtime源码的过程:

  • 1.找到objc-os.mm这个文件,这个文件是runtime的入口文件。
  • 2.在objc-os.mm中找到_objc_init(void)这个方法,这个方法是运行时的初始化。
  • 3.在_objc_init(void)中会调用_dyld_objc_notify_register(&map_images, load_images, unmap_image);,这个函数会传入map_images这个参数,我们点进这个参数。
  • 4.点击进去map_images我们发现其中调用了map_images_nolock(count, paths, mhdrs);这个函数,我们点进这个函数。
  • 5.map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[])这个函数非常长,我们直接拉到这个函数最下面,找到_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);这个函数,点击进去。
  • 6.void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)这个方法大概就是读取模块的意思了。
    这个函数也是非常长,我们大概在中间位置找到了这样一行注释
// Discover categories.

这个基本上就是我们要找的处理Category的模块了。
我们在这行注释下面找到这几行代码:

if (cls->isRealized()) {
       remethodizeClass(cls);
       classExists = YES;
                }

 if (cls->ISA()->isRealized()) {
       remethodizeClass(cls->ISA());  //class的ISA指针指向的是元类对象
               }

这个代码里面有一个关键函数remethodizeClass,通过函数名我们大概猜测这个方法是重新组织类中的方法,如果传入的是类,则重新组织对象方法,如果传入的是元类,则重新组织类方法。

  • 7.然后我们点进这个方法里面查看:
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

我们看到这段代码的核心是调用了attachCategories(cls, cats, true /*flush caches*/);这个方法。这个方法中传入了一个类cls和所有的分类cats。

  • 8.我们点进attachCategories(cls, cats, true /*flush caches*/);这个方法。这个方法基本上就是核心方法了。
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    //方法数组
    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;
    while (i--) {
        //取出某个分类
        auto& entry = cats->list[i];
//确定是对象方法还是类方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        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;
        }
    }
//得到类对象里面的数据
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //将所有分类的对象方法,附加到类对象的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
//将所有分类的协议,附加到类对象的协议列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}
  • 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));

这里mlists,proplists,protolists都是用两个修饰的,说明是申请了一个二维数组。这三个二维数组里面的一级对象分别是方法列表,属性列表,以及协议列表。由于每一个分类Category都有一个方法列表,一个属性列表,一个协议列表,方法列表中装着这个分类的方法,属性列表中装着这个分类的属性。所以mlists也就是装着所有分类的所有方法。

  • 给前面创建的数组赋值
 while (i--) {
        //取出某个分类
        auto& entry = cats->list[i];
//确定是对象方法还是类方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

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

这段代码就很清楚了,通过一个while循环遍历所有的分类,然后获取该分类的所有方法,赋值给前面创建的大数组。

  • rw = cls->data();得到类对象里面的所有数据。
  • rw->methods.attachLists(mlists, mcount);将所有分类的方法,附加到类的方法列表中。
  • 9.我们点进这个方法里面看看具体的实现:
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]));
        }
    }

传进来的这个addedLists参数就是前面得到的这个类的所有分类的对象方法或者类方法,而addedCount就是addedLists这个数组的个数。假设这个类有两个分类,且每个分类有两个方法,那么addedLists的结构大概就应该是这样的:
[
[method, method]
[method, method]
]
addedCount = 2
我们看一下这个类的方法列表之前的结构:


7F4EE0B0-BD7D-4162-AD28-76209E034096.png

所以oldCount = 1

setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;

这一句是重新分配内存,由于要把分类的方法合并进来,所以以前分配的内存就不够了,重新分配后的内存:


82E1D1CD-096B-4AA8-9B23-96D05CAF7AD3.png
memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));

memmove这个函数是把第二个位置的对象移动到第一个位置。这里也就是把这个类本来的方法列表移动到第三个位置。

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

memcpy这个函数是把第二个位置的对象拷贝到第一个位置,也就是把addedLists拷贝到第一个位置,拷贝之后的内存应该是这样的:


E788A916-FD1B-4E98-9363-7F36AF16C403.png

至此就把分类中的方法列表合并到了类的方法列表中。
通过上面的合并过程我们也明白了,当分类和类中有同样的方法时,类中的方法并没有被覆盖,只是分类的方法被放在了类的方法前面,导致先找到了分类的方法,所以分类的方法就被执行了。

四 总结

1.通过runtime加载某个类的所有Category数据。
2.把所有Category的方法,属性,协议数据合并到一个大数组中,后参与编译的Category数据,会存放在数组的前面。
3.将合并后的分类数据(方法,属性,协议),插入到类原来数据的前面。
Category的本质<二>load,initialize方法

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

推荐阅读更多精彩内容