Category

先记录几个问题吧。

  1. category 的结构是什么?category 为什么不能添加变量?
  2. category 是怎样加载的?
  3. loadinitialize 的区别?

在此之前,可以下载 rumtime 的源码

category 的结构

objc-runtime-new.h可以找到 category 的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 实例方法表
struct method_list_t *classMethods; // 类方法表
struct protocol_list_t *protocols; // 协议表
struct property_list_t *instanceProperties; // 属性表
// 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);
};

从上述结构可以得知,category 的结构中有实例方法表,类方法表,协议表,属性表,类属性表。并没有变量表,这也是 category 无法直接添加变量的原因。

category 的加载

其实,这个问题需要从位于 objc-os.mm 中的_objc_init方法中开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

重点关注 _dyld_objc_notify_register 传入的三个参数:map_images、load_images、unmap_image。从代码得知,map_images -> map_images_nolock -> _read_images 这样的层层调用关系。
其中,从 _read_images 的方法中,可以得知,这个方法就是读取 class 文件,protocol,以及 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
// 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);
}
}
}
}

通过 _getObjc2CategoryList 获取一个 catlist ,也是存放分类的一个列表,之后进行遍历,查看每个分类文件对应的类是否已经链接,如果缺失该类,那么也会将 catlist 中对应的给置为 nil,然后继续下一个分类文件。接着会把分类注册到对应的类去,然后会重写类的方法列表,其中会先添加实例方法和属性,再进行添加类方法和类属性。一直持续到所有分类文件链接读取完毕。
而重写类的方法列表则是由 remethodizeClass 方法来进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
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);
}
}

从代码可以得知,category 的方法等也是在这里进行加入的,且看 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
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 方法,将他们加入到对应的原数组中。有一个重要的是,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
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]));
}
}

loadinitialize 的区别

举个🌰吧。

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
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SuperClass : NSObject
@end
NS_ASSUME_NONNULL_END
#import "SuperClass.h"
@implementation SuperClass
+ (void)initialize {
NSLog(@"%s this class is %@", __func__, NSStringFromClass(self));
}
+ (void)load {
NSLog(@"%s this class is %@", __func__, NSStringFromClass(self));
}
@end
#import "SuperClass.h"
NS_ASSUME_NONNULL_BEGIN
@interface SubClass : SuperClass
@end
NS_ASSUME_NONNULL_END
#import "SubClass.h"
@implementation SubClass
@end
.....
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
SubClass *subClass1 = SubClass.new;
SubClass *subClass2 = SubClass.new;
}
@end

先说下 load 方法是怎样一个调用的过程。
在运行时,还是会从 _objc_init 方法开始。为什么会是从这里开始呢。其实,我们在+load中打上断点。那么在运行过程中,可以看到其是这样的一个顺序。load_images - call_load_methods - load 。将前两者在 runtime 源码中搜索,可以得到对应的方法。load_images 位于objc-runtime-new.mm;call_load_methods 位于objc-loadmethod.mm。而 load_images 恰恰是在 _objc_init 内部进行调用。

1

再者,也可以加上一个 symbolic 断点。

2

在运行时可以得知,load_images 是在 _objc_init 之后调用的。

3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}

load_images 会先通过调用 prepare_load_methods 方法来决定一些所需加载方法的数据。比方其内部会通过 add_category_to_loadable_list 方法来把需要加载的类的一些相关数据准备好,如 loadable_categories 等。
之后,才是调用 call_load_methods 方法来进行方法的加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}

load 方法实则就是 call_load_methods 来进行的。在这个方法中,会注意到 call_category_loads() 的方法调用,这里也就是我们在写 category 时,多个 load 方法存在时进行调用的地方了,并且只加载一次。

以上大概就是 load 加载的过程。那么 initialize 又是怎样的呢?

在父类的 initialize 方法中打上断点。可以知道其实际上是通过 _class_initialize 方法来实现。该方法位于 objc-initialize.mm 文件中。也可以知道,initialize 方法是在初始化才进行调用。

4

并且可以从 _class_initialize 的源码中发现,initialize 是通过 objc_msgSend 来进行调用的。

因此,load 和 initialize 的区别主要是:

  1. 前者是运行时就进行调用,并且是发生在 main 函数调用前;而后者是在其初始化时才进行调用,并且实则是经过 objc_msgSend 进行消息发送的
  2. 前者只调用一次;后者则是每初始化一次就调用一次
  3. 前者可以多个 load 进行调用;但是后者根据上一节的描述,会发现在 category 中重写 initialize 方法则会进行覆盖

参考

  1. iOS 程序 main 函数之前发生了什么
  2. 深入理解Objective-C:Category
  3. NSObject +load and +initialize - What do they do?