iOS 底层原理|Category 本质
一、Category 简介
Category (分类) 是
Objective-C2.0
添加的语言特性,主要为已存在的类添加方法。Category 在既不子类化,也无需修改一个类的源码情况下,为原有的类添加方法,从而实现扩展一个类或者分离一个类。
虽然继承也能为已有的类添加新的方法和属性,但是继承关系增加了不必要的代码复杂度,在运行时候无法与父类的方法区分。
1. Category 作用
- 把类的不同实现方法分开到不同的文件里。这样做有几个显而易见的好处:
a) 可以减少单个文件的体积
b) 可以把不同的功能组织到不同的 Category 里
c) 可以由多个开发者共同完成一个类
d) 可以按需加载想要的 Category
e) ... - 声明私有方法。
- 模拟多继承。
- 将 framework 私有方法公开化。
2. Category(分类) 与 Extension(扩展) 区别
Extension 看起来很像一个匿名的 Category,但是 Extension 和有名字的category几乎完全是两个东西。 Extension 在编译期决议,它就是类的一部分,在编译期和头文件里的 @interface
以及实现文件里的 @implement
一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。Extension 一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加 Extension ,所以你无法为系统的类比如 NSString
添加 Extension。
但是 Category 则完全不一样,它是在运行期决议的。就 Category 和 Extension 的区别来看,我们可以推导出一个明显的事实,Extension 可以添加实例变量,而 Category 是无法添加实例变量的(因为在运行时,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。
二、Category 的本质
我们知道,所有的 OC 类和对象(iOS底层原理 | OC对象的本质),在 runtime 层都是用 struct 结构体,category 也不例外,在 objc-runtime-new.h
中,Category 被定义为 category_t
结构体。category_t
结构体的数据结构如下:
typedef struct category_t *Category;
struct category_t {
const char *name; // 类名
classref_t cls; // 类,在运行时阶段通过 clasee_name(类名)对应到类对象
struct method_list_t *instanceMethods; // Category 中所有添加的对象方法列表
struct method_list_t *classMethods; // Category 中所有添加的类方法列表
struct protocol_list_t *protocols; // Category 中实现的所有协议列表
struct property_list_t *instanceProperties; // Category 中添加的所有属性
};
从 Category 的结构体定义中也可以看出,Category 可以为类添加对象方法、类方法、协议、属性。但是 Category 无法添加成员变量。
1. 透过 Category 的 C++ 源码观察 Category 的本质
想要了解 Category 的本质,我们需要通过 Category 的 C++ 源码进行分析。
以下是我们写的关于 Category 例子:
CategoryEssence.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol CategoryEssenceProtocol <NSObject>
- (void)categoryEssenceProtocolMethod;
+ (void)categoryEssenceProtocolClassMethod;
@end
@interface CategoryEssence : NSObject
- (void)printName;
+ (void)printClassName;
@end
@interface CategoryEssence (Addition) <CategoryEssenceProtocol>
- (void)printName;
+ (void)printClassName;
@end
NS_ASSUME_NONNULL_END
CategoryEssence.m
#import "CategoryEssence.h"
@implementation CategoryEssence
- (void)printName {
NSLog(@"CategoryEssence printName");
}
+ (void)printClassName {
NSLog(@"CategoryEssence printClassName");
}
@end
@implementation CategoryEssence(Addition)
- (void)printName {
NSLog(@"CategoryEssence(Addition) printName");
}
+ (void)printClassName {
NSLog(@"CategoryEssence(Addition) printClassName");
}
#pragma mark -- 协议方法
- (void)categoryEssenceProtocolMethod {
NSLog(@"categoryEssenceProtocolMethod");
}
+ (void)categoryEssenceProtocolClassMethod {
NSLog(@"categoryEssenceProtocolClassMethod");
}
@end
将 CategoryEssence.m
通过
clang -rewrite-objc CategoryEssence.m
得到一个 4.5M、11W+ 的.cpp文件(在 iOS 工程下创建)。忽略掉无关代码,我们分析一波:
Category 的本质就是 _category_t
结构体 类型,其中包含了以下几部分:
_method_list_t
类型的对象方法列表结构体;_method_list_t
类型的类方法列表结构体;_protocol_list_t
类型的协议列表结构体;_prop_list_t
类型的属性列表结构体;
但是 _category_t
不包含 _ivar_list_t
成员变量结构体,这间接说明 Category 不能添加成员变量的事实。
// @implementation CategoryEssence
static void _I_CategoryEssence_printName(CategoryEssence * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3j_fvy599js5nnglvw7jslq09t80000gn_T_CategoryEssence_88f40a_mi_0);
}
static void _C_CategoryEssence_printClassName(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3j_fvy599js5nnglvw7jslq09t80000gn_T_CategoryEssence_88f40a_mi_1);
}
// @end
// @implementation CategoryEssence(Addition)
static void _I_CategoryEssence_Addition_printName(CategoryEssence * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3j_fvy599js5nnglvw7jslq09t80000gn_T_CategoryEssence_88f40a_mi_2);
}
static void _C_CategoryEssence_Addition_printClassName(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3j_fvy599js5nnglvw7jslq09t80000gn_T_CategoryEssence_88f40a_mi_3);
}
static void _I_CategoryEssence_Addition_categoryEssenceProtocolMethod(CategoryEssence * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3j_fvy599js5nnglvw7jslq09t80000gn_T_CategoryEssence_88f40a_mi_4);
}
static void _C_CategoryEssence_Addition_categoryEssenceProtocolClassMethod(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3j_fvy599js5nnglvw7jslq09t80000gn_T_CategoryEssence_88f40a_mi_5);
}
// @end
#pragma warning(disable:4273)
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_INSTANCE_METHODS_CategoryEssence __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_CategoryEssence_printName}}
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CLASS_METHODS_CategoryEssence __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"printClassName", "v16@0:8", (void *)_C_CategoryEssence_printClassName}}
};
static struct _class_ro_t _OBJC_METACLASS_RO_$_CategoryEssence __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1, sizeof(struct _class_t), sizeof(struct _class_t),
(unsigned int)0,
0,
"CategoryEssence",
(const struct _method_list_t *)&_OBJC_$_CLASS_METHODS_CategoryEssence,
0,
0,
0,
0,
};
static struct _class_ro_t _OBJC_CLASS_RO_$_CategoryEssence __attribute__ ((used, section ("__DATA,__objc_const"))) = {
0, sizeof(struct CategoryEssence_IMPL), sizeof(struct CategoryEssence_IMPL),
(unsigned int)0,
0,
"CategoryEssence",
(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_CategoryEssence,
0,
0,
0,
0,
};
extern "C" __declspec(dllimport) struct _class_t OBJC_METACLASS_$_NSObject;
extern "C" __declspec(dllexport) struct _class_t OBJC_METACLASS_$_CategoryEssence __attribute__ ((used, section ("__DATA,__objc_data"))) = {
0, // &OBJC_METACLASS_$_NSObject,
0, // &OBJC_METACLASS_$_NSObject,
0, // (void *)&_objc_empty_cache,
0, // unused, was (void *)&_objc_empty_vtable,
&_OBJC_METACLASS_RO_$_CategoryEssence,
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_NSObject;
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_CategoryEssence __attribute__ ((used, section ("__DATA,__objc_data"))) = {
0, // &OBJC_METACLASS_$_CategoryEssence,
0, // &OBJC_CLASS_$_NSObject,
0, // (void *)&_objc_empty_cache,
0, // unused, was (void *)&_objc_empty_vtable,
&_OBJC_CLASS_RO_$_CategoryEssence,
};
static void OBJC_CLASS_SETUP_$_CategoryEssence(void ) {
OBJC_METACLASS_$_CategoryEssence.isa = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_CategoryEssence.superclass = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_CategoryEssence.cache = &_objc_empty_cache;
OBJC_CLASS_$_CategoryEssence.isa = &OBJC_METACLASS_$_CategoryEssence;
OBJC_CLASS_$_CategoryEssence.superclass = &OBJC_CLASS_$_NSObject;
OBJC_CLASS_$_CategoryEssence.cache = &_objc_empty_cache;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CLASS_SETUP[] = {
(void *)&OBJC_CLASS_SETUP_$_CategoryEssence,
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_CategoryEssence_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_CategoryEssence_Addition_printName},
{(struct objc_selector *)"categoryEssenceProtocolMethod", "v16@0:8", (void *)_I_CategoryEssence_Addition_categoryEssenceProtocolMethod}}
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_CategoryEssence_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"printClassName", "v16@0:8", (void *)_C_CategoryEssence_Addition_printClassName},
{(struct objc_selector *)"categoryEssenceProtocolClassMethod", "v16@0:8", (void *)_C_CategoryEssence_Addition_categoryEssenceProtocolClassMethod}}
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_OPT_INSTANCE_METHODS_NSObject __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"debugDescription", "@16@0:8", 0}}
};
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[4];
} _OBJC_PROTOCOL_PROPERTIES_NSObject __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
4,
{{"hash","TQ,R"},
{"superclass","T#,R"},
{"description","T@\"NSString\",R,C"},
{"debugDescription","T@\"NSString\",R,C"}}
};
struct _protocol_t _OBJC_PROTOCOL_NSObject __attribute__ ((used)) = {
0,
"NSObject",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSObject,
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_OPT_INSTANCE_METHODS_NSObject,
0,
(const struct _prop_list_t *)&_OBJC_PROTOCOL_PROPERTIES_NSObject,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSObject
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSObject = &_OBJC_PROTOCOL_NSObject;
static const char *_OBJC_PROTOCOL_METHOD_TYPES_CategoryEssenceProtocol [] __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"v16@0:8",
"v16@0:8"
};
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_PROTOCOL_REFS_CategoryEssenceProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSObject
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_CategoryEssenceProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"categoryEssenceProtocolMethod", "v16@0:8", 0}}
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_CLASS_METHODS_CategoryEssenceProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"categoryEssenceProtocolClassMethod", "v16@0:8", 0}}
};
struct _protocol_t _OBJC_PROTOCOL_CategoryEssenceProtocol __attribute__ ((used)) = {
0,
"CategoryEssenceProtocol",
(const struct _protocol_list_t *)&_OBJC_PROTOCOL_REFS_CategoryEssenceProtocol,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_CategoryEssenceProtocol,
(const struct method_list_t *)&_OBJC_PROTOCOL_CLASS_METHODS_CategoryEssenceProtocol,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_CategoryEssenceProtocol
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_CategoryEssenceProtocol = &_OBJC_PROTOCOL_CategoryEssenceProtocol;
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_CategoryEssence_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_CategoryEssenceProtocol
};
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_CategoryEssence;
static struct _category_t _OBJC_$_CATEGORY_CategoryEssence_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"CategoryEssence",
0, // &OBJC_CLASS_$_CategoryEssence,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_CategoryEssence_$_Addition,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_CategoryEssence_$_Addition,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_CategoryEssence_$_Addition,
0,
};
static void OBJC_CATEGORY_SETUP_$_CategoryEssence_$_Addition(void ) {
_OBJC_$_CATEGORY_CategoryEssence_$_Addition.cls = &OBJC_CLASS_$_CategoryEssence;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_CategoryEssence_$_Addition,
};
static struct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
&OBJC_CLASS_$_CategoryEssence,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_CategoryEssence_$_Addition,
};
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
二、Category 的加载方式
我们知道,Objective-C
的运行是依赖 Objective-C
的 Runtime 的,而 Objective-C
的 runtime 和其他系统库一样,是 OS X 和 iOS 通过 dyld 动态加载的。
1. dyld 加载大致流程
dyld(the dynamic link editor) 的相关代码可在苹果开源网站上进行下载:dyld 苹果开源代码
关于 dyld 的详解可移步到 dyld详解
dyld 大致加载步骤:
- 配置环境变量;
- 加载共享缓存;
- 初始化主 APP;
- 插入动态缓存库;
- 链接主程序;
- 链接插入的动态库;
- 初始化主程序:
OC
,C++
全局变量初始化; - 返回主程序入口函数。
2. Category 的加载过程
Runtime 是在在第七步开始初始化的,所以 Category 也在这一步做加载。
我们先看一在主程序初始化时候的调用栈:
dyldbootstrap::start ---> dyld::_main ---> initializeMainExecutable ---> runInitializers ---> recursiveInitialization ---> doInitialization ---> doModInitFunctions ---> _objc_init
最后调用的 _objc_init
是 libobjc
库中的方法, 是 Runtime 的初始化过程,也是 Objective-C
的入口。
在 _objc_init
这一步中:Runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 Runtime 进行处理,Runtime 接手后调用 map_images
做解析和处理,调用 _read_images
方法把 Category 的对象方法、协议、属性添加到类上,把 Category(分类) 的类方法、协议添加到类的 MetaClass
上;接下来 load_images
中调用 call_load_methods
方法,遍历所有加载进来的 Class,按继承层级和编译顺序依次调用 Class 的 load
方法和其 Category 的 load
方法。
加载 Category 的调用栈:
_objc_init ---> map_images ---> map_images_nolock ---> _read_images(加载分类) ---> load_images
关于 Category 的加载过程,在这里我就浅尝即止,主要就了解一下加载过程,有兴趣可以去下载官方源码进行详细阅读分析。
三、Category 和 Class 的 +load 方法
Category 中的的方法、属性、协议附加到类上的操作,是在 + load
方法执行之前进行的。也就是说,在 + load
方法执行之前,类中就已经加载了 Category 中的的方法、属性、协议。
而 Category 和 Class 的 + load
方法的调用顺序规则如下所示:
- 先调用主类,按照编译顺序,顺序地根据继承关系由父类向子类调用;
- 调用完主类,再调用分类,按照编译顺序,依次调用;
+ load
方法除非主动调用,否则只会调用一次。
通过这样的调用规则,我们可以知道:主类的 + load
方法调用一定在分类 + load
方法调用之前。但是分类 + load
方法调用顺序并不是按照继承关系调用的,而是依照编译顺序确定的,这也导致了 + load
方法的调用顺序并不一定确定。一个顺序可能是:
父类 -> 子类 -> 父类分类 -> 子类分类
也可能是
父类 -> 子类 -> 子类分类 -> 父类分类
四、Category 和 Class 的 +initialize 方法
+ initialize
方法会在类第一次接收到消息时调用。
调用顺序:先调用父类的 + initialize
,再调用子类的 + initialize
。 (先初始化父类,再初始化子类,每个类只会初始化 1 次)
+ initialize
和 + load
的很大区别是,+ initialize
是通过objc_msgSend进行调用的,所以有以下特点:
- 如果子类没有实现
+ initialize
,会调用父类的,+ initialize
。(所以父类的+ initialize
可能会被调用多次) - 如果分类实现了
+ initialize
,就覆盖类本身的+ initialize
调用。
五、Caregory 通过关联对象添加成员变量
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
// 分类的结构体中有声明属性存放的结构体指针 properties
const struct _prop_list_t *properties;
};
分类中可以添加属性,会自动生成属性的 set 和 get 方法声明,但是不会生成成员变量以及 set 和 get 方法的实现。不自动生成 set 和 get 方法好解决,直接定义属性的 set 和 get 方法就行了。而让分类生成成员变量就需要借助关联对象的方式。
1. 关联对象方法具体使用
// 1. 通过 key : value 的形式给对象 object 设置关联属性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 2. 通过 key 获取关联的属性 object
id objc_getAssociatedObject(id object, const void *key);
// 3. 移除对象所关联的属性
void objc_removeAssociatedObjects(id object);
给个网络上的例子:
UIImage 分类中增加网络地址属性
/********************* UIImage+Property.h 文件 *********************/
#import <UIKit/UIKit.h>
@interface UIImage (Property)
/* 图片网络地址 */
@property (nonatomic, copy) NSString *urlString;
// 用于清除关联对象
- (void)clearAssociatedObjcet;
@end
/********************* UIImage+Property.m 文件 *********************/
#import "UIImage+Property.h"
#import <objc/runtime.h>
@implementation UIImage (Property)
// set 方法
- (void)setUrlString:(NSString *)urlString {
objc_setAssociatedObject(self, @selector(urlString), urlString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
// get 方法
- (NSString *)urlString {
return objc_getAssociatedObject(self, @selector(urlString));
}
// 清除关联对象
- (void)clearAssociatedObjcet {
objc_removeAssociatedObjects(self);
}
@end
借助关联对象,我们成功的在 UIImage
分类中为 UImage
类增加了 urlString
关联属性,并实现了 getter
、setter
方法。
注意:使用 objc_removeAssociatedObjects
可以断开所有的关联。通常情况下不建议使用,因为它会断开所有的关联。如果想要断开关联可以使用 objc_setAssociatedObject
,将关联对象传入 nil
即可。
2.关于关联对象
关联对象的详解看这篇:iOS开发·runtime原理与实践: 关联对象篇,这里我就不详述了。
功能使用挺便捷的,但不要频繁使用吧,毕竟关联对象是 Runtime 的 API,在运行时去做处理。