摘要
Category 主要作用是为已有的类,添加方法、属性、协议。
其实现原理,一方面,在编译时期,会生成 category_t 及相关结构体。
另一方面,在运行时期,会将这些方法、属性、协议添加到类之中。
category_t 结构
// objc4-750.1/runtime/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);
};
比较特殊地,它并非继承 objc_object。
而且,当中并没有 ivars,这也解释了,为什么 Category 不能添加变量。
插个题外话,其中的 _classProperties 比较少见,查了下,才发现是为配合 Swift,Xcode8 推出的新特性。
语法如下:
@property (class) NSString *someString;
编译时期
- 编译时期,不同 Category,会被编译成不同的 category_t 结构体。
- 而 Category 中的方法、属性、协议等列表,也会被编译成不同的结构体,存于 category_t 之中。
- Category 的相关信息最终会在 Mach-O 中有所记录。
具体的,先来个简单代码,看看会编译成什么。
@implementation Cat (JD)
- (void)run {
NSLog(@"run");
}
@end
转化成 c++ 代码
clang -x objective-c -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 Cat+JD.m
可看到 run 方法被转换成以下静态函数
// @implementation Cat (JD)
static void _I_Cat_JD_run(Cat * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_mv_t42fx9pj0mn9k96m8y1rwwd40000gn_T_Cat_JD_0bdbd6_mi_0);
}
// @end
创建了名为 _OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD
的 _method_list_t
。
并与上面的静态函数关联起来。
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_Cat_$_JD __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Cat_JD_run}}
};
创建名为 _OBJC_$_CATEGORY_Cat_$_JD
的 _category_t
(即 category_t) 结构体。
static struct _category_t _OBJC_$_CATEGORY_Cat_$_JD __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Cat",
0, // &OBJC_CLASS_$_Cat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD,
0,
0,
0,
};
与原来的类关联
static void OBJC_CATEGORY_SETUP_$_Cat_$_JD(void) {
_OBJC_$_CATEGORY_Cat_$_JD.cls = &OBJC_CLASS_$_Cat;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_Cat_$_JD,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Cat_$_JD,
};
源码中有不少 section ("__DATA,__objc_const")
这样的代码,其实就是 Mach-O 的相关存储位置,会在加载时用到。
运行时期
Objc 初始化时,从 Mach-O 中拿到编译时期产生的 category_t 数组。
再做了以下操作:
- 将 category_t 中的实例属性、实例方法、协议添加到 class 之中。
- 将类属性、类方法、协议添加到 metaClass 中。
添加之后, Category 方法、属性、协议都会在原列表的前面。
来看看具体实现。
_read_images()
从 ObjC 初始化方法 _objc_init()
开始,会调用 _read_images()
。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
...
// _objc_init->map_images->map_images_nolock->_read_images->realizeClass
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
在 _read_images()
之中,既给 class 添加了实例方法、协议、实例属性,也给 metaClass 添加了类方法、协议、类属性。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
...
// 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);
...
// 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;
// 实例方法、协议、实例属性 -> class
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
...
}
// 类方法、协议、类属性 -> class
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
...
}
}
}
...
}
attachCategories()
而添加的具体逻辑,并不在 remethodizeClass() 之中,而在 attachCategories()。
其调用堆栈
attachCategories() 核心代码如下。
可以看到,后加载到的 category 信息,会先添加。
// 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, category_list *cats, bool flush_caches)
{
...
// 逆序添加
// 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();
}
// 省略了属性、协议 ...
}
auto rw = cls->data();
...
// remethodizeClass() 调用的,需要更新缓存; methodizeClass() 则不需要
if (flush_caches && mcount> 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
// 省略了协议 ...
}
list_array_tt::attachLists()
添加列表的逻辑在 list_array_tt::attachLists()
之中
从中可看到,会保留类原来的列表,只是将其顺序往后移动了,所以调用时,会优先调用分类方法。
// objc4-750.1/runtime/objc-runtime-new.h
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]));
}
}
many lists -> many lists 当中,关键的 2 行代码
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
其实也就是将 Category 里的列表,移动到原列表之前,翻译过来,可用一张图表示。
小结
======== 编译时期 ========
不同 Category,会被编译成不同的 category_t 结构体。
包括其中的方法、属性、协议等列表,也被编译成结构体,存于 category_t 之中。
所有这些信息最终会在 Mach-O 中有所记录。
======== 运行时期 ========
Objc 初始化时,从 Mach-O 中拿到编译时期产生的 category_t 数组。
再做了以下操作:
- 将 category_t 中的实例属性、实例方法、协议添加到 class 之中。
- 将类属性、类方法、协议添加到 metaClass 中。
添加之后, Category 方法、属性、协议列表都会在前面。
疑问
上面有提到 Category 无法添加变量的原因是 category_t 中无 ivars。
那为什么不在 category_t 中也增加 ivars 呢?
个人见解如下:
结合 objc_class 的结构来看,变量列表存在于 class_ro_t 中,是不可变的。
而变量其实是存在于对象内存布局之中,Category 是给类添加东西,不应影响对象。
struct class_ro_t {
// 变量列表
const ivar_list_t * ivars;
}
但不知正确与否,麻烦知道的同学告知一声,感激不尽!