Loading

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 结构体 类型,其中包含了以下几部分:

  1. _method_list_t 类型的对象方法列表结构体;
  2. _method_list_t 类型的类方法列表结构体;
  3. _protocol_list_t 类型的协议列表结构体;
  4. _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 大致加载步骤:

  1. 配置环境变量;
  2. 加载共享缓存;
  3. 初始化主 APP;
  4. 插入动态缓存库;
  5. 链接主程序;
  6. 链接插入的动态库;
  7. 初始化主程序:OC, C++ 全局变量初始化;
  8. 返回主程序入口函数。

2. Category 的加载过程

Runtime 是在在第七步开始初始化的,所以 Category 也在这一步做加载。

我们先看一在主程序初始化时候的调用栈:

dyldbootstrap::start ---> dyld::_main ---> initializeMainExecutable ---> runInitializers ---> recursiveInitialization ---> doInitialization ---> doModInitFunctions ---> _objc_init

最后调用的 _objc_initlibobjc 库中的方法, 是 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 方法的调用顺序规则如下所示:

  1. 先调用主类,按照编译顺序,顺序地根据继承关系由父类向子类调用;
  2. 调用完主类,再调用分类,按照编译顺序,依次调用;
  3. + 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 关联属性,并实现了 gettersetter 方法。
注意:使用 objc_removeAssociatedObjects 可以断开所有的关联。通常情况下不建议使用,因为它会断开所有的关联。如果想要断开关联可以使用 objc_setAssociatedObject,将关联对象传入 nil 即可。

2.关于关联对象

关联对象的详解看这篇:iOS开发·runtime原理与实践: 关联对象篇,这里我就不详述了。
功能使用挺便捷的,但不要频繁使用吧,毕竟关联对象是 Runtime 的 API,在运行时去做处理。

参考文档

  1. 《Cocoa Core Competencies》 苹果官方文档

  2. 《iOS 开发:『Runtime』详解(三)Category》底层原理 行走少年郎

  3. 《深入理解Objective-C:Category》美团技术团队

posted @ 2021-03-27 01:22  QiuZH's  阅读(472)  评论(0编辑  收藏  举报