[toc]
参考
Category 的底层源码
objc所有类和对象都是c结构体(指针), category当然也一样, 下面是 runtime 中 category 的结构:
// category文件转为c++代码可以得到:
struct _category_t {
const char *name; // 1
struct _class_t *cls; // 2
const struct _method_list_t *instance_methods; // 3
const struct _method_list_t *class_methods; // 4
const struct _protocol_list_t *protocols; // 5
const struct _prop_list_t *properties; // 6
};
1. name 类名 (注意, 并不是 category 小括号里写的名字)
2. cls 要扩展的类对象, 编译期间这个值是不会有的, 在app被 runtime 加载时才会根据name对应到类对象 ★★??
3. instance_methods 这个 category 所有的 - 方法
4. class_methods 这个 category 所有的 + 方法
5. protocols 这个 category 实现的 protocol
6. properties 这个 category 所有的 property; 这也是 category 里面可以定义属性的原因,不过这个 property 不会@synthesize实例变量, 一般有需求添加实例变量属性时会采用 objc_setAssociatedObject 和 objc_getAssociatedObject 方法绑定方法绑定, 不过这种方法生成的与一个普通的实例变量完全是两码事。
程序一经编译, 所有分类都会转换为 struct _category_t
变量
Category 作用
- 为已经存在的类添加方法; (在不改变原来类的基础上, 为类增加对象方法、类方法)
- 可以把类的实现分开在几个不同的文件里面。(Apple推荐)
a) 可以减少单个文件的体积;
b) 可以把不同的功能组织到不同的category里;
c) 可以由多个开发者共同完成一个类;
d) 可以按需加载想要的category 等等; - 声明私有方法 (Apple推荐)
- 模拟多继承
- 把framework的私有方法公开
Category 实现原理
Category 编译之后的底层结构是 struct category_t
, 里面存储着分类的对象方法、类方法、属性、协议信息等
在程序运行时, runtime 动态将 Category 的这些信息, 附加到类(类对象、元类对象)信息中。
Category 使用注意
- 分类只能增加方法;
- 分类方法实现中可以访问原来类中声明的成员变量;
- 分类可以重写原来类中的方法, 但是会覆盖掉原来的方法, 会导致原来的方法没法再使用;
- 方法调用的优先级: 分类(最后参与编译的分类优先) ––> 原来类 ––> 父类;
- 分类添加属性, 不会自动生成下划线开头的成员变量和
getter/setter
方法;
Category 合并的过程
通过 Runtime 加载某个类的所有Category数据
把所有 Category 的方法、属性、协议数据, 分别合并到一个二维数组中
后面参与编译的 Category 数据, 放在数组的前面(向前附加)
将合并后的分类数据(方法、属性、协议), attach到类原来数据的前面
imageCategory 源码解读顺序
-
objc-os.mm
// 运行时入口文件
_objc_init // 运行时初始化
map_images
map_images_nolock -
objc-runtime-new.mm
_read_images // 读取镜像
remethodizeClass // 新版源码不存在
attachCategories
attachLists
realloc、memmove、 memcpy
Category 中的 load
与其他方法不同, 每个类的 load 都是独立的, 不存在继承、重写;
在 Category 中重写 load 函数不会替换原始类中的 load , 原始类和 Category 中的 load 函数都会被执行, 原始类的load会先被执行, 再执行 Category 中的 load 函数。
当有多个 Category 都实现了 load 函数, 这几个 load 函数都会执行且顺序不确定。[TBC]???
Category 中的 initialize
如果被发消息的类的分类实现了 +initialize 方法, 就会覆盖这个类中的实现。
AssociatedObjects 关联对象
不使用继承, 如何給NSArray添加一个属性?
不能使用继承, 但通常使用分类只能添加方法, 而不能添加属性 (不会生成getter和setter方法), 这种情况应该使用runtime的关联对象, 来为分类的属性手动添加getter和setter, 关联上这个属性的存取。
关联对象 是指某个OC对象通过一个唯一的key连接到一个类的实例上。
举个例子: xiaoming是Person类的一个实例, 他的dog(一个OC对象) 通过一根绳子(key)被他牵着散步, 这可以说xiaoming和dog是关联起来的, 当然xiaoming可以牵着多个dog。
设置关联
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
获取关联
id objc_getAssociatedObject(id object, const void *key)
移除关联
void objc_removeAssociatedObjects(id object)
object : 关联的源对象
key : 获取被关联者的索引key, 要求唯一
value : 被关联者
policy : 关联策略,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
使用 objc_removeAssociatedObjects
可以断开所有关联。通常情况下不建议使用这个函数,因为他会断开所有关联。只有在需要把对象恢复到“原始状态”的时候才会使用这个函数。
例1
为UIButton增加一个Category, 定义一个方法供外部传入实现button点击回调的block, 然后用 objc_setAssociatedObject 设置UIButton关联该block, 在UIButton的点击事件中用 objc_getAssociatedObject 获取该block, 进行回调。
#import "UIButton+block.h"
#import <objc/runtime.h>
// 声明一个静态的索引key,用于获取被关联对象的值
static char *buttonClickKey;
@implementation UIButton (block)
// 供外部调用的方法, 传入的block为点击btn要执行的代码
- (void)handleClickCallBack:(ButtonClickCallBack)callBack {
[self addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];
// 将button的实例与回调的block通过索引key进行关联
objc_setAssociatedObject(self, &buttonClickKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)buttonClicked {
// 通过静态的索引key获取被关联对象(这里就是回调的block)
ButtonClickCallBack callBack = objc_getAssociatedObject(self, &buttonClickKey);
if (callBack) {
callBack(self);
}
}
@end
例2
为UIButton+associate分类添加属性, 并利用Associate手动实现其getter/setter
#import <UIKit/UIKit.h>
@interface UIButton (associate)
@property (nonatomic, copy) NSString *titleStr;
- (void)clickWithBlock:(void(^)(UIButton *button))callBack;
@end
#import "UIButton+associate.h"
#import <objc/runtime.h>
static char *btnClickKey;
static char *titleStrKey;
@interface UIButton ()
@property (nonatomic, copy) void (^callBack)(UIButton *button);
@end
@implementation UIButton (associate)
- (void)setTitleStr:(NSString *)titleStr {
objc_setAssociatedObject(self, &titleStrKey, titleStr, OBJC_ASSOCIATION_COPY);
[self setTitle:titleStr forState:UIControlStateNormal];
}
- (NSString *)titleStr {
return objc_getAssociatedObject(self, &titleStrKey);
}
- (void)setCallBack:(void (^)(UIButton *))callBack {
objc_setAssociatedObject(self, &btnClickKey, callBack, OBJC_ASSOCIATION_COPY);
}
- (void (^)(UIButton *))callBack {
return objc_getAssociatedObject(self, &btnClickKey);
}
- (void)clickWithBlock:(void(^)(UIButton *button))callBack {
[self addTarget:self action:@selector(btnClicked) forControlEvents:UIControlEventTouchUpInside];
self.callBack = callBack;
}
- (void)btnClicked {
if (self.callBack) {
self.callBack(self);
}
}
@end
面试题
Category的使用场景?
见本文 Category 作用
Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承, 但是一般情况下不会主动去调用load方法, 都是让系统自动调用
load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
Category能否添加成员变量?如果可以, 如何给Category添加成员变量?
不能直接给 Category 添加成员变量, 但是可以间接实现 Category 有成员变量的效果
Category 和 Class Extension 的区别?
-
数据合并
Category 是在运行时, 才会将数据合并到类信息中。Extension 在编译时, 它的数据就已经包含在类信息中;
-
@implementation
Category 具有 @implementation , 而 Extension 没有。
-
使用目的
Extension 基本目的是私有化属性、方法、成员变量、协议等信息 (一般写在.m文件中)。
Category 的目的 (见本文)。
-
使用限制
必须有一个类的源码才能为一个类添加 Extension, 所以一般情况下你无法为系统的类(比如NSString) 添加Extension, 因为Extension只能写在类的.h或.m中;
但是可以通过写在 Category 中, 间接导入
Category 为何会覆盖主类的方法?
分类中的方法、属性、协议等信息会通过 runtime 机制, 附加到对应类的类对象和元类对象结构中。
因为倒序组装, 所有分类列表和向前附加的动作 (将分类的方法属性协议等信息插入到类对象列表前面), 这就会导致:
- 分类与类, 优先调用分类的方法
- 分类与分类, 优先调用后编译的分类方法
这不是真正的覆盖, 原先的方法还在, 只是在方法调用时, 优先被找到
Category 中添加属性, 为什么要手动实现 getter/setter 方法?
在分类中添加属性, Xcode不会自动为其生成一个下划线开头的成员变量
及setter/getter
方法, 如果没有手动的实现这两个方法, 直接在外面通过点语法调用这个属性, 肯定就直接挂了, Unrecognised selector send to instance, 因为他压根就没有这两个方法, 能不挂吗?
所以当你真的在分类中声明了一个属性的时候, 就要手动的去实现这个属性的setter/getter方法, 这个时候就要用到 runtime 了, 关联上这个属性的存取过程 (关联对象AssociatedObject) 。
Category 为什么不能手动添加一个下划线开头的成员变量, 然后在实现存取器方法的时候对这个成员变量来存取呢?非要用到所谓的运行时吗?
因为: 成员变量是类的内容, 分类本身并不是一个真正的类, 它并没有自己的ISA指针。类最开始生成了很多基本属性, 比如IvarList, MethodList, 分类只会将自己的Method attach到主类, 并不会影响到主类的IvarList。这就是分类里面不能增加成员变量的原因。
总结: 其实分类中是可以添加属性的, 但是一定做不到添加成员变量, 因为现在XCode会自动给类的属性生成下划线开头的成员变量让大家对这两个概念有点混淆。
给分类添加成员变量会报错: Instance variables may not be placed in categories
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
网友评论