Category(分类)
-
在本节中,被扩展的类,统一称为"本类"
-
Category 简介
Category 是
Objective-C 2.0
之后添加的语言特性,其主要作用是为已经存在的类添加方法(无论工程中是否有该类的源码)。Category 可以做到在既不子类化,也不侵入一个类的源码的情况下,为原有的类添加新的方法,从而实现扩展一个类或者分离一个类的目的。在程序运行时,RunTime 会把 Category 中的内容附加到本类中
Apple 的 SDK 中就大面积地使用了Category 这一特性。比如UIKit.framework
中的UIView
,Apple 把不同功能的 API 进行了分类,这些分类包括:UIViewGeometry
、UIViewHierarchy
、UIViewRendering
等Category 的特点:
- Category 中能添加 对象方法、类方法、协议、属性(属性可以添加,但只会生成
getter
setter
的声明,不会生成getter
setter
的实现,也不会生成对应的成员变量),Category 中不能添加成员变量 - Category 中的内容(对象方法、类方法、协议、属性)是在运行阶段加入对应的本类中的,这意味着 Category 不写方法的实现也可以编译通过,但运行时调用会报错
- Category 有独立的 .h 和 .m 文件,通常 Category 的 .h 文件命名为
Class+Category.h
,.m 文件命名为Class+Category.m
- 如果 Category 中存在和本类同名的方法,调用时会执行 Category 的方法(即会忽略本类的方法)。所以同名方法调用的优先级为:分类 > 本类 > 父类
- 如果多个 Category 中存在同名的方法,调用时会执行最后参与编译的 Category 的方法
- 一个类的 Category 的数量是没有限制的,但是每一个 Category 的名字不能相同(同名的 Category 编译会报错)
- Category 如果想要访问本类中公开的 成员变量、属性、方法,则需要导入本类的头文件
- Category 能访问本类中
@public
和@protected
权限的成员变量,不能访问@private
权限的成员变量。Category 和子类一样,只能通过本类暴露的方法来访问@private
权限的成员变量 - Category 是 Objective-C 中特有的语法,其本质是一个指向
struct category_t
的指针
Category 的作用:
- 可以在不修改原有类的基础上,为原有类添加方法,无论工程中是否有该类的源码(最主要的作用是给系统类扩展自己定义的方法)
- 可以实现多个开发者共同开发同一个类
- 可以将类的实现按功能拆解成多个模块,分散到不同的文件中,减小单一类文件的体积。使用时,按功能需求选择性加载不同的 Category
- 模拟 Objective-C 不支持的多继承(使用 Protocol 也可以模拟多继承)
- Category 可以用来公开本类中私有的属性和方法
- 向类中添加非正式协议
- Category 中能添加 对象方法、类方法、协议、属性(属性可以添加,但只会生成
-
创建 Category
例如,给 UICoclor 添加一个把 16 进制字符串转换为 RGB 颜色的分类,在工程中 -
New File - iOS - Objective-C File
,文件类型(File Type
)选择 Category:
创建的 Category 如下所示:
-
Category 中可以添加哪些内容
Objective-C 的分类是由
Category
类型来表示的,而Category
类型实际上是一个指向struct category_t
的指针,它的定义如下:typedef struct category_t *Category; 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; // 分类的属性列表 };
由
struct category_t
的定义可知,
分类中可以添加:对象方法、类方法、协议、属性
分类中不能添加:成员变量(因为没有成员变量列表struct objc_ivar_list *ivars
)因为在分类中,没有用于存储成员变量的
struct objc_ivar_list *ivars
所以在分类中,虽然可以使用@property
添加属性,但是编译器只会生成属性getter
setter
方法的声明,并不会生成带下划线的成员变量,更不会生成属性getter
setter
方法的实现因此,如果在分类中添加了属性,并且代码中使用 点语法 调用了属性的
getter
或者setter
,那么在程序运行时会报方法找不到的错误 -
为什么 Category 中不能添加成员变量
Question ①:
为什么表示分类的结构体struct category_t
中不添加一个存储成员变量的字段struct objc_ivar_list *ivars
,这样分类就可以存储成员变量,并且能正常地使用属性了?Answer ①:
在程序运行时,RunTime 会把 分类中的内容 附加到 本类 中Objective-C 的类是由
Class
类型来表示的,而Class
类型实际上是一个指向struct objc_class
的指针,它的定义如下:typedef struct objc_class *Class; struct objc_class { Class isa OBJC_ISA_AVAILABILITY; // is-a 指针,对象的 is-a 指针指向类对象,类对象的 is-a 指针指向元类对象,元类对象的 is-a 指针指向根源类对象 #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父类 const char *name OBJC2_UNAVAILABLE; // 类名 long version OBJC2_UNABAILABLE; // 类的版本信息,默认为 0 long info OBJC2_UNAVAILABLE; // 类信息,供运行时使用的一些标识位 long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表(一维) struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 该类的方法列表链表(二维) struct objc_cache *cache OBJC2_UNAVAILABLE; // 该类的方法缓存 struct objc_protocol_list *protocoles OBJC2_UNAVAILABLE; // 该类的协议链表 #endif } OBJC_UNAVAILABLE;
在运行时,
struct objc_class
的大小是固定的,不能往struct objc_class
中添加数据,只能修改数据。在struct objc_class
中:
ivars(成员变量列表)
是struct objc_ivar_list
类型的指针,并且ivars(成员变量列表)
指向的是一块大小固定的区域:只能修改成员变量的值,不能增加新的成员变量
methodLists(方法列表数组)
是objc_method_list *
类型的指针(指向指针的指针,即指向方法列表的指针)。即,methodLists(方法列表数组)
是一个二维数组,虽然没有办法扩展methodLists
指向的内存区域,但是可以修改*methodLists
的值来增加成员方法
因此,在运行时,Class 中不能动态地添加成员变量,只能动态地添加方法
又因为,Category 中的内容在运行时会被附加到 Class 中,所以 Category 中不能添加成员变量,只能添加方法(Category 的属性,本质上是getter
setter
方法,所以 Category 可以添加属性)Question ②:
为什么不把类的成员变量列表设计成可变的呢?Answer ②:
因为在 RunTime 初始化时,对象的内存布局已经确定,如果此时再向对象中添加成员变量就会破坏类的内部布局,这对编译型语言来说是灾难性的 -
如何向 Category 中添加属性
一般情况下,
属性(property)
是对成员变量(instance variable)
的封装
既然在 Category 中使用@property
添加属性时,编译器只会生成属性getter
setter
方法的声明,并不会生成带下划线的成员变量,更不会生成属性getter
setter
方法的实现(这将导致我们无法访问成员变量,也无法自己手动实现成员变量的存取方法)
那么在 Category 中添加一个不能访问的属性有什么意义呢?实际上可以使用 RunTime 的关联对象为 Category 已有的属性设置一个关联值,并添加
getter
setter
方法的实现Person 类:
// Person.h #import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, strong) NSString* name; @property (nonatomic, assign) int age; @end ------------------------------------------------------------------------------------------------ // Person.m #import "Person.h" @implementation Person @end
Person(SportLover) 分类:
// Person+SportLover.h #import "Person.h" @interface Person (SportLover) @property (nonatomic, strong) NSString* sportName; @end ------------------------------------------------------------------------------------------------ // Person+SportLover.m #import "Person+SportLover.h" #import <objc/runtime.h> // 关联对象中属性的 key 值,需要确保全局唯一性 static NSString* kSportName = @"Person+SportLover.sportName"; @implementation Person (SportLover) // 通过 RunTime 的关联对象,为分类的属性实现 getter -(NSString *)sportName { return objc_getAssociatedObject(self, &kSportName); } // 通过 RunTime 的关联对象,为分类的属性实现 setter -(void)setSportName:(NSString *)sportName { objc_setAssociatedObject(self, &kSportName, sportName, OBJC_ASSOCIATION_COPY_NONATOMIC); } @end
调用 Person(SportLover) 分类的属性:
-(void)test { Person* p = [[Person alloc] init]; p.sportName = @"basketball"; NSLog(@"p.sportName = %@", p.sportName ); // 输出: // 2021-01-21 19:05:27.374669+0800 CategoryDemo[8978:621582] p.sportName = basketball }
注意:
虽然可以通过 RunTime 为 Category 的属性动态地添加getter
setter
方法的实现
但是编译器自始至终都没有为 Category 的属性生成带下划线的成员变量
所以在代码中,如果使用_成员变量
的方式调用 Category 属性对应的成员变量,程序还是会报错RunTime 中用于 添加、获取、移除 指定对象上关联值的函数,如下所示:
// 用给定的 key 与关联策略为指定的对象设置关联的值 // param0.object 源对象 // param1.key 用来标记关联值的 key // param2.value 关联值 // param3.policy 关联策略 void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) // 用给定的 key 获取指定对象的关联值 // param0.object 源对象 // param1.key 用来标记关联值的 key // return 关联值 id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key) // 移除指定对象上的所有关联值 // param0.object 源对象 void objc_removeAssociatedObjects(id _Nonnull object) // 关联策略 typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, //关联对象的属性是弱引用 OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //关联对象的属性是强引用并且关联对象不使用原子性 OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //关联对象的属性是 copy 并且关联对象不使用原子性 OBJC_ASSOCIATION_RETAIN = 01401, //关联对象的属性是强引用并且关联对象使用原子性 OBJC_ASSOCIATION_COPY = 01403 //关联对象的属性是 copy 并且关联对象使用原子性 }; // 注意: objc_removeAssociatedObjects(...) 函数的作用是移除指定对象上的所有关联值 如果要移除指定对象上的某个关联值,可以使用 objc_setAssociatedObject(object, &key, nil, policy),即关联值 value 传 nil 关联对象由 AssociationsManager 统一管理 所有对象的关联内容统一放在一个容器 全局哈希表 AssociationsHashMap 中
-
Category、Class、Super Class 方法调用的优先级
如果 Category 中存在和本类同名的方法,调用时会执行 Category 中的方法,即会忽略本类的方法。所以同名方法调用的优先级为:分类 > 本类 > 父类
如果多个 Category 中存在同名的方法,调用时会执行最后参与编译的 Category 的方法在项目 CategoryDemo 中:
① Man 继承 Person,Man+Chinese 和 Man+Japanese 是 Man 的两个分类
② 在这 4 个类中都声明和定义了一个+run
方法,让其简单地输出方法名
③ 到Target - Build Phases - Compile Sources
下,设置这 4 个 .m 文件的编译顺序
④ 各自调用 Person 和 Man 的+run
方法
根据不同的编译顺序,+run
方法的调用结果如下-(void)test { [Person run]; [Man run]; }
注意:
在程序运行时,RunTime 会把 分类中的内容 附加到 本类 中,但是这里的同名方法并没有相互覆盖
也就是说:在 Category 附加完成后,Man 元类的方法列表里会有 3 个+run
方法
其实所有同名的方法都会存储在方法列表中,只是先编译的同名方法访问不到了而已
因为 RunTime 在动态地往本类添加方法时,是倒序遍历方法列表的,而最后编译的 Category 的方法会被放在方法列表的最前面。但是 RunTime 在查找方法的时候是沿着方法列表的正序查找的,它只要一找到对应名称的方法,就会马上调用,殊不知后面可能还有一样名称的方法
在Target - Build Phases - Compile Sources
中,编译顺序是从上往下的,想要 Category 中的同名方法调用的优先级越高,其源文件就需要越往后编译,其源文件就需要越往下放输出 Man 的类方法列表,可以看到 Man、Man(Chinese)、Man(Japanese) 的
+run
方法,都在 Man 的类方法列表中:#import <objc/runtime.h> -(void)printMethodName { // 注意:类方法存储在元类对象中 unsigned int count = 0; Class metaClass = object_getClass([Man class]); Method* methodList = class_copyMethodList(metaClass, &count); for (unsigned i = 0; i < count; i++) { Method method = methodList[i]; NSString* methodName = NSStringFromSelector(method_getName(method)); NSLog(@"method(%02d) : %@", i, methodName); } free(methodList); } // 输出结果如下:Man、Man+Chinese、Man+Japanese 的 +run 方法,都存在于元类的方法列表中 2021-01-22 16:26:19.627455+0800 CategoryDemo[10773:834896] method(00) : run 2021-01-22 16:26:19.628006+0800 CategoryDemo[10773:834896] method(01) : run 2021-01-22 16:26:19.628348+0800 CategoryDemo[10773:834896] method(02) : run
-
Category 与 Class 的 +load 方法的调用顺序
+load
方法不是通过消息传递方式调用(objc_msgSend
),而是直接通过函数指针调用
因为+load
方法是不可以继承的,所以+load
方法的调用不存在类的层级遍历
本类中有+load
方法,Category 中也有+load
方法
本类中的+load
方法和 Category 中的+load
方法,并不是简单的继承或者覆盖的关系,而是独立的两个+load
方法RunTime 加载时,调用
+load
方法的顺序为:父类 > 子类 > 分类
并且 Category 中+load
方法的调用顺序取决于 Category 的编译顺序,先编译的 Category 的+load
方法会先调用在项目 CategoryDemo 中:
① Man 继承 Person,Person+Category 为 Person 的分类,Man+Category 为 Man 的分类
② 重写这 4 个类的+load
方法,让其简单地输出方法名
③ 到Target - Build Phases - Compile Sources
下,设置这 4 个 .m 文件的编译顺序
根据不同的编译顺序,+load
方法的调用结果如下
-
利用 Category 公开本类中私有的:属性 && 方法
可以通过在 Category 的头文件中声明本类中私有的属性或方法的方式
来达到公开本类私有属性或者方法的目的Person 类:
// Person.h #import <Foundation/Foundation.h> @interface Person : NSObject @end ------------------------------------------------------------------------------------------------ // Person.m #import "Person.h" @interface Person () // Person 类私有的属性 @property (nonatomic, strong) NSString* name; @property (nonatomic, assign) int age; @end @implementation Person // Person 类私的有对象方法 -(void)breathe { NSLog(@"呼吸..."); } // Person 类的私有类方法 +(void)protectEarth { NSLog(@"保护地球!!!"); } @end
Person + Export 分类:
// Person+Export.h #import "Person.h" @interface Person (Export) // 在分类的头文件中声明 Person 类的私有属性 @property (nonatomic, strong) NSString* name; @property (nonatomic, assign) int age; // 在分类的头文件中声明 Person 类的私有方法 -(void)breathe; +(void)protectEarth; @end ------------------------------------------------------------------------------------------------ // Person+Export.m #pragma clang diagnostic push #import "Person+Export.h" // 因为在分类的实现文件里,没有关于分类头文件中属性 getter 和 setter 的定义,也没有关于分类头文件中声明的方法的定义 // 所以 clang 编译器会报属性未定义和方法未定义的警告 // 这两个宏的作用只是为了告诉 clang 编译器,在本文件中忽略属性未定义和方法未定义的警告 #pragma clang diagnostic ignored "-Wincomplete-implementation" #pragma clang diagnostic ignored "-Wobjc-property-implementation" @implementation Person (Export) // 在分类的实现文件里面,并没有定义分类头文件中属性的 getter 和 setter,也没有定义分类头文件中声明的方法 // 此时 RunTime 会去调用本类中对应的私有属性和私有方法 @end #pragma clang diagnostic pop
调用 Person 类中的私有属性和方法:
#import "Person+Export.h" -(void)test { Person* p = [[Person alloc] init]; // 访问 Person 类的私有属性 p.name = @"hcg"; p.age = 20; NSLog(@"p.name = %@, p.age = %d", p.name, p.age); // 调用 Person 对象的私有方法 [p breathe]; // 调用 Person 类的私有方法 [Person protectEarth]; } // 输出如下: 2021-01-22 11:27:33.342818+0800 CategoryDemo[9651:707598] p.name = hcg, p.age = 20 2021-01-22 11:27:33.343048+0800 CategoryDemo[9651:707598] 呼吸... 2021-01-22 11:27:33.343201+0800 CategoryDemo[9651:707598] 保护地球!!!
注意:
Category 中公开的属性的类型和名称,必须和本类中私有的属性的类型和名称相同
Category 中公开的方法的类型和名称,必须和本类中私有的方法的类型和名称相同 -
Category 与(本类、子类)的关系
在运行时,Category 中的方法会成为本类的一部分,与本类中原有的方法没有任何区别
通过 Category 添加到本类中的方法会被本类所有的子类继承,就和本类的其它方法一样
Category 中的方法能做,任何在本类中正常定义的方法所能做的事
但是,如果子类要调用父类 Category 中的方法,则需要导入父类 Category 的头文件子类中定义的方法,不会包含在父类的方法列表中
父类的分类定义的方法,会被包含在父类的方法列表中
Extension(扩展)
-
在本节中,被扩展的类,统一称为"本类"
-
Extension 简介
Extension 能为指定的类声明额外的:成员变量、属性、对象方法、类方法
因为 Extension 只有 .h 文件(没有独立的 .m 文件),所以 Extension 所声明的内容,必须依靠本类的 .m 文件实现。因此,只能为已知源代码的类添加 Extension(即无法为系统的类添加 Extension)Extension 的常用形式并不是以一个单独的. h 文件存在,而是寄生在本类的文件中:
Extension 如果定义在本类的 .h 文件中,那么其声明的内容就是公有的
Extension 如果定义在本类的 .m 文件中,那么其声明的内容就是私有的
一般情况下,Extension 会被定义在本类的 .m 文件中,用于声明本类私有的 成员变量、属性、对象方法、类方法Extension 中所声明的内容会在编译阶段被附加到本类中,它就是本类的一部分。即,在编译阶段,Extension 会和本类 .h 文件里的 @interface 以及本类 .m 件里的 @implement 一起形成一个完整的类,Extension 伴随着本类的产生而产生,亦随着本类的消亡而消亡
虽然 Extension 听上去很抽象,但是其实在我们日常的开发中,会经常用到它
回忆一下:(继承自 UIViewController 的 ViewController)与(继承自 NSObject 的 Person),在 .m 文件中有何不同?
下面是通过New File - iOS - Objective-C File
创建的 ViewController 的独立的 Extension 文件
单独创建的 Extension 只有一个 .h 头文件,基于此:
Extension 可以理解成是本类多了一个 .h 头文件,在该 .h 头文件里面声明的 成员变量、属性、方法 等,都需要在本类的 .m 文件中有对应的实现
-
Extension 的两种定义方式
① Extension 定义在本类的 .h 文件中,用于声明公有的:成员变量、属性、对象方法、类方法(不常用)
② Extension 定义在本类的 .m 文件中,用于声明私有的:成员变量、属性、对象方法、类方法(很常用)
Extension 定义在 .m 文件中的所有内容,默认都是 @private 类型,其作用范围只能在自身类,子类和分类都访问不到
注意:
这里说的私有并不是绝对的私有,只是一般情况下可以达到私有的效果,Objective-C 中没有绝对的 私有变量 和 私有方法
因为即使这些 私有变量、私有方法 隐藏在 .m 实现文件里,不暴露在头文件中,开发者仍然可以利用 RunTime 对其进行暴力访问 -
使用 Extension 声明对外是 readonly,对内是 readwrite 的属性
Category 与 Extension 的区别
- Category 在运行时被附加到本类,Extension 在编译时被附加到本类
- Category 不能为本类声明成员变量,Extension 可以为本类声明成员变量
- Category 中声明的属性只会自动生成
getter
setter
的声明,不会生成对应的成员变量,不会生成getter
setter
的实现
Extension 中声明的属性会自动生成getter
setter
的声明,对应的成员变量,以及getter
setter
的实现 - 添加 Category 不需要知道本类的源码,添加 Extension 需要知道本类的源码
- 程序文件:类接口 vs 分类 vs 扩展
注意:
虽然 Extension 的定义和 Category 的定义在格式上非常的类似(Extension 就像是一个没有名字的 Category)
但是因此就把 Extension 称之为 匿名分类,是不合适的
因为通过前面对 Category 和 Extension 的介绍我们知道了:从底层机制上来说,Category 和 Extension 是两个完全不同的东西
其他
-
一些常见的英文单词
成员变量(instance variable = ivar)
属性(property)
对象方法(instance method)
类方法(class method)
协议(protocol) -
方法的声明 && 方法的定义(方法的实现) && 方法的调用
-(void)personTest { // 方法的调用 [Person doSomething]; }
-
如何修改对象的私有属性
#import "Person.h" #import <objc/runtime.h> -(void)personTest { Person* p = [[Person alloc] init]; // 打印 p 对象的成员变量, 获取私有的成员变量名 unsigned int count = 0; Ivar* ivarList = class_copyIvarList([p class], &count); for (int i = 0; i < count; i++) { Ivar variable = ivarList[i]; const char* name = ivar_getName(variable); const char* typeEncoding = ivar_getTypeEncoding(variable); ptrdiff_t offset = ivar_getOffset(variable); NSLog(@"name = %s, type = %s, offset = %ld", name, typeEncoding, offset); } free(ivarList); // 设置 p 对象的私有成员变量 [p setValue:@20 forKey:@"_age"]; NSNumber* personAge = [p valueForKey:@"_age"]; NSLog(@"p.age = %d", [personAge intValue]); [p setValue:@"hcg" forKey:@"_name"]; NSString* personName = [p valueForKey:@"_name"]; NSLog(@"p.name = %@", personName); } 2021-01-27 16:11:23.221590+0800 CategoryDemo[16017:1679710] name = _age, type = i, offset = 8 2021-01-27 16:11:23.221825+0800 CategoryDemo[16017:1679710] name = _name, type = @"NSString", offset = 16 2021-01-27 16:11:23.222245+0800 CategoryDemo[16017:1679710] p.name = hcg 2021-01-27 16:11:23.222643+0800 CategoryDemo[16017:1679710] p.age = 20