Category
也叫分类
或类别
,是OC提供的一种扩展类的方式。不论是自定义的类仍是系统的类,咱们均可以经过Category
给原有类扩展方法(实例方法和类方法均可以),并且扩展的方法和原有的方法的调用方式是如出一辙的。好比我项目中常常须要统计一个字符串中字母的个数,可是系统没有提供这个方法,那咱们就能够用Category
给NSString
类扩展一个方法,而后只需引入Category
的头文件就能够和调用系统方法同样来调用扩展的方法。算法
// 给NSString类添加一个Category,并扩展一个实例方法
@interface NSString (QJAdd)
- (NSInteger)letterCount;
@end
复制代码
// 在须要使用这个扩展方法的地方引入头文件 #import "NSString+QJAdd.h",而后就能够调用这个扩展方法了
- (void)test{
NSString *testStr = @"sdfjshdfjk.,d.889";
NSInteger letterCount = [testStr letterCount];
}
复制代码
Category
除了用来给类进行扩展外,还有一种比较高级的用法,就是用来拆分模块
,将一个大的模块拆分红多个小的模块,方便进行维护和管理。什么意思呢?我就举一个不少开发人员都会存在的问题,就是AppDelegate
这个类。这个类是刚建立项目时自动生成的,用来管理程序生命周期的。在刚建立项目时,这个类中是没有多少代码的,可是随着项目的进行,愈来愈多的代码会被放在这个类里面。好比说集成极光推送、友盟、百度地图、微信SDK等各类第三方框架时,这些第三方框架的初始化工做,甚至是相关的业务逻辑代码都会放在这个类里面,这就致使随着APP的功能愈来愈复杂,AppDelegate
中的代码就会愈来愈多,有的甚至有几千行,看着就让人头皮发麻。数组
这时咱们就能够利用Category
来对AppDelegate
进行拆分,首先咱们就须要对AppDelegate
中的代码进行划分,把同一种功能的代码抽取出来放在一个分类里面。好比说我能够新建一个极光推送的分类,而后把全部和极光推送有关的代码都抽出来放入这个分类,把全部和微信相关的代码抽出来放进微信的分类中,后面又有新的功要添加的话我只须要新建分类就行了。维护的时候要改什么功能的代码就直接找相应的分类就行了。bash
// 把全部和极光推送有关的代码都抽出来放入这个分类
#import "AppDelegate.h"
@interface AppDelegate (JPush)
@end
复制代码
在讲解这个问题以前,咱们须要对OC方法调用的底层机制以及类对象的内存存储结构有必定了解,对于这一块不熟悉的能够先去看看个人另一篇博客OC对象的本质。微信
咱们先来思考这样一个问题,当咱们经过Category
给一个类扩展了一个实例方法,咱们调用这个实例方法时,它也是经过实例的isa指针找到类对象,而后在类对象的方法列表中去查找这个方法。那么Category
扩展的方法是如何被添加到类对象的方法列表中去的呢?是编译的时候添加进去的仍是运行的时候添加进去的呢?app
首先咱们来看下Category
的内存存储结构,Category
底层其实就是一个category_t
类型的结构体,咱们能够在objc4
源码的objc-runtime-new.h
文件中看到它的定义:框架
// 定义在objc-runtime-new.h文件中
struct category_t {
const char *name; // 好比给Student添加分类,name就是Student的类名
classref_t cls;
struct method_list_t *instanceMethods; // 分类的实例方法列表
struct method_list_t *classMethods; // 分类的类方法列表
struct protocol_list_t *protocols; // 分类的协议列表
struct property_list_t *instanceProperties; // 分类的实例属性列表
struct property_list_t *_classProperties; // 分类的类属性列表
};
复制代码
从这个结构体能够看出,Category
中存储的不只有方法列表,还有协议列表和属性列表。函数
咱们每建立一个分类,在编译时都会生成这样一个结构体并将分类的方法列表等信息存入这个结构体。在编译阶段分类的相关信息和本类的相关信息是分开的。等到运行阶段,会经过runtime加载某个类的全部Category数据,把全部Category的方法、属性、协议数据分别合并到一个数组中,而后再将分类合并后的数据插入到本类的数据的前面。post
想要了解详细的流程,能够去看源码,因为源码太多,这里就不贴出来了,这里给你们提供一下解读源码时整个流程的函数调用过程:ui
从objc-os.mm
文件的_objc_init
函数开始-->map_images
-->map_images_nolock
-->_read_images
-->remethodizeClass
-->attachCategories
-->attachLists
-->realloc、memmove、 memcpy
。atom
下面我根据个人理解,举个例子描述一下整个流程,我这里只讲解实例方法列表的合并流程,类方法列表、属性列表、协议列表等信息的合并流程都是同样的。
首先咱们声明一个Student
类,而后建立2个分类:Student (aaa)
和Student (bbb)
,原本和分类中都有实现2个方法,以下代码所示:
// Student.m文件
#import "Student.h"
@implementation Student
- (void)study{
NSLog(@"%s",__func__);
}
- (void)studentTest{
NSLog(@"%s",__func__);
}
@end
// Student+aaa.m文件
#import "Student+aaa.h"
@implementation Student (aaa)
- (void)study{
NSLog(@"%s",__func__);
}
- (void)studentAaaTest{
NSLog(@"%s",__func__);
}
@end
// Student+bbb.m文件
#import "Student+bbb.h"
@implementation Student (bbb)
- (void)study{
NSLog(@"%s",__func__);
}
- (void)studentBbbTest{
NSLog(@"%s",__func__);
}
@end
复制代码
aaa->instanceMethods = @[@"study",@"studentAaaTest"]
和bbb->instanceMethods = @[@"study",@"studentBbbTest"]
。而此时Student
类对象的方法列表是存在class_ro_t
结构体的baseMethodList
中。因此在编译阶段各个方法列表都是分开存储的
。Student
类对象会初始化class_rw_t
结构体,这个结构体中也有个方法列表methods
,它是一个二维数组,它初始化后首先将class_ro_t
中的baseMethodList
拷贝过来,此时methods = @[baseMethodList]
。categoryMethodList
)中的顺序是和他们参与编译的顺序有关的,若是先编译aaa,再编译bbb,那么在合并后的数组中,bbb的方法列表在前面,aaa的方法列表在后面,因此此时categoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods]
。categoryMethodList
的数据添加到methods
中。添加以前methods
的容量大小是1,它会先根据categoryMethodList
中方法列表的个数(也就是有几个分类,这里是2个分类)进行扩容,methods
扩容后的大小是3,它先将methods
中原来的数据(baseMethodList
)移到最后,而后再将categoryMethodList
中的数据插入进来,因此最后的结果就是methods = @[bbb->instanceMethods,aaa->instanceMethods,baseMethodList]
。这样就完成了分类方法列表和本类方法列表的合并。因此合并后分类的方法在前面
(最后参与编译的那个分类的方法列表在最前面),本类的方法列表在最后面
。因此当分类中有和本类同名的方法时,调用这个方法执行的就是分类中的方法。从这个现象来看,就好像本类的方法被分类中同名的方法给覆盖了,实际上并无覆盖,只是调用方法时最早查找到了分类的方法因此就执行分类的方法。好比上面的例子,本类和2个分类中都有study
这个方法,若是咱们打印这个类的方法列表就会发现里面有3个叫study
的方法。
咱们先来看一下普通类定义一个属性,好比当咱们给一个Student
类定义一个属性@property (nonatomic , assign) NSInteger score;
时,编译器会自动帮咱们生成一个叫_score
的成员变量,而且会自动实现这个属性的setter/getter方法:
@implementation Student
{
NSInteger _score;
}
- (void)setScore:(NSInteger)score{
_score = score;
}
- (NSInteger)score{
return _score;
}
@end
复制代码
那咱们可不能够用Category
以一样的方式来给类扩展属性和成员变量呢?咱们从Category
的底层结构体category_t
能够看出,这个结构体中有方法列表、协议列表和属性列表,可是没有成员变量列表,因此咱们能够在Category
中定义属性,可是不能定义成员变量,定义成员变量的话编译器会直接报错。
若是咱们在Student
的分类中定义一个属性@property (nonatomic , strong) NSString *name;
,那编译器会为咱们作什么呢?编译器只会帮咱们声明- (void)setName:(NSString *)name;
和- (NSString *)name;
这两个方法,而不会实现这两个方法,也不会定义成员变量。因此此时若是咱们在外面给一个实例对象设置name属性值student.name = @"Jack"
,编译器并不会报错,由于setter方法是有声明的,可是一旦程序运行,就会抛出unrecognized selector
的异常,由于setter方法没有实现。
那要如何作才能让咱们正常的使用name
这个属性呢?咱们能够手动去实现setter/getter方法,实现这两个方法时关键就在于如何将属性值给保存起来,在普通的类中咱们是定义一个成员变量_name
来保存这个属性值,可是在分类中咱们没法定义成员变量,因此须要想其余办法来保存。咱们能够经过如下几种方式来实现这个需求。
什么叫以本类中已有的属性来进行存储呢?咱们直接举一个例子,好比我要给UIView
扩展x
和y
这两个属性,那么咱们能够添加一个分类来实现:
// .h文件
@interface UIView (Add)
@property (nonatomic , assign) CGFloat x;
@property (nonatomic , assign) CGFloat y;
@end
// .m文件
#import "UIView+Add.h"
@implementation UIView (Add)
- (void)setX:(CGFloat)x{
CGRect origionRect = self.frame;
CGRect newRect = CGRectMake(x, origionRect.origin.y, origionRect.size.width, origionRect.size.height);
self.frame = newRect;
}
- (CGFloat)x{
return self.frame.origin.x;
}
- (void)setY:(CGFloat)y{
CGRect origionRect = self.frame;
CGRect newRect = CGRectMake(origionRect.origin.x, y, origionRect.size.width, origionRect.size.height);
self.frame = newRect;
}
- (CGFloat)y{
return self.frame.origin.y;
}
@end
复制代码
这种方式就是经过UIView
原有的属性frame
来对新添加的属性x
、y
的值进行存取操做,很显然这种方式有很大的局限性,只有在上面这种特殊状况下才能使用。
好比给Student
添加了一个分类Student (add)
,分类中定义了2个属性name
和age
,那咱们就能够在分类的.m
文件中定义2个全局字典nameDic
和ageDic
,nameDic
用来存储全部实例对象的name
属性值,其中以实例对象的指针做为key,name
属性值做为value。ageDic
用来存储全部实例对象的age
属性值。代码以下所示:
#import "Student+add.h"
// 以实例对象的指针做为key
#define QJKey [NSString stringWithFormat:@"%p",self]
@implementation Student (add)
// 定义2个全局字典用来存储2个新增的属性的值
NSMutableDictionary *nameDic;
NSMutableDictionary *ageDic;
+ (void)load{
nameDic = [NSMutableDictionary dictionary];
ageDic = [NSMutableDictionary dictionary];
}
//
- (void)setName:(NSString *)name{
nameDic[QJKey] = name;
}
- (NSString *)name{
return nameDic[QJKey];
}
- (void)setAge:(NSInteger)age{
ageDic[QJKey] = @(age);
}
- (NSInteger)age{
return [ageDic[QJKey] integerValue];
}
@end
复制代码
这种方法虽然能够实现咱们的需求,可是有个问题,每当咱们实例化一个对象时都会往那两个全局字典中添加一个元素,而实例化的对象销毁时,全局字典中与之对应的元素又没有被移除,这样就会致使这两个字典占用内存会愈来愈大,有内存溢出的风险。
关联对象是runtime
提供的一组API,因此须要引入头文件#import <objc/runtime.h>
。
添加关联对象API:
void objc_setAssociatedObject(id object,
const void * key,
id value,
objc_AssociationPolicy policy);
复制代码
好比Student的分类中新增了一个name属性,我要给一个实例对象的stu的name属性赋值为Jack:
object
):关联的对象,也就是上面的stu
key
):这里传入一个void *
类型的指针做为key,这个key是本身随便设置的,后面获取关联对象也是根据这个key来获取的。后面我会列出几种经常使用的设置key的方式。value
):要设置的属性值,也就是上面的Jackpolicy
):要设置的属性的修饰类型,好比name的修饰类型是strong, nonatomic
,那这里对应的的policy就是OBJC_ASSOCIATION_RETAIN_NONATOMIC
。具体对应关系以下(注意是没有和weak对应的policy的):objc_AssociationPolicy | 对应的修饰符 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
OBJC_ASSOCIATION_RETAIN | strong, atomic |
OBJC_ASSOCIATION_COPY | copy, atomic |
获取关联对象:
id objc_getAssociatedObject(id object, const void * key);
复制代码
移除全部的关联对象:
void objc_removeAssociatedObjects(id object);
复制代码
设置key的经常使用方式: key是一个void *
类型的指针,原则上来讲随便设置一个指针均可以,只要保证设置关联对象时和获取关联对象时的key同样就好了。可是为了提升代码的可读性,咱们能够采用如下几种方式来设置key:
好比给stu
实例对象的name
属性赋值Jack
。
方式一:
对每个属性都声明一个静态全局的void *
类型的变量,这个变量中存储的值是它本身的地址,这样就能够保证这个变量值的惟一性。
// 声明全局静态变量
static void *_name = &_name;
// 设置关联对象
objc_setAssociatedObject(stu, _name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 获取关联对象
NSString *temName = objc_getAssociatedObject(stu, _name);
复制代码
方式二: 这种方式和上面那种方式差很少,只不过声明全局变量后不赋值,直接将变量的地址值设置为key:
static void *_name;
objc_setAssociatedObject(stu, &_name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, &_name);
复制代码
方式三:
用属性名字符串的地址做为key,注意key = @"name"
这种是将字符串的地址赋值给key,而不是将字符串自己赋值给key。在iOS中,不管定义多少个指针变量指向@"name"
这个字符串,这些指针指向的其实都是同一个内存空间,因此能够保证key的惟一性。
objc_setAssociatedObject(stu, @"name", @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, @"name");
复制代码
方式四:
用属性的getter方法个的@selector
做为key,由于一个类中同一个方法名对应的SEL
的地址始终是同一个。比较推荐使用这种方式,由于在写代码的时候会有提示。
objc_setAssociatedObject(stu, @selector(name), @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, @selector(name));
复制代码
// .h文件
#import "Student.h"
@interface Student (Add)
@property (nonatomic , strong) NSString *name;
@property (nonatomic , assign) NSInteger age;
@end
// .m文件
#import "Student+Add.h"
#import <objc/runtime.h>
@implementation Student (Add)
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
return objc_getAssociatedObject(self, @selector(name));
}
- (void)setAge:(NSInteger)age{
objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
}
- (NSInteger)age{
return [objc_getAssociatedObject(self, @selector(age)) integerValue];
}
@end
复制代码
有人可能会有疑问,关联对象底层是怎么实现的呢,它是否是经过属性生成了成员变量,而后合并到了类对象的成员属性列表中去呢?其实不是的,关联对象是另外单独存储的,底层实现关联对象技术的核心对象有4个:
ObjcAssociation
:这个对象里面有2个成员uintptr_t _policy
和id _value
,这两个很显然就是咱们设置关联对象传入的参数policy
和value
。ObjectAssociationMap
:这是一个HashMap
(以键值对方式存储,能够理解为是一个字典),以设置关联对象时传入的key
值做为HashMap
的键
,以ObjcAssociation
对象做为HashMap
的值
。好比一个分类添加了3个属性,那一个实例对象给这3个属性都赋值了,那么这个HashMap
中就有3个元素,若是给这个实例对象的其中一个属性赋值为nil,那这个HashMap
就会把这个属性对应的键值对给移除,而后HashMap
中就还剩2个元素。AssociationsHashMap
:这也是一个HashMap
,以设置关联属性时传入的参数object
做为键
(实际是对object对象经过某个算法计算出一个值做为键
)。以ObjectAssociationMap
做为值
。因此当某个类(前提是这个类的分类中有设置关联对象)每实例化一个对象,这个HashMap
就会新增一个元素,当某个实例化对象被释放时,其对应的键值对也会被这个HashMap
给移除。注意整个程序运行期间,AssociationsHashMap
只会有一个,也就是说全部的类的关联对象信息都是存储在这个HashMap
中。AssociationsManager
:从名字就能够看出它是一个管理者,注意整个程序运行期间它也只有一个,他就只包含一个AssociationsHashMap
。这4个对象的关系图以下图所示: