在平日编程中或阅读第三方代码时,category
可以说是无处不在。category
也可以说是OC作为一门动态语言的一大特色。category
为我们动态扩展类的功能提供了可能,或者我们也可以把一个庞大的类进行功能分解,按照category
进行组织。
关于category
的使用无需多言,今天我们来深入了解一下,category
是如何在runtime中实现的。
category的数据结构
category对应到runtime中的结构体是struct category_t(位于objc-runtime-new.h):
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_t
的定义很简单。从定义中看出,category
的可为:添加实例方法(instanceMethods
),类方法(classMethods
),协议(protocols
)和实例属性(instanceProperties
),以及不可为:不能够添加实例变量(关于实例属性和实例变量的区别,我们将会在别的章节中探讨)。
category的加载
知道了category
的数据结构,我们来深入探究一下category
是如何在runtime
中实现的。
原理很简单:runtime
会分别将category
结构体中的instanceMethods
, protocols
,instanceProperties
添加到target class
的实例方法列表,协议列表,属性列表中,会将category
结构体中的classMethods
添加到target class
所对应的元类
的实例方法列表中。其本质就相当于runtime
在运行时期,修改了target class
的结构。
经过这一番修改,category
中的方法,就变成了target class
方法列表中的一部分,其调用方式也就一模一样啦~
现在,就来看一下具体是怎么实现的。
首先,我们在Mach-O格式和runtime 介绍过在Mach-O文件中,category数据会被存放在__DATA段下的__objc_catlist section中。
当OC被dyld加载起来时,OC进入其入口点函数_objc_init
:
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);
}
我们忽略一堆init方法,重点来看_dyld_objc_notify_register
方法。该方法会向dyld注册监听Mach-O中OC相关section被加载入\载出内存的事件。
具体有三个事件:
_dyld_objc_notify_mapped
(对应&map_images回调):当dyld已将images加载入内存时。
_dyld_objc_notify_init
(对应load_images回调):当dyld初始化image后。OC调用类的+load方法,就是在这时进行的。
_dyld_objc_notify_unmapped
(对应unmap_image回调):当dyld将images移除内存时。
而category写入target class的方法列表,则是在_dyld_objc_notify_mapped
,即将Mach-O相关sections都加载到内存之后所发生的。
我们可以看到其对应回调为map_images
方法。
在map_images
最终会调用_read_images
方法来读取OC相关sections,并以此来初始化OC内存环境。_read_images
的极简实现版如下,可以看到,rumtime是如何根据Mach-O各个section的信息来初始化其自身的:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
static bool doneOnce;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertWriting();
if (!doneOnce) {
doneOnce = YES;
ts.log("IMAGE TIMES: first time tasks");
}
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
for (EACH_HEADER) {
classref_t *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
}
}
ts.log("IMAGE TIMES: discover classes");
// Fix up remapped classes
// Class list and nonlazy class list remain unremapped.
// Class refs and super refs are remapped for message dispatching.
for (EACH_HEADER) {
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
// fixme why doesn't test future1 catch the absence of this?
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
}
ts.log("IMAGE TIMES: remap classes");
for (EACH_HEADER) {
if (hi->isPreoptimized()) continue;
bool isBundle = hi->isBundle();
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
ts.log("IMAGE TIMES: fix up selector references");
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
ts.log("IMAGE TIMES: d