由于需求的变化,要为原有类扩展新功能,我们一般有两个方法:继承和组合。而category
就是组合的一种具体实现技术。在Objective-C 2.0中,提供了category
这个语言特性,可以动态地为已有类添加新行为。
分类功能及特点
我们使用分类可以做哪些事情呢?
- 扩展已有类(为已存在的类添加方法、属性、协议等)
- 分解体积庞大的类文件(减少单个文件体积、不同功能组织到不同category中、按需加载想要的category)
声明私有方法(只在需要的地方引入分类,即可调用分类中的方法,不引用该分类的就无法调用(重写原类方法的除外))
模拟多继承(分类中声明别的类中的方法,不实现,通过消息转发实现调用)
- 把Framework的私有方法公开化(只要知道Framework中私有方法的声明,那么可以在分类中声明Framework中的私有方法,这样可以在需要的地方引入这个分类,就可以调用到Framework中的私有方法了)
分类的特点:
- 运行时决议(分类编写好后,并没有为宿主类添加上分类的内容,而是在运行时通过runtime,将分类中添加的内容,添加到宿主类中)
- 可为系统类添加分类
分类中可添加的内容:
- 实例方法
- 类方法
- 协议
- 实例属性
- 类属性(形如:@property (nonatomic, strong, class,readonly) NSString *classProperty;)
分类结构体:
1 | struct category_t { |
源码分析
分类的加载调用栈:
分类源码分析:
1 | static void remethodizeClass(Class cls) |
分类源码分析结论:
- 同名分类方法谁能生效取决于编译顺序(—>attachCategories中的源码可以看出)
- 分类的添加的方法可以“覆盖”原类方法,并不是真的覆盖,只是分类的方法排在原来方法之前,方法查找时会先查到分类的方法,所以会调用分类的方法,表现上像是原类的方法被“覆盖”了似的
代码验证
1、验证同名分类方法 生效与否与编译顺序的关系
代码结构如图:
三个类中都有方法- (void)methodA
,只不过是三个类中打印的内容不同
当编译顺序与打印结果的对照表如下:
编译顺序 | 打印结果 | 说明 |
---|---|---|
MyClass+Test1:-[MyClass(Test1) methodA] | 后编译的先被访问 | |
MyClass+Test2:-[MyClass(Test2) methodA] | 后编译的先被访问 | |
MyClass+Test1:-[MyClass(Test1) methodA] | 原类的方法会被分类的直接”覆盖”,与编译顺序无关 |
2、私有方法公开化
在MyClass.m中定义并实现了一个私有方法
1 | - (void)privateMethod{ |
在MyClass的分类Test1的.h中声明这个私有方法
1 | @interface MyClass (Test1) |
在想要调用这个私有方法的地方引入#import "MyClass+Test1.h"
就能调用到MyClass的私有方法了
1 | #import "MyClass+Test1.h |
3、+(void)load
和+(void)initialize
的调用顺序
在MyClass及其分类中都添加这两个方法,只不过打印不同
1 | +(void)load |
+(void)load
方法,文件镜像被读取时调用,直观的表现是程序刚运行就会看到打印结果
编译顺序与打印结果如下:
编译顺序 | 打印结果 | 说明 |
---|---|---|
MyClass:+[MyClass load] MyClass+Test2:+[MyClass(Test2) load] MyClass+Test1:+[MyClass(Test1) load] |
原类最先加载,分类的加载顺序与编译顺序相同 | |
MyClass:+[MyClass load] MyClass+Test1:+[MyClass(Test1) load] MyClass+Test2:+[MyClass(Test2) load] |
原类最先加载,分类的加载顺序与编译顺序相同 | |
MyClass:+[MyClass load] MyClass+Test2:+[MyClass(Test2) load] MyClass+Test1:+[MyClass(Test1) load] |
原类最先加载,分类的加载顺序与编译顺序相同 |
+(void)initialize
只有在第一次被使用是会调用一次且仅一次,其他与普通方法相同
编译顺序与打印结果如下:
编译顺序 | 打印结果 | 说明 |
---|---|---|
MyClass+Test1:+[MyClass(Test1) initialize] | 后编译的先被访问 | |
MyClass+Test1:+[MyClass(Test2) initialize] | 后编译的先被访问 | |
MyClass+Test1:+[MyClass(Test1) initialize] | 原类的方法会被分类的”覆盖” |
QA
Q:
1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?
2)、这么些个+load方法,调用顺序是咋样的呢?
A:
1)可以
2)先调用原类的load方法,在调用分类的,分类间的调用顺序根据与编译顺序是相同的
验证:
在Xcode
中点击Edit Scheme
,添加如下两个环境变量:
1 | //执行load的方法是打印 |
配置如图(更多的环境变量选项可参见objc-private.h)
打印结果如下:
结合当前的编译顺序如下:
所以,对于上面两个问题,答案是很明显的:
1)可以调用,因为附加category到类的工作会先于+load方法的执行
2)+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。
Q:
扩展(Extension)有哪些作用?
A:
声明私有属性
声明私有成员变量
声明私有方法
Q:
分类和扩展的区别是什么?
A:
扩展的特点 | 分类的特点 |
---|---|
编译时决议 | 运行时决议 |
只能以声明的形式存在,多数情况下寄生于宿主类的.m中 | 分类有声明也有实现 |
不能为系统类添加扩展 | 可以为系统类添加分类 |
可以添加实例变量 | 不可以添加实例变量(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的) |