1 、Category的简单应用
Category的使用非常频繁,他可以动态的为已经存在的类添加新的行为。当你不想对原类文件进行修改的时候,你就可以通过添加Category去定制自己需要的方法。这样,不需要访问其源代码、也不需要创建子类,Category使用简单的方式,实现了类的相关方法的模块化,把不同的类方法分配到不同的分类文件中,这样可以保证类的原原来的基础上,较小的改动就可以增加需要的功能。比如扩展系统提供的方法,扩展CocoaPod管理的方法时,我们会经常用到Category。
上example(我习惯将原理写在代码注释中,所以认真看哟,当然最后我也会做总结):
创建MJPerson类和两个分类MJPerson+Test、MJPerson+Eat:
//MJPerson.h和MJPerson.m文件#import@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)@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*)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 runCategoryTest[2737:706571] I can testCategoryTest[2737:706571] I can eatCategoryTest[2737:706571] 刚刚赋值的age: 10CategoryTest[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 voidattachCategories(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进行赋值的,否则会报错。