1 、Category的简单应用
Category的使用非常频繁,他可以动态的为已经存在的类添加新的行为。当你不想对原类文件进行修改的时候,你就可以通过添加Category去定制自己需要的方法。这样,不需要访问其源代码、也不需要创建子类,Category使用简单的方式,实现了类的相关方法的模块化,把不同的类方法分配到不同的分类文件中,这样可以保证类的原原来的基础上,较小的改动就可以增加需要的功能。比如扩展系统提供的方法,扩展CocoaPod管理的方法时,我们会经常用到Category。
上example(我习惯将原理写在代码注释中,所以认真看哟,当然最后我也会做总结):
创建MJPerson类和两个分类MJPerson+Test、MJPerson+Eat:
//MJPerson.h和MJPerson.m文件
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@property(nonatomic,strong)NSString *personName;
@property(nonatomic,assign)int personAge;
-(void)run;
@end
===============================================
#import "MJPerson.h"
@implementation MJPerson
-(void)run{
NSLog(@"I can run");
}
@end
//MJPerson+Test.h和MJPerson+Test.m文件
#import "MJPerson.h"
@interface MJPerson (Test)
-(void)test;
@end
===============================================
#import "MJPerson+Test.h"
@implementation MJPerson (Test)
-(void)test{
NSLog(@"I can test");
}
@end
//MJPerson+Eat.h和MJPerson+Eat.m文件
#import "MJPerson.h"
@interface MJPerson (Eat)<NSCopying,NSCopying>
@property(nonatomic,strong)NSString *name;
@property(nonatomic,assign)int age;
-(void)eat;
-(void)eat1;
+(void)eat2;
+(void)eat3;
@end
===============================================
#import "MJPerson+Eat.h"
@implementation MJPerson (Eat)
- (void)setName:(NSString *)name{
self.personName = name;
}
- (void)setAge:(int)age{
self.personAge = age;
}
-(NSString *)name{
return self.personName;
}
-(int)age{
return self.personAge;
}
-(void)eat{
NSLog(@"I can eat");
}
-(void)eat1{
NSLog(@"实例方法eat1");
}
+(void)eat2{
NSLog(@"类方法eat2");
}
+(void)eat3{
NSLog(@"类方法eat3");
}
@end
//ViewController.m文件
#import "ViewController.h"
#import "MJPerson.h"
#import "MJPerson+Test.h"
#import "MJPerson+Eat.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
MJPerson *person = [[MJPerson alloc] init];
// 这种方法调用的方式都是通过 objc_msgSend(对象名,@selector(方法名)) 来实现的
[person run];
[person test];
[person eat];
person.age = 10;
person.name = @"ychen3022";
NSLog(@"刚刚赋值的age: %d",person.age);
NSLog(@"刚刚赋值的name: %@",person.name);
// 分类的实现原理
// 给person发送一个消息:通过objc_msgSend(person,@selector(eat))
// 发消息的这种机制又是怎么实现的呢?去哪里找到对应的方法呢?
// -(void)eat是个实例方法,存储在class(类对象)里面的,所以是给实例对象发送消息,通过isa指针找到这个class(类对象),在class(类对象)的方法里面找到实例方法的实现(IMP),然后进行调用
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
=========================================================
打印结果:
CategoryTest[2737:706571] I can run
CategoryTest[2737:706571] I can test
CategoryTest[2737:706571] I can eat
CategoryTest[2737:706571] 刚刚赋值的age: 10
CategoryTest[2737:706571] 刚刚赋值的name: ychen3022
2 、探究Category本质
使用clang编译命令行,把MJPerson+Eat.m文件转成.cpp文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Eat.m
从文件中,我们可以找到有个_category_t的结构体,如下:
struct _category_t {
const char *name; //类名,并不是category小括号里写的名字,而是类的名字
struct _class_t *cls; //cls要扩展的类对象,编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象
const struct _method_list_t *instance_methods; //存放这个category所有的对象方法列表
const struct _method_list_t *class_methods; //存放这个category所有的类方法列表
const struct _protocol_list_t *protocols; //存放这个category所有的协议列表
const struct _prop_list_t *properties; //存放这个category所有的属性列表
};
同时,我们也可以找到MJPerson+Eat这个分类的数据内容
(可以继续找到对应的详细内容,我就不贴代码了)
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被存储在这个结构体里的,那一直是存储在这里吗?调用的时候也是从这个结构体调用?带着疑问往下看:
从苹果源码网站下载下来runtime的objc4-723源码,分析得出:
在运行时初始化的时候,会加载项目中所有的类,并将Category中的内容附加到类对象中。
所以当含有同样的方法名时,会优先调用分类中的方法(因为后添加进来先调用)。同是分类中的同名方法,谁后编译就调用谁的。
obje-runtime-new.mm文件中的部分代码如下:
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
// 将方法列表、属性、协议从分类文件中附加到class(类对象)中
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;
// 通过循环,将某个类的所有分类文件中的方法列表、属性列表、协议列表都放到了mlists、proplists、protolists
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);
//将所有分类的对象方法附加到class(类对象)的方法列表中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
//将所有分类的属性附加到class(类对象)的属性列表中
rw->properties.attachLists(proplists, propcount);
free(proplists);
//将所有分类的协议附加到class(类对象)的协议列表中
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
通过上面的分析,我们可以知道:
在编译阶段,Category的底层结构是struct category_t,里面存储了该分类文件中的数据(方法、属性、协议等)。
在程序运行时候,通过runtime加载某个类的所有Category数据,把该类对应的所有Category数据合并到一个大数组中。
最后,将合并后的Category数据插入到该类原来数据的前面。
3、总结
<1>Category的实现原理
Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
<2>Category和Class Extension的区别是什么?
-
扩展是在编译的时候,它的数据就已经包含在类信息中,
分类是在运行时,才会将数据合并到类信息中 -
分类中原则上只能增加方法(能添加属性的的原因只是通过runtime解决无setter/getter的问题而已)
-
扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型(使用范围只能在自身类,而不是子类或其他地方)
-
扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中
-
扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,扩展所声明的方法必须依托对应类的实现部分来实现
-
定义在 .m 文件中的扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。扩展是在 .m 文件中声明私有方法的非常好的方式
<3>Category中能定义属性吗?
是可以的,但是分类可以用@property来添加属性,此种方式会自动生成对应属性的set和get方法的声明,但是没有set和get方法的实现,也不会自动生成带有“_”的属性。
如上example所示,我们必须在MJPerson+Eat.m中对属性age、name的get、set方法进行重写,而且是对其原来的类MJPerson的属性personAge和personName进行赋值的,否则会报错。
网友评论