OC分类原理

Category分类原理

分类的结构

每个分类文件编译后产生的结构体 _category_t 都是独立的,以下为分类的结构体

1
2
3
4
5
6
7
8
9
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 _prot_list_t *properties;
};
instance_methods class_methods protocols properties
实例方法 类方法 遵守的协议 属性列表

分类的合并

分类中的实例方法和类方法会通过运行时机制 Runtime 分别合并到类对象和元类对象中

打开 obje-os.mm 源码进行探究

找寻的顺序依次为 _objc_init() -> map_images() -> map_images_nolock() -> _read_images() -> remethodizeClass() -> 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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{//cls 类对象 cats 分类列表
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);

bool isMeta = cls->isMetaClass();

// fixme rearrange to remove these intermediate allocations
//方法列表数组
/*
[
[method_t, method_t],
[method_t, method_t]
]
*/
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);
}


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;

//将原来内存的位置的数据挪动后面,array()->lists为原来的方法列表
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));

//拷贝将新的数据,从内存首端开始
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
  • old_method 为原类方法所占用的内存,new_malloc 为新申请的内存区域
  • 然后 old_method 复制到所申请新的内存区域的最末端
  • 最后添加分类的方法 new_method ,从整个方法的内存从首端开始覆盖
  • 编译顺序越靠后的分类方法的优先级越高

所以在调用实例对象的方法时,会优先调用分类中的实例对象方法,具体的分类顺序需要在 XCode 中设置分类文件的编译顺序,在 Project-BuildPhases-Compile Sources 中进行设置

分类中的load方法和initialize方法

load方法

load() 方法会在runtime加载 分类 时调用,无论这些 分类 是否包含于 main() 函数的文件中或包含于其他的类文件中,它们各自的load() 方法都会在内存中被执行,且 load() 方法不会通过消息的发送机制进行调用。

load方法的调用顺序

分类中的load方法

和分类中的实例方法 相反load() 方法的内存位置并不会被后编译的 分类 中的 load() 所覆盖,只与 分类 文件的编译顺序有关,编译越靠前的分类越优先调用。

继承中的load方法

先查找到其源码,在 objc-runtime-new.mm 中的prepare_load_methods() 方法中调用了schedule_class_load() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize

if (cls->data()->flags & RW_LOADED) return;

// Ensure superclass-first ordering
schedule_class_load(cls->superclass);

add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}

其中有一个递归的调用,传入的参数为cls->superclass,且注释说明其用意是确保父类最优先调用。

initialize方法

仅在类第一次接收消息的时候调用 initialize() 方法,如果分类中实现了 initialize() ,会直接覆盖本身的initialize()方法。

打开源码,在 objc-runtime-new.mm 文件中

1
2
3
4
5
if (initialize  &&  !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}

其中对类是否初始化进行了判断,如果没有初始化则进入 _class_initialize() 方法。

于是找到方法所在的文件 objc-initialize.mm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _class_initialize(Class cls)
{
assert(!cls->isMetaClass());

Class supercls;
bool reallyInitialize = NO;

// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}

// Try to atomically set CLS_INITIALIZING.
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
cls->setInitializing();
reallyInitialize = YES;
}
}
}

if (supercls && !supercls->isInitialized()) 会对传入的父类先进行 initialize 的判断,然后再对子类进行 initialize

所以如果是继承关系的类,在子类第一次接收消息的时候会先调用父类的 initialize() 方法,然后再调用子类的 initialize() 方法。

总结

  • 调用方式

load() 根据函数地址直接调用

initialize() 是通过 objc_msgSend() 调用

  • 调用时刻

    load() 是运行时在加载类,分类的时候调用(只会调用1次)

    initialize() 是类第一次接收到消息的时候调用,每一个类只会使用 initialize() 初始化一次,但父类的 initialize() 可能会被调用多次

  • 调用顺序

    load

    1. 先调用类的 load()
    • 先编译的类,优先调用 load()
    • 调用子类的 load() 之前,会先调用父类的 load()
    1. 再调用分类的 load()
    • 先编译的分类,优先调用 load()

      initialize

    1. 先初始化父类
    2. 再初始化子类(可能最终调用的是父类的initialize方法)