Category分类

一、Demo展示

创建一个Person类,在创建一个Person+eatPerson+test两个分类。

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

@implementation Person (test)
- (void)test {
    NSLog(@"test");
}
@end

@implementation Person (eat)
- (void)eat {
    NSLog(@"eat");
}
@end

// 运行下面代码
Person *person = [[Person alloc] init];

[person run];
[person test];
[person eat];
        

当然上面代码,会打印出”run“/"test"/"eat"
我们知道当我们调用一个方法是,底层会调用objc_msgSend(person, @selector(xxx))这个方法,根据OC对象的本质得知,具体的实现是person 的isa 找到类对象里面的实例方法,如果是类方法,则会去元类对象找类方法。

思考如上 Demo里面的两个分类会生成两个新的类吗?
不会,一个isa只会有一个类对象,程序会通过runtime动态将实例方法合并到类对象里面的对象方法中,类方法都会合并到元类对象的类方法

二、Category 内部实现

(一)demo1

使用clang编译器把OC代码转成C++,在终端上cd到当前项目的目录,输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+eat.m会自动生成 .cpp 文件

打开上面生成的Person+eat.cpp文件,我们会看到下面代码

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; // 属性属性列表
};

// 生成的 Category
static struct _category_t _OBJC_$_CATEGORY_Person_$_eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person", // 给了我们上面的name
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_eat, // 就是我们的实例方法 instance_methods
    0,
    0,
    0,
};

// _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_eat
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_$_eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_eat_eat}}
};

(二)demo2

Person+test.h类中添加

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

Person+test.m类中添加

+ (void)test {
    NSLog(@"+test");
}
- (void)test1
{
    NSLog(@"eat1");
}

+ (void)test2
{
    
}

+ (void)test3
{
    
}

然后同样运行上面 clang 代码xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+test.m 生成.cpp文件

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,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_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[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_test_test},
    {(struct objc_selector *)"test1", "v16@0:8", (void *)_I_Person_test_test1}}
};
// 类方法 _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[3];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    3,
    {{(struct objc_selector *)"test", "v16@0:8", (void *)_C_Person_test_test},
    {(struct objc_selector *)"test2", "v16@0:8", (void *)_C_Person_test_test2},
    {(struct objc_selector *)"test3", "v16@0:8", (void *)_C_Person_test_test3}}
};
// 属性列表 _OBJC_$_PROP_LIST_Person_$_test
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_Person_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    2,
    {{"weight","Ti,N"},
    {"height","Td,N"}}
};

也就是说,我们写的分类都会变成_category_t这种结构体,在合适的时机合并到类的对象方法或元类的类方法中。

(三)Category 的加载处理过程

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

2.把所有 Category 的方法、属性、协议数据、合并到一个大数组中

  • 后面参与编译的 Category 数据,会在数组的前面

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

所以,我们在开发过程中,如果分类和类中的方法名字相同,会调用分类里面的。

三、load函数在Category 中的加载

(一)demo

// Person
@implementation Person
+ (void)run {
    NSLog(@"Person +run");
}
+ (void)load {
    NSLog(@"Person +load");
}
@end

// Person+test
@implementation Person (test)

+ (void)load {
    NSLog(@"Person (test) +load");
}
+ (void)test {
    NSLog(@"Person (test) +test");
}
@end

// Person+eat
@implementation Person (eat)
+ (void)load {
    NSLog(@"Person (eat) +load");
}
+ (void)eat {
    NSLog(@"Person (eat) +eat");
}
@end

// 调用 Person 的 +run方法
[Person run];

我们运行程序发现

2020-06-11 23:18:48.786224+0800 TestDemo[18522:2035515] Person +load
2020-06-11 23:18:48.786723+0800 TestDemo[18522:2035515] Person (test) +load
2020-06-11 23:18:48.786785+0800 TestDemo[18522:2035515] Person (eat) +load
2020-06-11 23:18:48.786926+0800 TestDemo[18522:2035515] Person (eat) +run

思考在上面的 Category 内部实现证明了, 如果该分类的方法和该类的方法名一样,会优先调用分类的方法,类里面的方法不会被调用。为什么 load 里面的方法都会被调用呢,而不是像 run 方法一样?

  • load 方法的调用,是因为在程序加载过程中,如果发现是分类,会直接指向分类的类方法列表,而不是去调用的组合后的方法列表。所以会调用三次。

  • +load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用

  • test 方法的调用,我们知道方法的调用实际就是调用 objc_megSend([Person class] @selector(test)) 会通过isa找到当前对应的类对象或元类对象,调用里面的方法列表。因为重新组装的方法列表,Person+eat 分类在最前面,所以会调用 Person+eat 这个类中的 run 方法

  • +load方法会在 Runtime 加载类、分类时间调用,并且每个类、分类的 +load 在程序运行过程中只会调用一次。

(二)load 调用顺序

1、先调用的 +load 方法

  • 按照编译先后顺序调用(先编译,先调用)
  • 调用子类的 +load 之前会先调用父类的 +load

2、在调用分类的 +load 方法

  • 按照编译先后顺序调用(先编译,先调用)

扩展

打印出某个类中的所有方法

- (void)printMethodNamesOfClass:(Class )cls {
    
    unsigned int count;
    // 获取方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@, %@", cls, methodNames);
}

问题

1、给一个存在的类添加两个分类,会生成两个新的类吗?

不会,一个isa只会有一个类对象,程序会通过runtime动态将实例方法合并到类对象里面的对象方法中,类方法都会合并到元类对象的类方法中。

2、Category 的使用场合

  • 给一个类添加新的方法,可以为系统的类扩展功能。
  • 分解体积庞大的类文件,可以将一个类按照功能拆解成多个模块,方便代码管理。
  • 创建对私有方法的前向引用:声明私有方法,把Framework的私有方法公开等,直接调用其他类的私有方法时编译器会报错,这时候可以创建一个该类的分类,在分类中声明这些私有方法(不必提供方法实现),接着导入这个分类的头文件就可以正常调用这些私有方法。
  • 向对象添加非正式协议:创建一个 NSObject 或其子类的分类称为 “创建一个非正式协议”。

正式协议是通过 protocol 指定的一系列方法的声明,然后由遵守该协议的类自己去实现这些方法。而非正式协议是通过给 NSObject 或其子类添加一个分类来实现。非正式协议已经渐渐被正式协议取代,正式协议最大的优点就是可以使用泛型约束,而非正式协议不可以。)

3、Category中都可以添加哪些内容

  • 实例方法、类方法、协议、属性(只生成setter和getter方法的声明,不会生成setter和getter方法的实现以及下划线成员变量)。
  • 默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中,但可以通过关联对象来间接实现这种效果。

4、Category的优缺点、特点、注意点

Category 描述
优点 1、使用场合,
2、可以按照需求加载不同的类。
缺点 1、不能直接添加成员变量,可以通过关联对象实现这种效果,
2、分类方法会“覆盖”同名的宿主类方法,如果使用不当会造成问题
特点 1、运行时决议,
2、可以有声明、可以有实现。
3、可以为系统的类添加分类,
运行时决议:Category 编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,这时候分类中的数据还没有合并到类中,而是在程序运行的时候通过Runtime机制将所有分类数据合并到类(类对象、元类对象)中去。(这是分类最大的特点,也是分类和扩展的最大区别,扩展是在编译的时候就将所有数据都合并到类中去了)
注意点 1、分类方法会“覆盖”同名的宿主类方法,如果使用不当会造成问题;
2、同名分类方法谁能生效取决于编译顺序,最后参与编译的分类中的同名方法会最终生效;
3、名字相同的分类会引起编译报错。

5、Category 的实现原理

  • 分类的实现原理取决于运行时决议;
  • 同名分类方法谁能生效取决于编译顺序,最后参与编译的分类中的同名方法会最终生效;
  • 分类方法会“覆盖”同名的宿主类(原类)方法,这里说的“覆盖”并不是指原来的方法没了。消息传递过程中优先查找宿主类中靠前的元素,找到同名方法就进行调用,但实际上宿主类中原有同名方法的实现仍然是存在的。我们可以通过一些手段来调用到宿主类原有同名方法的实现,如可以通过Runtime的class_copyMethodList方法打印类的方法列表,找到宿主类方法的imp,进行调用(可以交换方法实现)。

6、Category的加载处理过程

在编译时,Category 中的数据还没有合并到类中,而是在程序运行的时候通过Runtime机制将所有分类数据合并到类(类对象、元类对象)中去。下面我们来看一下 Category 的加载处理过程。

① 通过Runtime加载某个类的所有 Category 数据;
② 把所有的分类数据(方法、属性、协议),合并到一个大数组中;(后面参与编译的 Category 数据,会在数组的前面)
③ 将合并后的分类数据(方法、属性、协议),插入到宿主类原来数据的前面。(所以会优先调用最后参与编译的分类中的同名方法)

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

推荐阅读更多精彩内容