Category(分类)

基本用法:

Person文件:
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)run;
+ (void)run2;
@end

#import "Person.h"
@implementation Person
- (void)run{
    NSLog(@"run");
}
+ (void)run2{
    NSLog(@"run2");
}
@end
Person+Test文件(Test分类)
#import "Person.h"
@interface Person (Test)
- (void)test;
+ (void)test2;
- (void)run;
@end

#import "Person+Test.h"
@implementation Person (Test)
- (void)test{
    NSLog(@"test");
}
+ (void)test2{
    NSLog(@"test2");
}
- (void)run{
    NSLog(@"test - run");
}
@end
Person+Eat文件(Eat分类)
#import "Person.h"
@interface Person (Eat)
- (void)eat;
+ (void)eat2;
- (void)run;
@end

#import "Person+Eat.h"
@implementation Person (Eat)
- (void)eat{
    NSLog(@"eat");
}
+ (void)eat2{
    NSLog(@"eat2");
}
- (void)run{
    NSLog(@"eat - run");
}
@end
Person *person = [[Person alloc] init];
[person run];

[person test];
[person eat];

[Person run2];
[Person test2];
[Person eat2];

通过runtime动态将分类的方法合并到类对象、元类对象中(程序运行过程中合并,而不是编译时)
程序编译时,所有的分类都变成了结构体,方法数据结构都存在结构体中

调用的run方法是Eat分类还是Test分类中的run方法,取决于最后编译的文件是哪个,最后编译的文件的方法列表会最先调用,如何判断先编译哪个文件,查看如下顺序。


Category的加载处理过程
1、通过Runtime加载某个类的所有Category数据
2、把所有Category的方法、属性、协议数据,合并到一个大数据中,后面参与编译的Category数据,会在数组的前面
3、将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

类扩展:
相当于.h文件里的属性或者方法,挪到.m文件中。公有化变私有化,编译时就在类对象里边了。和分类是不一样的。

#import "Person.h"
//class extension (匿名分类)也可以叫类扩展
@interface Person ()
{
    int _abc;
}
@property (nonatomic,assign) int age;
- (void)abc;
@end
@implementation Person
- (void)abc{
    NSLog(@"类扩展 - abc");
}
- (void)run{
    NSLog(@"run");
}
@end

探究Category中的load、initialize方法

+load方法
  • +load方法会在runtime加载类、分类时调用
  • 每个类、分类的+load,在程序运行过程中只调用一次
  • 调用顺序:
    1.先调用类的+load:按照编译先后顺序调用(先编译,先调用);调用子类的+load之前会先调用父类的+load
    2.再调用分类的+load:按照编译先后顺序调用(先编译,先调用)

新建继承自NSObject的Person类、Person的分类Test1、和Person的分类Test2、继承自Person类的Student类、Student类的分类Test1、Student的分类Test2.

Person文件
#import <Foundation/Foundation.h>
@interface Person : NSObject

@end

#import "Person.h"
@implementation Person
+ (void)load{
    NSLog(@"Person + load");
}

+ (void)test{
    NSLog(@"Person + test");
}
@end
Person+Test1文件
#import "Person.h"
@interface Person (Test1)

@end

#import "Person+Test1.h"
@implementation Person (Test1)
+ (void)load{
    NSLog(@"Person(Test1) + load");
}

+ (void)test{
    NSLog(@"Person(Test1) + test");
}
@end
Person+Test2文件
#import "Person.h"
@interface Person (Test2)

@end

#import "Person+Test2.h"
@implementation Person (Test2)
+ (void)load{
    NSLog(@"Person(Test2) + load");
}

+ (void)test{
    NSLog(@"Person(Test2) + test");
}
@end
Student文件
#import "Person.h"
@interface Student : Person

@end

#import "Student.h"
@implementation Student
+ (void)load{
    NSLog(@"Student + load");
}
@end
Student+Test1文件
#import "Student.h"
@interface Student (Test1)

@end

#import "Student+Test1.h"
@implementation Student (Test1)
+ (void)load{
    NSLog(@"Student (Test1) + load");
}
@end
Student+Test2文件
#import "Student.h"
@interface Student (Test2)

@end

#import "Student+Test2.h"
@implementation Student (Test2)
+ (void)load{
    NSLog(@"Student (Test2) + load");
}
@end
运行之后lldb打印结果
2023-03-23 15:56:32.764696+0800 interview[45205:125689529] Person + load
2023-03-23 15:56:32.765497+0800 interview[45205:125689529] Student + load
2023-03-23 15:56:32.765526+0800 interview[45205:125689529] Person(Test1) + load
2023-03-23 15:56:32.765547+0800 interview[45205:125689529] Person(Test2) + load
2023-03-23 15:56:32.765565+0800 interview[45205:125689529] Student (Test1) + load
2023-03-23 15:56:32.765584+0800 interview[45205:125689529] Student (Test2) + load
+ initialize方法
Person文件
#import <Foundation/Foundation.h>
@interface Person : NSObject

@end

#import "Person.h"
@implementation Person
+ (void)initialize{
    NSLog(@"Person + initialize");
}

+ (void)test{
    NSLog(@"Person + test");
}
@end
Person+Test1文件
#import "Person.h"
@interface Person (Test1)

@end

#import "Person+Test1.h"
@implementation Person (Test1)
+ (void)initialize{
    NSLog(@"Person(Test1) + initialize");
}

+ (void)test{
    NSLog(@"Person(Test1) + test");
}
@end
Person+Test2文件
#import "Person.h"
@interface Person (Test2)

@end

#import "Person+Test2.h"
@implementation Person (Test2)
+ (void)initialize{
    NSLog(@"Person(Test2) + initialize");
}

+ (void)test{
    NSLog(@"Person(Test2) + test");
}
@end
Student文件
#import "Person.h"
@interface Student : Person

@end

#import "Student.h"
@implementation Student
+ (void)initialize{
    NSLog(@"Student + initialize");
}
@end
Student+Test1文件
#import "Student.h"
@interface Student (Test1)

@end

#import "Student+Test1.h"
@implementation Student (Test1)
+ (void)initialize{
    NSLog(@"Student (Test1) + initialize");
}
@end
Student+Test2文件
#import "Student.h"
@interface Student (Test2)

@end

#import "Student+Test2.h"
@implementation Student (Test2)
+ (void)initialize{
    NSLog(@"Student (Test2) + initialize");
}
@end
Dog文件
#import <Foundation/Foundation.h>
@interface Dog : NSObject

@end

#import "Dog.h"
@implementation Dog
+ (void)initialize{
    NSLog(@"Dog + initialize");
}
@end
Cat文件
#import <Foundation/Foundation.h>
@interface Cat : NSObject

@end

#import "Cat.h"
@implementation Cat
+ (void)initialize{
    NSLog(@"Cat + initialize");
}
@end
Teacher文件,(继承自Person,但是.m文件中没有实现initialize方法)
#import "Person.h"
@interface Teacher : Person

@end

#import "Teacher.h"

@implementation Teacher

@end
main.m文件
[Person alloc];
lldb打印结果:
2023-03-23 17:00:02.419047+0800 interview[46841:125744184] Person(Test2) + initialize
[Student alloc];
lldb打印结果:
2023-03-23 18:54:33.026981+0800 interview[47435:125764517] Person(Test2) + initialize
2023-03-23 18:54:33.027458+0800 interview[47435:125764517] Student (Test2) + initialize

[Person alloc];
[Student alloc];
lldb打印结果:
2023-03-23 18:56:17.971696+0800 interview[47484:125766784] Person(Test2) + initialize
2023-03-23 18:56:17.972027+0800 interview[47484:125766784] Student (Test2) + initialize

[Teacher alloc];
lldb打印结果:
2023-03-23 19:30:35.313728+0800 interview[48235:125793488] Person(Test2) + initialize
2023-03-23 19:30:35.314508+0800 interview[48235:125793488] Person(Test2) + initialize
  • initialize方法会在类第一次接收到消息时调用
  • 调用顺序:
    1.先调用父类的+initialize,再调用子类的+initialize
    用Person和Student类举例:(伪代码)
if(Student 没有初始化){
    if(Person没有初始化){
          objc_msgSend([Person class],@selector(initialize));
     }
    objc_msgSend([Student class],@selector(initialize));
}

2.先初始化父类,再初始化子类,每个类只会初始化一次

  • +initialize和+load的很大区别是,+ initialize是通过objc_msgSend进行调用的,所以有以下特点
    1.如果子类没有实现+ initialize,会调用父类的+ initialize(所以父类的 + initialize可能会被调用多次)
    2.如果分类实现了+ initialize,就覆盖类本身的+ initialize调用
关联对象 - 分类添加属性

分类中直接写@property属性的话,只能实现set、get方法的声明,不能声明成员变量和set、get方法的实现。
那么首先想到第一种办法,在分类的.m文件中声明一个全局变量,保存传进来的数据,代码如下:

Person.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,assign) int age;
@end

Person.m文件
#import "Person.h"
@implementation Person

@end
Person+Test.h文件
#import "Person.h"
@interface Person (Test)
@property (nonatomic,assign) int weight;
@end
Person+Test.m文件
#import "Person+Test.h"
@implementation Person (Test)

int weight_;

- (void)setWeight:(int)weight{
    weight_ = weight;
}
- (int)weight{
    return weight_;
}
@end
main文件
Person *person1 = [[Person alloc] init];
person1.age = 10;
person1.weight = 20;
    
Person *person2 = [[Person alloc] init];
person2.age = 100;
person2.weight = 200;
    
NSLog(@"age1 is %d,weight1 is %d",person1.age,person1.weight);
NSLog(@"age2 is %d,weight2 is %d",person2.age,person2.weight);
打印结果:
age1 is 10,weight1 is 200
age2 is 100,weight2 is 200

但是这种方法存在一种弊端,就是实例化多个person对象,会共用一个weight全局变量。所以不能保证每一个person有一个自己对应的weight。

那么这种方案不行,继续改进,用一个NSMutableDictionary字典去实现每一个person对象有一个自己对应的weight,分类.m文件代码修改如下

Person+Test.m文件
#import "Person+Test.h"

@implementation Person (Test)

NSMutableDictionary *weights_;

+ (void)load{
    weights_ = [NSMutableDictionary dictionary];
}
- (void)setWeight:(int)weight{
    NSString *key = [NSString stringWithFormat:@"%p",self];
    weights_[key] = @(weight);
}

- (int)weight{
    NSString *key = [NSString stringWithFormat:@"%p",self];
    return [weights_[key] intValue];
}
@end
main文件
Person *person1 = [[Person alloc] init];
person1.age = 10;
person1.weight = 20;
    
Person *person2 = [[Person alloc] init];
person2.age = 100;
person2.weight = 200;
    
NSLog(@"age1 is %d,weight1 is %d",person1.age,person1.weight);
NSLog(@"age2 is %d,weight2 is %d",person2.age,person2.weight);
打印结果:
age1 is 10,weight1 is 20
age2 is 100,weight2 is 200

那么这种方案也存在一定的弊端:比如内存泄漏、线程安全等问题,还有一个问题就是,添加多个属性的话,会重复写多个字典、set、get方法,比较麻烦。

那么第三种方案就是采用关联对象的方案:
关联对象提供了以下API:

  • 添加关联对象:
    void objc_setAssociatedObject(id object,const void * key, id value, objc_AssociationPolicy policy)
  • 获得关联对象
    id objc_getAssociatedObject(id object, const void * key)
  • 移除所有的关联对象
    void objc_removeAssociatedObjects(id object)

    objc_AssociationPolicy:
Person+Test.h文件
#import "Person.h"
@interface Person (Test)
@property (nonatomic,copy) NSString *name;
@end
Person+Test.m文件
#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)

const void *NameKey = &NameKey;

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, NameKey);
}
@end

上边的方案有一个问题,就是全局变量const void *NameKey = &NameKey;在外部其他的文件中是可以访问的, extern const void *NameKey;因为上边的代码逻辑是想要文件内部访问的,并不想文件外部也能访问的到,所以需要改进一下,把const void *NameKey = &NameKey;前边加一个static修饰词,外部就无法访问,代码如下:

Person+Test.m文件
#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)

static const void *NameKey = &NameKey;

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, NameKey);
}
@end

上边的代码,关联对象的key还可以进一步进行优化,代码如下:

Person+Test.m文件
#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)

static const char NameKey;

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, &NameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, &NameKey);
}
@end

还有一种方案,key直接写成固定的字符串:

Person+Test.m文件
#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)
- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, @"name");
}
@end

最后一种方案,key的写法:

#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, @selector(name));
}
@end

或者写成

#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, _cmd);
}//_cmd == @selector(当前方法名)
@end
关联对象的原理
  • 实现关联对象技术的核心对象有:
    AssociationsManager
    AssociationsHashMap
    ObjectAssociationMap
    ObjcAssociation
    AssociationsManager 对象里边有一个AssociationsHashMap,AssociationsHashMap里边放字典,ObjectAssociationMap作为AssociationsHashMap里的value值,ObjectAssociationMap里边也是放字典,ObjcAssociation是作为ObjectAssociationMap里边字典的value。ObjcAssociation里边放了两个冬秀,一个policy,一个value

    objc4源码解读:objc-references.mm

关联对象的原理

所以关联对象里边的值,并不是放在了原来的类的实例对象里边,而是单独维护了一份Manager等四个类。

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

推荐阅读更多精彩内容