iOS-底层-Category分类

iOS中的Category我们经常使用,主要是给一些类添加新的方法,或者拆分类。进行方法调用的时候,如果调用的是写在类里面的方法,调用顺序是:首先,实例对象根据它的isa找到类对象,然后去类对象里面的方法列表里面寻找方法的实现,如果找到,就会调用这个方法,完毕。但是如果调用的方法是写在分类里面,那么调用流程是什么呢?

其实无论写多少分类,最后运行的时候,Runtime会把分类所有的对象方法合并到类对象的方法列表里面,把类方法合并到元类对象的方法列表里面

一. 编译的时候

首先看一下编译的时候分类发生了什么。
创建一个MJPerson类,给MJPerson类添加一个MJPerson+Eat分类,在分类中添加属性、协议、对象方法、类方法,代码如下:

#import "MJPerson.h"

@interface MJPerson (Eat) <NSCopying, NSCoding>

- (void)eat;

@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;

@end
#import "MJPerson+Eat.h"

@implementation MJPerson (Eat)

- (void)run
{
    NSLog(@"MJPerson (Eat) - run");
}

- (void)eat
{
    NSLog(@"eat");
}

- (void)eat1
{
    NSLog(@"eat1");
}

+ (void)eat2
{
    
}

+ (void)eat3
{
    
}
@end

按照NSObject的本质文章中的指令,把MJPerson+Eat.m文件转换成C++文件,在C++文件中搜索_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; //属性
};

这就是分类的底层结构,相当于,我们写的分类编译完成之后就会变成上面那种结构,有多少分类就有多少上面的结构体。当程序运行的时候就会把上面结构体的东西合并到类对象或者元类对象里面去。

所以分类的功能我们也知道了,分类可以添加属性、协议、对象方法、类方法。

再次进入C++源码,会发现如下代码:

//对象方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_MJPerson_Eat_eat},
    {(struct objc_selector *)"eat1", "v16@0:8", (void *)_I_MJPerson_Eat_eat1}}
};

//类方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"eat2", "v16@0:8", (void *)_C_MJPerson_Eat_eat2},
    {(struct objc_selector *)"eat3", "v16@0:8", (void *)_C_MJPerson_Eat_eat3}}
};

//下面是关于NSCopy协议的一些描述
static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCopying [] __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "@24@0:8^{_NSZone=}16"
};

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};

struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
    0,
    "NSCopying",
    0,
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
    0,
    0,
    0,
    0,
    sizeof(_protocol_t),
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;

//下面是关于NSCoding协议的一些描述
static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCoding [] __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "v24@0:8@\"NSCoder\"16",
    "@24@0:8@\"NSCoder\"16"
};

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"encodeWithCoder:", "v24@0:8@16", 0},
    {(struct objc_selector *)"initWithCoder:", "@24@0:8@16", 0}}
};

struct _protocol_t _OBJC_PROTOCOL_NSCoding __attribute__ ((used)) = {
    0,
    "NSCoding",
    0,
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding,
    0,
    0,
    0,
    0,
    sizeof(_protocol_t),
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCoding
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCoding = &_OBJC_PROTOCOL_NSCoding;

//协议列表
static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    2,
    &_OBJC_PROTOCOL_NSCopying,
    &_OBJC_PROTOCOL_NSCoding
};

//属性和列表
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    2,
    {{"weight","Ti,N"},
    {"height","Td,N"}}
};

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

//把上面的列表传入这个结构体中
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MJPerson",
    0, // &OBJC_CLASS_$_MJPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Eat,
};

观察代码的最后那个结构体,可以发现,上面的代码是给分类的_category_t结构体赋值,分别给名称赋值MJPerson、cls赋值0、对象方法列表赋值、类方法列表赋值、协议赋值、 属性赋值。

下面我们在objc4源码里面查看一下category_t结构体的定义,打开源码,搜索“category_t {”找到如下代码:

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

可以发现和C++里面的定义大同小异。

总结:

编译完成之后,分类里的所有东西都在category_t结构体中,暂时和类是分开的。

二. 运行的时候

下面我们就通过分析objc4源码证明,最后运行的时候,Runtime就会把这个类所有的对象方法合并到类对象的方法列表里面,把类方法合并到元类对象的方法列表里面去。

源码解读顺序:
objc-os.mm文件

_objc_init (运行时的入口,运行时的初始化)
map_images
map_images_nolock

objc-runtime-new.mm文件

_read_images (加载一些模块、镜像,参数传入所有的类信息)
remethodizeClass (核心方法,将类对象和元类对象重新组织下)
attachCategories (核心方法,参数传入类对象(或者元类对象)和分类)
attachLists
realloc、memmove、 memcpy

由于源码阅读比较复杂,可按照上面的顺序来阅读,这里只贴上核心的代码。

attachCategories方法

//参数传入类对象或者元类对象 分类列表(里面有好几个分类)
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;
    while (i--) {  //i的值不断减小,先拿出的是最后面那个分类
        //取出某个分类
        auto& entry = cats->list[I];

        //取出分类的方法列表(根据isMeta决定拿出的是对象方法还是类方法)
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            //并且将取出的方法列表扔到mlists数组里面
            mlists[mcount++] = mlist;  //mcount从0开始,所以最后面的分类放到最前面
            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->或者元类对象(protolists, protocount);
    free(protolists);
}

上面的代码主要做的是将所有的分类的数据拿出来放到数组中,并且是最后面的分类放到最前面,详细解释可一步步看注释,下面进入attachLists方法

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return; //addedCount分类的个数
        
        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;

            //array()->lists原来的方法列表
            //addedCount分类的个数
            //将原来的方法列表往后移addedCount位置 前面的就空出来了
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));

            //addedLists所有分类的方法列表 
            //将所有分类的方法列表放到前面
            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]));
        }
  }
解释:

首先,扩容,然后将原来的方法列表挪到后面去,再将所有分类列表放到原来列表的位置。最后结果是分类在前面,原来的类在后面,所以,同样的方法会优先调用分类。如果不同分类中有相同的方法,由于后编译的分类放到了前面,所以后编译的分类会优先调用。

可在下图中查看和改变编译顺序:

编译顺序.png

总结:

① 运行的时候,通过Runtime加载某个类的所有Category数据
② 把所有Category的属性、协议、方法数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
③ 将合并后的分类数据(属性、协议、方法),插入到类原来数据的前面

补充:

memmove内存挪动和memcpy内存拷贝的区别:

分类添加示意图.png

如图,如果内存中的方法列表顺序是 3 4 1 ,如果想把3和4放到最后面两位(3 3 4),如果使用memcpy,会先变成 3 3 1,再变成 3 3 3。
如果使用memmove就直接一步到位将最后两位变成3 4,结果为 3 3 4。

所以,对于原来的类,肯定要一字不差的挪动过来,所以使用了memmove,而对于分类里的数据,就一个一个拷贝过来就好了,没必要用move。

面试题:

  1. Category的使用场合是什么?
    主要是给一些类添加新的方法,或者拆分类。

  2. Category的实现原理是什么?
    Category编译之后的底层结构是struct category_t,里面存储着分类的属性、协议、对象方法、类方法。
    在程序运行的时候,Runtime会将Category的数据,合并到类信息中(类对象、元类对象中)。

  3. Category和类扩展有什么区别呢?
    类扩展就是在类的.m里面添加一些属性,成员变量,方法声明,如下:

// class extension (类扩展)
@interface MJPerson()
{
    int _abc;
}
@property (nonatomic, assign) int age;

- (void)abc;
@end

其实类扩展就是相当于将原来写在.h文件里面的东西剪掉放到.m里面,把原来公开的属性,成员变量,方法声明私有化。所以,类扩展里面的东西在编译的时候就已经存在类对象里面了,这点和分类不同。

Demo地址:分类的本质

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