参考文章
iOS-分类(Category)
“class-continuation分类”的5大用处
Category底层结构及源码分析
iOS 给分类(Category文件)添加属性
demo
category的测试demo 包括给分类添加属性 以及私有方法的调用
什么是Category
- Category是Objective-C 2.0之后添加的语言特性,分类、类别其实都是指的Category。
- Category的主要作用是为已经存在的类添加方法。也可以说是将庞大的类代码按逻辑划入几个分区。
- Objective-C 中的 Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。
Category的优点
- 声明私有方法
- 分解体积庞大的类文件
- 把Framework私有方法公开
- 模拟多继承(另外可以模拟多继承的还有protocol)
Category的用法示例
在这个demo中,又一个person类,这个类很庞大,想要用Category按代码逻辑划分为几个分区。
分类的创建
2.
file的name:功能(这里我的是person的name分区)
filetype选择Category
class:我的继承于Person
Category源码分析
Category源码
Category源码
Category 是表示一个指向分类的结构体的指针,其定义如下:
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
Category结构体主要包含了分类定义的实例方法与类方法,其中instance_methods 列表是 objc_class 中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
分类的特点
-
分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。
原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性
-
分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量,会编译通过,但是引用变量会报错;
-
如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为 分类 > 本类 > 父类;
-
同名分类方法生效取决于编译顺序: 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。
分类的实现原理
我们不主动引入 Category 的头文件,Category 中的方法都会被添加进主类中。我们可以通过 - performSelector: 等方式对 Category 中的相应方法进行调用,但是调用的地方会有黄色警告,但是方法依然可以实现。(如果这里不想有这个黄色警告,可以导入分类的头文件)
实现原理:
a)将 Category 和它的主类(或元类)注册到哈希表中;
b)如果主类(或元类)已实现,那么重建它的方法列表。
Category 中的实例方法和属性被整合到主类中;
类方法则被整合到元类中;
对协议的处理比较特殊,Category 中的协议被同时整合到了主类和元类中。
最终都是通过调用 staticvoid remethodizeClass(Class cls) 函数来重新整理类的数据的。
为什么不能添加成员变量
运行时的class消息:
由下面代码可以看出,方法列表,属性列表,协议列表都是可读可写的,但是成员变量列表是只读的。(const常量符号)
为什么不能调用分类中的属性
- 虽然不能添加成员变量,但是是可以在分类中添加属性。
- 添加的属性系统并没有自动生成成员变量,也没有实现set和get方法,只是生成了set和get方法的声明。这就是为什么在分类中扩展了属性,在外部并没有办法调用。
- 在外部调用点语法设值和取值,本质其实就是调用属性的set和get方法,现在系统并没有实现这两个方法,所以外部就没法调用分类中扩展的属性。
Person *p = [[Person alloc] init];
p.age = 20;
NSLog(@"%d",p.age);
运行报错
-[Person setAge:]: unrecognized selector sent to instance 0x100709220
-[Person age]: unrecognized selector sent to instance 0x1005639e0
- 首先能调用p.page=20,和p.age两句,说明系统已经生成了set和get方法的声明;
- 运行时,又会报找不到setAge:和age方法而报错,说明系统没有实现set和get方法。
- 直接调用_age也会报错,说明没有生成成员变量。这样得以证明以上关于分类中属性的结论。
我的demo
可以正常编译,运行时会出错,说明声明了set和get方法,只是没有实现。
给分类添加属性(分类添加实例变量-关联对象)
首先介绍一下关联对象
-
AssociationsManager 是顶级的对象,维护了一个从 spinlock_t 锁到 AssociationsHashMap 哈希表的单例键值对映射;
-
AssociationsHashMap 是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射;
-
ObjectAssociationMap 是一个 C++ 中的 map ,维护了从 key 到 ObjcAssociation 的映射,即关联记录;
-
ObjcAssociation 是一个 C++ 的类,表示一个具体的关联结构,主要包括两个实例变量,_policy 表示关联策略,_value 表示关联对象。
把分类中的属性作为关联对象
核心代码
objc_setAssociatedObject(self, &propertyTestKey, testStr, OBJC_ASSOCIATION_COPY);
这句就是把定义的属性testStr作为self的键值为propertyTestKey的关联对象,关联方式为OBJC_ASSOCIATION_COPY。
self的所有关联对象构成了一张AssociationsMap表,其中键值为propertyTestKey的对象就是存贮testStr值和关联方式的。
Category中添加的方法为什么会覆盖原来类中的方法?解释原理?
分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法、属性,协议数据拷贝到类对象的方法列表中。
runtime首先加载某个类的所有Category数据,然后把所有Category的方法、属性、协议数据,合并到一个大数组中(后面参与编译的Category数据会在数组的前面),最后将合并后的分类数据(方法、属性、协议),插入到原来数据的前面。所以调用方法时会优先到调用Category中的方法,当父类中有同样的方法就不会调用。
一个特殊的分类——Class-continuation
“class-continuation 分类” 和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,
而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,“class-continuation 分类”没有名字,其写法如下:
@interface Person ()
// methods
@end
Class-continuation的作用
1. 隐藏类
我们下载的源码中,基本都是.h文件,.m文件是不公开的,而Class-continuation写在.m文件中,添加到其中的类可以被隐藏。
2. 编写Objctive-C++
编写 Objective-C++ 代码时 “class-continuation 分类” 也尤为有用。Objective-C++ 是 Objective-C 与 C++ 的混合体,其代码可以用这两种语言来编写。
假如某个类打算这样写:
#import <Foundation/Foundation.h>
#include "SomeCppClass.h"
@interface Class : NSObject {
@private
SomeCppClass _cppClass;
}
@end
Objective-C++是Objective-C与C++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后端一般用C++来写。另外,有时候要使用的第三方库可能只有C++绑定,此时也必须使用C++来编码。
该类的实现文件可能叫做 Class.mm,其中 .mm 扩展名表示编译器应该将此文件按 Objective-C++ 来编译,否则,就无法正确引入 SomeCppClass.h 了。然而请注意,名为 SomeCppClass 的这个 C++ 类必须完全引入,因为编译器要完整地解析其定义方能得知 _cppClass 实例变量的大小。
于是,只要是包含 Class.h 的类,都必须编译为 Objective-C++ 才行,因为它们都引入了 SomeCppClass 类的头文件。这很快就会失控,最终导致整个应用程序全部都要编译为 Objective-C++。这么做确实完全可行,不过相当别扭,尤其是将代码发布为程序库供其他应用程序使用时,更不应该如此。要求第三方开发者将其源文件扩展名均改为 .mm 不是很合适。
某些系统库用到了这种模式,比如网页浏览器框架 WebKit,其大部分代码都以 C++ 编写,然而对外展示出来的却是一套整洁的 Objective-C 接口。 CoreAnimation 里面也用到了此模式,它的许多后端代码都用 C++ 写成,但对外公布的却是一套纯 Objective-C 接口。
(OC++代码由两种语言编写和编译,如果导入C++文件,就必须编译为OC++对象,写在.h文件中,则所有导入该文件的文件都需要编译为OC++对象,写在.m文件中,就只有该文件编译为OC++对象)
3. 扩展public接口中声明的属性
将public接口中声明为“只读”的属性扩展为“可读写”,以便在类的内部设置其值。这样,封装在类中的数据就由实例本身来控制,而外部代码则无法修改其值。
// EOCPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
-(id)initWithFirstName:(NSString *)firstName
lastName:(NSString *)lastName;
@end
NS_ASSUME_NONNULL_END
// EOCPerson.m
@interfaceEOCPerson()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson
-(void)someMethod
{
NSString *test = @"test";
//在class-continuation分类中不管有没有定义readwrite,这个_firstName都可以赋值
_firstName = test;
//只有分类中定义了readwrite,才可以使用点语法和setFirstName:设置方法
self.firstName = test;
[self setFirstName:test];
}
@end
4. 声明只会在类实现代码中用到的方法
(语法上来说,这与定义在实现块里没什么区别)
只会在类的实现代码中用到的私有方法也可以声明在“class-continuation分类”中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。这些方法可以这样写:
@interface EOCPerson()
//私有方法
-(void)p_privateMethod;
@end
@implementation EOCPerson
-(void)p_privateMethod {
}
@end
5. 遵守的私有协议
若对象所遵从的协议只应视为私有,则可在“class-continuation分类”中声明。有时由于对象所遵从的某个协议在私有API中,所以我们可能不太想在公共接口中泄露这一信息。
#import "EOCPerson.h"
#import "EOCSecretDelegate.h"
@interfaceEOCPerson() <EOCSecretDelegate>
@end
@implementation EOCPerson
/*… */
@end