本文章参考自:
iOS 开发:『Runtime』详解(三)Category 底层原理
iOS Category源码探究
本文章不做任何商业用途。优秀的作品要大家一起欣赏,如有疑问请联系删除。
本文用来介绍iOS开发中「Runtime」中的 ·Category· 底层原理。通过本文您将了解到:
- Category(分类)简介
- Category 的实质
- Category 的加载过程
- Category(分类)和 Class(类)的
+load
方法 - Category 与关联对象
1、Category(分类)简介
1.1 什么是 Category(分类)?
Category(分类) 是Objective-C 2.0 添加的语言特性,主要作用是为已经存在的类添加方法。Category 可以做到在即不子类化,也不侵入一个类的源码的情况下,为原有的类添加新的方法,从而实现扩展一个类或者分离一个类的目的。在日常开发中,我们经常使用Category为已有的类扩展功能。
虽然继承也能为已有类增加新的方法,而且还能直接增加属性,但是继承增加了不必要的代码复杂度,在运行时,也无法与父类的原始方法进行区分。所以我们可以优先考虑使用自定义的Category(分类)。
通常Category(分类)有以下几种使用场景:
- 把类的不同实现方法分开到不同的文件里。
- 声明私有方法。
- 模拟多继承。
- 将
framework
私有方法公开化。
1.2 Category(分类)和 Extension(扩展)
Category(分类)看起来和 Extension(扩展)有点相似。Extension(扩展)有时候也被称为匿名分类。但两者实质上是不同的东西。Extension(扩展)是在编译阶段与该类同时编译的,是类的一部分。而且Extension(扩展)中声明的方法只能在该类的@implementation
中实现,这也就意味着,你无法对系统的类(例如NSArray
类)使用Extension(扩展)。
而且和Category(分类)不同的是,Extension(扩展)不但可以声明方法,还可以声明成员变量,这是Category(分类)所做不到的。
为什么Category(分类)不能像Extension(扩展)一样添加成员变量?
因为Extension(扩展)是在编译阶段与该类同时编译的,就是类的一部分。既然作为类的一部分,且与类同时编译,那么就可以在编译阶段为类添加成员变量。
而Category(分类)则不同,
Category
的特性是:可以在运行时阶段动态的为已有类添加新行为。Category
是在运行时阶段决定的。而成员变量的内存布局已经在编译阶段确定好了,如果在运行时阶段添加成员变量的话,就会破坏原有类的内存布局,从而造成可怕的后果,所以Categoty
无法添加成员变量。
2、Category 的实质
2.1 Category 结构体简介
在iOS底层探索 ---Runtime(一)--- 基础知识中我们知道了:Object (对象)
和Class (类)
的实质分别是objc_object 结构体
和objc_class 结构体
,这里Category
也不例外,在objc-runtime-new.h
中,Category(分类)被定义为category_t 结构体
。category_t 结构体
的数据结构如下:
struct category_t {
const char *name; /// 类名
classref_t cls; /// 类,在运行时阶段通过 class_name(类名) 对应到类对象
WrappedPtr instanceMethods; /// Category 中所有添加的对象方法列表
WrappedPtr classMethods; /// Category 中所有添加的类方法列表
struct protocol_list_t *protocols; /// Category 中实现的所有协议列表
struct property_list_t *instanceProperties; /// Category 中添加的所有属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
从Category(分类)的结构体定义中也可以看出,Category(分类)可以为类添加对象方法
、类方法
、协议
、属性
。同时,也能发现Category(分类)无法添加成员变量
。
2.2 Category 的 C++ 源码
想要了解Category
的本质,我们需要借助于Category
的C++
源码。这种方式我们在探索Block
的时候已经使用过了。有兴趣的同学可以参考:Block 底层原理(一)
首先我们建立一个Person
类,并添加一个分类Person+Additon
;在分类中添加对象方法
、类方法
、属性
、代理
。
代码如下:
/******************** Person+Addition.h ********************/
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@protocol PersonProtocol
- (void)PersonProtocolMethod;
+ (void)PersonProtocolClassMethod;
@end
@interface Person (Addition)
/* name 属性 */
@property (nonatomic, copy) NSString *personName;
// 类方法
+ (void)printClassName;
// 对象方法
- (void)printName;
@end
NS_ASSUME_NONNULL_END
/******************** Person+Addition.m ********************/
#import "Person+Addition.h"
@implementation Person (Addition)
+ (void)printClassName {
NSLog(@"printClassName");
}
- (void)printName {
NSLog(@"printName");
}
#pragma mark - PersonProtocol 方法
- (void)PersonProtocolMethod {
NSLog(@"PersonProtocolMethod");
}
+ (void)PersonProtocolClassMethod {
NSLog(@"PersonProtocolClassMethod");
}
@end
在终端执行以下指令,生成
.CPP
文件:
clang -rewrite-objc Person+Addition.m
下面我们就要分析以下生成的Person+Addition.cpp
文件
这里有一个小技巧,生成的代码会有很多,一般从最后一行往前找。
2.2.1 Category 结构体
// Person 类的 Category 结构体
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;
const struct _prop_list_t *properties;
};
// Person 类的 Category 结构体赋值
static struct _category_t _OBJC_$_CATEGORY_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Addition,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Addition,
};
// Category 数组,如果 Person 有多个分类,则 Category 数组中对应多个 Category
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Person_$_Addition,
};
从
Category 结构体
源码中我们可以看到:
- Category 结构体。
- Category 结构体的赋值语句。
- Category 结构体数组
第一个
Category 结构体
和2.1 Category 结构体简介中的结构体是一一对应的。可以看做是同一个结构体。第三个Category 结构体数组
中存放了Person 类
的相关分类,如果有多个分类,则数组中存放对应数目的Category 结构体
。
2.2.2 Category 中的 「对象方法列表结构体」
// - (void)printName; 对象方法的实现
static void _I_Person_Addition_printName(Person * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jb_6hgmfgfx44s5vlhxxjmrcxqh0000gn_T_Person_Addition_e519a1_mi_1);
}
// - (void)PersonProtocolMethod; 方法的实现
static void _I_Person_Addition_PersonProtocolMethod(Person * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jb_6hgmfgfx44s5vlhxxjmrcxqh0000gn_T_Person_Addition_e519a1_mi_2);
}
// Person 分类中添加 「对象方法列表结构体」
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_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_Person_Addition_printName},
{(struct objc_selector *)"PersonProtocolMethod", "v16@0:8", (void *)_I_Person_Addition_PersonProtocolMethod}}
};
从「对象方法列表结构体」源码中我们可以看到:
-
- (void)printName;
对象方法的实现。 -
- (void)PersonProtocolMethod;
方法的实现。 - 对象方法列表结构体
只要是
Category
中实现了的对象方法(包括代理方法中的对象方法)。都会添加到 对象方法列表结构体_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition
中来。如果只是在Person.h
中定义,而没有实现,则不会添加。
2.2.3 Category 中的 「类方法列表结构体」
// + (void)printClassName; 类方法的实现
static void _C_Person_Addition_printClassName(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jb_6hgmfgfx44s5vlhxxjmrcxqh0000gn_T_Person_Addition_e519a1_mi_0);
}
// + (void)PersonProtocolClassMethod; 方法的实现
static void _C_Person_Addition_PersonProtocolClassMethod(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jb_6hgmfgfx44s5vlhxxjmrcxqh0000gn_T_Person_Addition_e519a1_mi_3);
}
// Person 分类中添加 「类方法列表结构体」
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_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"printClassName", "v16@0:8", (void *)_C_Person_Addition_printClassName},
{(struct objc_selector *)"PersonProtocolClassMethod", "v16@0:8", (void *)_C_Person_Addition_PersonProtocolClassMethod}}
};
从「类方法列表结构体」源码中我们可以看到:
-
+ (void)printClassName;
类方法的实现。 -
+ (void)PersonProtocolClassMethod;
类方法的实现。 - 类方法列表结构体
只要是
Category
中实现了的类方法(包括代理中的类方法)。都会添加到类方法列表结构体_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition
中来。如果只是下Person.h
中定义,而没有实现,则不会添加。
2.4 Category 中「协议列表结构体」
// Person 分类中添加 「协议列表结构体」
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_PROTOCOL_REFS_PersonProtocol __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_PersonProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"PersonProtocolMethod", "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_PersonProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"PersonProtocolClassMethod", "v16@0:8", 0}}
};
// PersonProtocol 结构体赋值
struct _protocol_t _OBJC_PROTOCOL_PersonProtocol __attribute__ ((used)) = {
0,
"PersonProtocol",
(const struct _protocol_list_t *)&_OBJC_PROTOCOL_REFS_PersonProtocol,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_PersonProtocol,
(const struct method_list_t *)&_OBJC_PROTOCOL_CLASS_METHODS_PersonProtocol,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_PersonProtocol
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_PersonProtocol = &_OBJC_PROTOCOL_PersonProtocol;
从「协议列表结构体」源码中我们可以看到:
- 协议列表结构体。
- 协议列表 对象方法列表结构体。
- 协议列表 *类方法列表结构体。
-
PersonProtocol
协议结构体赋值语句。
2.2.5 Category 中 「属性列表结构体」
// Person 分类中添加的属性列表
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"personName","T@\"NSString\",C,N"}}
};
从「属性列表结构体」源码中我们可以看到:
只有
Person
分类中添加的属性列表结构体_OBJC_$_PROP_LIST_Person_$_Addition
,没有成员变量结构体_ivar_list_t 结构体
。更没有对应的set 方法 / get 方法
相关内容。这也直接说明了Category
中不能添加成员变量这一事实。
2.3 Category 的实质总结
Category
的本质是_category_t 结构体
,其中包含以下几个部分:
-
_method_list_t
类型的「对象方法列表结构体」; -
_method_list_t
类型的「类方法列表结构体」; -
_protocol_list_t
类型的「协议列表结构体」; -
_prop_list_t
类型的「属性列表结构体」。
_category_t 结构体
中不包含_ivar_list_t
类型,也就是不包含「成员变量结构体」。
3 Category 的加载过程
3.1 dyld的加载流程
我们在iOS底层探索 --- dyld加载流程中详细的探索了dyld
的加载流程;而我们之前也说过Category (分类)
是在运行时阶段动态加载的;而Runtime (运行时)
加载的过程离不开我们的dyld
。
下面我们就来简单回顾一下dyld
的加载流程:
[图片上传失败...(image-193f0f-1622199142993)]
在这里,我们只需要关系执行初始化方法这一步。我们再来回顾一下_objc_init
方法:
不知道大家还记不记得,我们再探索
lldb
加载流程的时候,探索到_objc_init
方法的时候,找到了关键的一段代码:
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
当时我们探索了第二个参数load_images
;而我们的Category
的加载流程就藏在第一个参数map_images
里面。按照我们之前的探索方式,持续的跟进,我们会发现这样一条函数调用栈:
_objc_init --> map_images --> map_images_nolock --> _read_images
到这里的时候,我并没有发现关于Category
的代码,于是搜索了一下,在_read_images
方法里面发现了这个:
那么就进入
load_categories_nolock
看一下,突然觉得找对了地方,里面有很多关于关于Category
的内容,下面我们一起来探索一下:
static void load_categories_nolock(header_info *hi) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
size_t count;
auto processCatlist = [&](category_t * const *catlist) {
for (unsigned i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
locstamped_category_t lc{cat, hi};
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Ignore the category.
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
if (cls->isStubClass()) {
// Stub classes are never realized. Stub classes
// don't know their metaclass until they're
// initialized, so we have to add categories with
// class methods or properties to the stub itself.
// methodizeClass() will find them and add them to
// the metaclass as appropriate.
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
}
};
processCatlist(hi->catlist(&count));
processCatlist(hi->catlist2(&count));
}
这里看到attachCategories
这个函数,跟进去(注意看官方注释,这个很重要,顿时豁然开朗,这就是要找的核心代码呀!!!):
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/
/// 创建方法列表、属性列表、协议列表,用来存储分类的方法,属性,协议
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0; /// 记录方法的数量
uint32_t propcount = 0; /// 记录属性的数量
uint32_t protocount = 0; /// 记录协议的数量
bool fromBundle = NO; /// 记录是否是从 bundle 中提取的
bool isMeta = (flags & ATTACH_METACLASS);
/// auto是C++的语法,表示 类型根据代码自动推断
/// cls->data()->extAllocIfNeeded() 返回的就是 'class_rw_ext_t *' 类型
/// class_rw_ext_t 中存放有 '方法'、'属性'、'协议'等信息。('extAllocIfNeeded' 跟进去就可以看到)
/*
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
*/
auto rwe = cls->data()->extAllocIfNeeded();
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i]; ///取出当前分类;entry -> locstamped_category_t
method_list_t *mlist = entry.cat->methodsForMeta(isMeta); ///取出分类中的方法
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
/// 根据上面可以知道 ‘ATTACH_BUFSIZ = 64’
/// 也就是说这句话等于 ‘mlists[64 - ++mcount] = mlist;’
/// 倒叙合并分类的 mlists, 后面会将 mlists 再合并到类的方法列表中
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
/// 分类的头部信息中储存了是否是 bundle,将其记录
fromBundle |= entry.hi->isBundle();
}
/// 取出分类中的属性列表,如果是元类,取得的是 nil
/*
property_list_t *
category_t::propertiesForMeta(bool isMeta, struct header_info *hi)
{
if (!isMeta) return instanceProperties;
else if (hi->info()->hasCategoryClassProperties()) return _classProperties;
else return nil;
}
*/
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
///取出分类中遵循的协议列表
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
///将 mlists 合并到 rwe->methods
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__, [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
看上面代码的最后两行,可以看到协议和属性调用了同一个方法attachLists
:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
///根据下面的代码,可以看出来,分类中的方法会被添加到原有方法的前面。
///在方法查询的时候,分类中的方法会先被查到并返回。
///如果分类中定义了和原类中一样的方法,那么只会执行分类中的方法。
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
这里有一点很重要,注意看我在
attachLists
中的注释。不过我还是要再强调一遍:
分类中的方法会被添加到原有方法的前面。
在方法查询的时候,分类中的方法会先被查到并返回。
如果分类中定义了和原类中一样的方法,那么只会执行分类中的方法。
由此可见,
Category
在运行时被加载的流程如下:
_objc_init --> map_images --> map_images_nolock --> _read_images --> attachCategories --> attachLists
4 Category(分类)和 Class(类)的+load
方法
Category(分类)中的方法
、属性
、协议
附加到类上面的操作,是在+load
方法执行之前进行的。也就是说+load
方法执行之前,类中就已经加载了Category(分类)中的方法
、属性
、协议
。
而 Category(分类)和 Class(类)的+load
方法的调用顺序规则如下所示:
- 先调用主类,按照编译顺序,依次根据继承关系,由父类向子类调用;
- 调用完主类,再调用分类,按照编译顺序,依次调用;
-
+load
方法除非主动调用,否则只会调用一次。
通过这样的调用规则,我们可以知道:主类的+load
方法一定在分类+load
方法之前调用。但是分类的+load
方法调用顺序并不是按照继承关系调用的,而是按照编译顺序确定的,这一导致了+load
方法的调用顺序并不确定。
可能是:父类 -> 子类 -> 父类分类 -> 子类分类
;
也可能是:父类 -> 子类 -> 子类分类 -> 父类分类
。
5 Category 与 关联对象
上面我们说过,在Category
中虽然可以添加属性,但是不会生成对应的成员变量,也不能生成getter
、setter
方法。因此调用Category
中声明的属性时会报错。
那么有什么办法可以解决这个问题吗?还记得我们在iOS底层探索 --- Runtime(二)Method Swizzling使用到的关联对象吗?这就是一个很好的解决办法。下面我们一起来回顾一下:
我们可以自己来实现getter
、setter
方法,并借助关联对象(Objcticve-C Associated Objects
)来实现getter
、settter
方法。关联对象能够帮助我们在运行时阶段将任意的属性关联到一个对象上。具体要用到方法如下:
///1、通过 key : value 的形式给对象 object 设置关联属性
void objc_setAssociatedObject(id _Nonnull NSObject, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy);
///2、通过 key 获取关联的属性 object
id objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
///3、移除对象所关联的属性
void objc_removeAssociatedObjects(id _Nonnull object);
详细内容可以参考:关联对象 AssociatedObject 完全解析
5.1 UIImage 分类中增加网络地址属性
/****************** UIImage+Property.h ******************/
#import
NS_ASSUME_NONNULL_BEGIN
@interface UIImage (Property)
///图片网络地址
@property (nonatomic, copy) NSString *urlString;
///用于清除关联对象
- (void)clearAssociatedObject;
@end
NS_ASSUME_NONNULL_END
/****************** UIImage+Property.m ******************/
#import "UIImage+Property.h"
#import
@implementation UIImage (Property)
/// set 方法
- (void)setUrlString:(NSString *)urlString {
objc_setAssociatedObject(self, @selector(urlString), urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/// get方法
- (NSString *)urlString {
return objc_getAssociatedObject(self, @selector(urlString));
}
/// 清除关联对象
- (void)clearAssociatedObject {
objc_removeAssociatedObjects(self);
}
@end
⚠️ 注意:使用
objc_removeAssociatedObjects
可以断开所有的关联。通常情况下不建议使用,因为它会断开所有的关联。如果想要断开关联,可以使用objc_setAssociatedObject
,将关联对象设置成nil
。
比如我们上面的清除关联对象可以这样写:objc_setAssociatedObject(self, @selector(urlString), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
。