发布于 

Category 实现原理

本篇是整理之前的笔记,梳理下在 runtime 层面是怎么处理的 Category。

分析源码基于:DeveloperErenLiu/RuntimeAnalyze/objc4-750

Category 的数据结构

Category 在底层是一个结构体,如下:

1
2
3
4
5
6
7
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

看到 Category 中有:

  • 实例方法列表
  • 类方法列表
  • 协议列表
  • 属性列表

大致可以猜测出 Category 可以做的有:

  • 添加实例方法
  • 添加类方法
  • 实现协议
  • 添加属性

我们知道的是:Category 不支持添加实例变量。原因是,类对象在编译时内存布局就已经确定,而 Categroy 的工作是在 Runtime 时生效,这时如果添加实例方法会破坏内存布局。

Category 的编译

编译器会在编译时将 OC 对象编译为 Runtime 需要的结构体,Category 也会这样。我们 Cland 一下,看下结果。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// FFClass
@interface FFClass : NSObject
@end

@implementation FFClass
@end

// FFClass+FFCategory
@interface FFClass (FFCategory)<NSCopying>
@property (nonatomic, strong) FFClass *prop;
- (void)hello;
@end

@implementation FFClass (FFCategory)
- (void)hello {
//
}
@end

执行如下命令:

1
xcrun -sdk iphonesimulator clang -rewrite-objc FFClass.m

得到编译以后的 cpp 文件,会看到:

1
2
3
4
5
6
7
8
9
static struct _category_t _OBJC_$_CATEGORY_FFClass_$_FFCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
"FFClass",
0, // &OBJC_CLASS_$_FFClass,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_FFClass_$_FFCategory,
0,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_FFClass_$_FFCategory,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_FFClass_$_FFCategory,
};

对应查找看到 _method_list_t_protocol_list_t_prop_list_t,这些即是我们的方法列表、属性列表和协议列表。对应的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//方法
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_FFClass_$_FFCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"hello"}}
};
//协议
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_FFClass_$_FFCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};

//属性
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_FFClass_$_FFCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
prop
};

到这里编译后的内容即如上代码。

Runtime 的处理

苹果的动态链接器(dyld)在启动 App 时,会进入 runtime 的初始化,调用 _objc_init()。其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;

// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

看到进行了一系列的初始化,如环境初始化、static 初始、锁初始化等。官方的注释:

1
dyld will call the "mapped" function with already loaded objc images.

mapped 即这里的 map_images,进入 map_images 看下实现。

1
2
3
4
5
6
7
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}

跟着方法调用,进入下一步 map_images_nolock,这里大概进行的通过 Objective-C metadata 找到所有的 images,最后判断 images 数量是否 >0,若 >0 调用 **_read_images()**。在 _read_images() 源码里,我们提取出 Category 相关的处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
// 1. 初始化工作、判断回收机制
// 2. 一些针对 OSX 的处理
// 3. 获取所有的类
// 4. 建立 hash,建立类名和类的映射
// 5. 注册 SEL
// 6. 加载 protocol
// 7. 实现 class 在 runtime 生成类对象
// 8. Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();

for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);

if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}

// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}

if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}

ts.log("IMAGE TIMES: discover categories");

// Category discovery MUST BE LAST to avoid potential races
// when other threads call the new category code before
// this thread finishes its fixups.

// ...
}

根据源码分步来看。

获取 catlist

通过 _getObjc2CategoryList 获取编译后的 Category 集合。

1
2
category_t **catlist = 
_getObjc2CategoryList(hi, &count);

判断 Category 对应的类是否存在

前面已经拿到了 catlist,接着遍历 catlist,通过 remapClass 在类的 hash 表中查找这个类是否存在,如果不存在,直接处理下一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);

if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}

实例方法、协议、属性的处理

在找到了 Category 对应的类之后,接着判断 Category 的实例方法、协议、属性是否存在。如果存在,则绑定这个 Category 到类中,之后生成新的类信息,将方法列表、属性列表、协议列表合并。最后输出 log。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}

我们看到了两个方法 addUnattachedCategoryForClassremethodizeClassaddUnattachedCategoryForClass 是将未绑定的 Category 映射到 class 上。该方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
header_info *catHeader)
{
runtimeLock.assertLocked();

// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;

list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}

实现过程:

  • 在 map 中查找执行 class 的 catlist。如果找到,将 cat 加到 catlist 中;如果没找到,通过 cat 新建一个 catlist。

remethodizeClass 的作用是重新生成类的实例方法列表、协议列表、属性列表。对应源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;

runtimeLock.assertLocked();

isMeta = cls->isMetaClass();

// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}

attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}

实现过程:

  • 通过 cls 获取它的 catlist,使用了 unattachedCategoriesForClass 方法。
  • 之后通过 attachCategories 将 cats 合并到 cls 中

attachCategories 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);

bool isMeta = cls->isMetaClass();

// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));

// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];

method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}

property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}

protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}

auto rw = cls->data();

prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);

rw->properties.attachLists(proplists, propcount);
free(proplists);

rw->protocols.attachLists(protolists, protocount);
free(protolists);
}

实现过程:

  • 获取 Category 对应的方法列表、属性列表、协议列表
  • 之后通过 attachLists 来合并这些项

attachLists 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* 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;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}

添加列表时,先基于旧容量根据添加数扩容,得到新的内存空间。之后使用 memmove 将旧的项移到列表后面,使用 memcpy 将新的项放到列表前面。这也解释了为什么 Category 方法会覆盖类的方法。因为方法如果同名,Category 的方法一定在前面,会被先找到,如果找到了方法 runtime 不会再继续查找。

处理类方法和协议

1
2
3
4
5
6
7
8
9
10
11
12
if (cat->classMethods  ||  cat->protocols  
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}

和实例方法一样,调用 addUnattachedCategoryForClassremethodizeClass,通过 cls->ISA()

总结

  • Category 会在编译阶段编译成 category_t 结构体。
  • runtime 会将 class 和 category 做一个映射。
  • Category 的方法总会在 class 的方法之前,所以会覆盖 class 的方法。但方法是保留的。
  • Category 可以处理实例方法、协议、属性、类方法,但不能处理实例变量

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站由 @JonyFang 创建,使用 Stellar 作为主题,您可以在 GitHub 找到本站源码。