iOS-分类Category详解和关联对象
Category的实现原理
- Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
Category和Class Extension的区别是什么?
- Class Extension在编译的时候,它的数据就已经包含在类信息中
- Category是在运行时,才会将数据合并到类信息中
Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
- 有load方法
- load方法在runtime加载类、分类的时候调用
- load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用 (子类不会覆盖父类的load方法 因为load是根据函数地址直接调用 而不是是通过objc_msgSend调用具体的可以看这个链接 里面有详细的load 和 initialize的区别)
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
- 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果 实现方式
- 因为分类中的属性只是生成了 set 和 get 方法的声明,其实现和下划线变量都没有生成 所以我们需要将为做的事情完成
#import "MJPerson.h" @interface MJPerson (Test) //{ // int _weight; //} //分类里面声明属性 只是生成 属性的set 和 get 方法 但是并没有去实现和生成_变量 @property (assign, nonatomic) int weight; @property (copy, nonatomic) NSString* name; //- (void)setWeight:(int)weight; //- (int)weight; @end
#define MJKey [NSString stringWithFormat:@"%p", self] @implementation MJPerson (Test) NSMutableDictionary *names_; NSMutableDictionary *weights_; + (void)load { weights_ = [NSMutableDictionary dictionary]; names_ = [NSMutableDictionary dictionary]; } - (void)setName:(NSString *)name { // NSString *key = [NSString stringWithFormat:@"%p", self]; names_[MJKey] = name; } - (NSString *)name { // NSString *key = [NSString stringWithFormat:@"%p", self]; return names_[MJKey]; } - (void)setWeight:(int)weight { // NSString *key = [NSString stringWithFormat:@"%p", self]; weights_[MJKey] = @(weight); } - (int)weight { // NSString *key = [NSString stringWithFormat:@"%p", self]; return [weights_[MJKey] intValue]; } @end
//用字典来实现有已下问题 /*1.存储的地方不一样 //person2.age = 20; // 20是存储在peron2对象内部 //person2.weight = 50; // 50是存放在全局的字典对象里面 2.线程安全问题 (可以加锁解决) */ #import <Foundation/Foundation.h> #import "MJPerson.h" #import "MJPerson+Test.h" int main(int argc, const char * argv[]) { @autoreleasepool { MJPerson *person = [[MJPerson alloc] init]; person.age = 10; person.weight = 40; MJPerson *person2 = [[MJPerson alloc] init]; person2.age = 20; // 20是存储在peron2对象内部 person2.weight = 50; // 50是存放在全局的字典对象里面 NSLog(@"person - age is %d, weight is %d", person.age, person.weight); NSLog(@"person2 - age is %d, weight is %d", person2.age, person2.weight); } return 0; }
- 使用关联对象来实现
#import "MJPerson+Test.h" #import <objc/runtime.h> @implementation MJPerson (Test) - (void)setName:(NSString *)name { // 关联对象 就是将传进来的name 和 person对象(self) 关联起来 ,达到 一个person对象对应一个name 就不用使用字典的方式来做了 /* 参数讲解 <#id _Nonnull object#> 你要给哪一个对象添加关联对象 person对象(self) <#const void * _Nonnull key#> 关联的key 取的时候需要用相当于内部应该有个字典 <#id _Nullable value#> 关联对象是什么(关联的值) name <#objc_AssociationPolicy 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 // objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>) */ objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString *)name { // 隐式参数 // _cmd == @selector(name) return objc_getAssociatedObject(self, _cmd); } - (void)setWeight:(int)weight { objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (int)weight { // _cmd == @selector(weight) return [objc_getAssociatedObject(self, _cmd) intValue]; } //- (NSString *)name //{ // return objc_getAssociatedObject(self, @selector(name)); //} //- (int)weight //{ // return [objc_getAssociatedObject(self, @selector(weight)) intValue]; //} //#define MJNameKey @"name" //#define MJWeightKey @"weight" //- (void)setName:(NSString *)name //{ // objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC); //} // //- (NSString *)name //{ // return objc_getAssociatedObject(self, MJNameKey); //} // //- (void)setWeight:(int)weight //{ // objc_setAssociatedObject(self, MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); //} // //- (int)weight //{ // return [objc_getAssociatedObject(self, MJWeightKey) intValue]; //} //static const void *MJNameKey = &MJNameKey; //static const void *MJWeightKey = &MJWeightKey; //- (void)setName:(NSString *)name //{ // objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC); //} // //- (NSString *)name //{ // return objc_getAssociatedObject(self, MJNameKey); //} // //- (void)setWeight:(int)weight //{ // objc_setAssociatedObject(self, MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); //} // //- (int)weight //{ // return [objc_getAssociatedObject(self, MJWeightKey) intValue]; //} //static const char MJNameKey; //static const char MJWeightKey; //- (void)setName:(NSString *)name //{ // objc_setAssociatedObject(self, &MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC); //} // //- (NSString *)name //{ // return objc_getAssociatedObject(self, &MJNameKey); //} // //- (void)setWeight:(int)weight //{ // objc_setAssociatedObject(self, &MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); //} // //- (int)weight //{ // return [objc_getAssociatedObject(self, &MJWeightKey) intValue]; //} @end
#import <Foundation/Foundation.h> #import "MJPerson.h" #import "MJPerson+Test.h" #import <objc/runtime.h> int main(int argc, const char * argv[]) { @autoreleasepool { MJPerson *person = [[MJPerson alloc] init]; person.age = 10; person.name = @"jack"; person.weight = 30; MJPerson *person2 = [[MJPerson alloc] init]; person2.age = 20; person2.name = @"rose"; person2.name = nil; person2.weight = 50; NSLog(@"person - age is %d, name is %@, weight is %d", person.age, person.name, person.weight); NSLog(@"person2 - age is %d, name is %@, weight is %d", person2.age, person2.name, person2.weight); } return 0; }
关联对象的原理
Category的底层结构
分类的本质(实现原理):
- Category编译之后的底层结构是 struct category_t (结构体),里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
分类底层结构定义的如下:
//objc-runtime-new.h struct category_t { const char *name; classref_t cls; struct method_list_t *instanceMethods; struct method_list_t *classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; // Fields below this point are not always present on disk. struct property_list_t *_classProperties; method_list_t *methodsForMeta(bool isMeta) { if (isMeta) return classMethods; else return instanceMethods; } property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); };
项目中每定义一个分类,底层都会增加一个category_t对象。
Category源码阅读顺序:
- objc-os.mm (runtime入口)
- _objc_init (runtime初始化)
- map_images
- map_images_nolock
- objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attachLists
- realloc、memmove、 memcpy
category的加载过程:
- 通过runtime加载类的所有分类数据
- 将所有分类的方法,属性,协议数据分别合并到一个数组 (后面参与编译的Category数据,会在数组的前面)
- 将合并后的分类数据(方法,属性,协议)插入到类原来到数据之前
由源码可见,对同名方法而言,会优先调用分类中的方法。如果多个分类中包含同名方法,则会调用最后参与编译的分类中的方法。
摘录源码中核心的attachCategories实现如下(objc4-756.2):
// 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. 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_t,method_t],[method_t,method_t,method_t]] //二维数组中的一个元素(数组)存放一个分类中的方法列表 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; 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; } } //取出(元)类对象中的数据(class_rw_t) auto rw = cls->data(); prepareMethodLists(cls, mlists, mcount, NO, fromBundle); //将所有分类的方法添加到(元)类对象的方法列表中 rw->methods.attachLists(mlists, mcount); free(mlists); if (flush_caches && mcount > 0) flushCaches(cls); //将所有分类的属性添加到(元)类对象的属性列表中 rw->properties.attachLists(proplists, propcount); free(proplists); //将所有分类的协议添加到(元)类对象的协议列表中 rw->protocols.attachLists(protolists, protocount); free(protolists); }
Category和Extension的区别
区别一
- Category
- Extension
- 可以说是特殊的分类,也称作匿名分类
- 可以给类添加成员属性,但是是私有变量
- 可以给类添加方法,也是私有方法
区别二
虽然有人说Extension是一个特殊的Category,也有人将Extension成为匿名分类,但是两者的区别很大。
-
Category
- 是运行期决定的
- 类扩展可以添加实例变量,分类不能添加实例变量(原因:因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对编译性语言是灾难性的。)
-
Extension
- 在编译器决定,是类的一部分,在编译器和头文件的
@interface
和实现文件里的@implement
一起形成了一个完整的类。 - 伴随着类的产生而产生,也随着类的消失而消失。
- Extension一般用来隐藏类的私有消息,必须有一个类的源码才能添加一个类的Extension,所以对于系统的一个类,比如NSString,就无法添加类扩展。
- 在编译器决定,是类的一部分,在编译器和头文件的
1.Category 基本使用场合
经常用的是给类添加方法,协议、属性,编写的分类里面的方法, 最终是在运行过程中的时候合并到类对象(对象方法)/元类对象(类方法)里面 (不是编译的时候合并的)