目录
泛型
约束,而非正式协议不可以。)struct category_t
,里面存储着分类的对象方法、类方法、属性、协议信息,这时候分类中的数据还没有合并到类中,而是在程序运行的时候通过Runtime
机制将所有分类数据合并到类(类对象、元类对象)中去。(这是分类最大的特点,也是分类和扩展的最大区别,扩展是在编译的时候就将所有数据都合并到类中去了)Runtime
的class_copyMethodList
方法打印类的方法列表,找到宿主类方法的imp
,进行调用(可以交换方法实现)。通过 Clang 将以下分类代码转换为 C++ 代码,来分析分类的底层实现。
// Clang
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m
#import "Person.h"
@interface Person (Test)<NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)eat;
- (void)sleep;
+ (void)run;
+ (void)walk;
@end
#import "Person+Test.h"
@implementation Person (Test)
- (void)eat {
NSLog(@"eat");
}
- (void)sleep {
NSLog(@"sleep");
}
+ (void)run {
NSLog(@"run");
}
+ (void)walk {
NSLog(@"walk");
}
@end
// Person+Test.cpp
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 _prop_list_t *properties;
};
// 实例方法列表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Test_eat},
{(struct objc_selector *)"sleep", "v16@0:8", (void *)_I_Person_Test_sleep}}
};
// 类方法列表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"run", "v16@0:8", (void *)_C_Person_Test_run},
{(struct objc_selector *)"walk", "v16@0:8", (void *)_C_Person_Test_walk}}
};
// 协议列表
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test __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[2];
} _OBJC_$_PROP_LIST_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"name","T@\"NSString\",C,N"},
{"age","Ti,N"}}
};
// Person+Test 分类编译的底层结构
static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Test,
};
从以上可以看到,Category 编译之后的底层结构时struct category_t
。
objc4源码链接:https://opensource.apple.com/tarballs/objc4/
下面我们进入Runtime
的最新源代码objc4-756.2
进行分析。在源代码中与 Category 相关的代码基本都放在objc-runtime-new.h
和objc-runtime-new.mm
两个文件中。我们先来看一下 Category 在源代码中的定义struct category_t
。
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 中的数据还没有合并到类中,而是在程序运行的时候通过Runtime
机制将所有分类数据合并到类(类对象、元类对象)中去。下面我们来看一下 Category 的加载处理过程。
Runtime
加载某个类的所有 Category 数据;加载函数调用栈:
Runtime
的入口函数,进行一些初始化操作
② map_images:加锁
③ map_images_nolock:程序或内存镜像的处理下面我们通过⑤⑥⑦三个函数来分析分类中实例方法的添加逻辑:
remethodizeClass
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
/*
我们只分析分类中实例方法的添加逻辑
因此这里假设 isMeta = NO
*/
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
// 获取 cls 中所有未完成整合的分类
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:所有未完成整合的分类
*/
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
attachCategories
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
/*
我们只分析分类中实例方法的添加逻辑
因此这里假设 isMeta = NO
*/
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
/* 二维数组
[[method_t,method_t,...], [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;
}
}
//获取宿主类的 class_rw_t 数据
auto rw = cls->data();
//主要是针对 分类中有关于内存管理相关方法情况下的 一些特殊处理
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
/*
rw->methods:宿主类的方法列表
mcount:含有方法列表的分类个数
mlists:所有分类的方法列表二维数组
[[method_t,method_t,...], [method_t], [method_t,method_t,method_t,...]]
----------------------- ---------- --------------------------------
分类A的方法列表(A) B C
attachLists:将含有 mcount 个元素的 mlists 合并到 rw->methods 中
*/
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);
}
attachLists
/*
addedLists:所有分类的方法列表二维数组
[[method_t,method_t,...], [method_t], [method_t,method_t,method_t,...]]
----------------------- ---------- --------------------------------
分类A的方法列表(A) B C
addedCount:含有方法列表的分类个数,即 addedLists 的元素个数,假设
addedCount = 3
*/
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
//宿主类rw->methods方法列表中原有元素总数,假设 oldCount = 2
uint32_t oldCount = array()->count;
//合并之后的元素总数 oldCount + addedCount = 5
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]));
/*
内存拷贝:(类似于 memmove() ,将 addedLists 拷贝到类中原来的方法列表指向的位置)
[
A ---> [addedLists中的第一个元素],即最后参与编译的分类的方法列表
B ---> [addedLists中的第二个元素],即倒二参与编译的分类的方法列表
C ---> [addedLists中的第三个元素],
[原有的第一个元素],
[原有的第二个元素]
]
这也就是分类方法会“覆盖”宿主类方法的原因
*/
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]));
}
}
Runtime
机制将所有数据合并到类中去(运行时决议)。Category | Extension |
---|---|
运行时决议 | 编译时决议 |
可以有声明,可以有实现 | 只以声明的形式存在,多数情况下寄生于宿主类的.m中 |
可以为系统的类添加分类 | 不能为系统类添加扩展 |
由于分类底层结构的限制,不能直接给 Category 添加成员变量,但是可以通过关联对象间接实现 Category 有成员变量的效果。
因为类的内存布局在编译的时候会确定,但是分类是在运行时才加载,在运行时Runtime
会将分类的数据,合并到宿主类中。
为了保证挪动数据的完整性。而将分类的方法列表合并进来,不用考虑被覆盖的问题,所以用 memcpy 就好。
attachCategories()
方法中,从所有未完成整合的分类取出分类的过程是倒序遍历,最先访问最后编译的分类。然后获取该分类中的方法等列表,添加到二维数组中,所以最后编译的分类中的数据最先加到分类二维数组中,最后插入到宿主类的方法列表前面。而消息传递过程中优先查找宿主类中靠前的元素,找到同名方法就进行调用,所以优先调用最后编译的分类的方法。
回答此道问题需要先了解Runtime
的数据结构objc_class
。
baseMethodList
基础的方法列表,是ro
只读的,不可修改,可以看成是合并分类方法列表前的methods
的拷贝;methods
是rw
可读写的,将来运行时要合并分类方法列表。+load
方法;+load
方法在Runtime
加载类、分类的时候调用;+load
方法可以继承,但是一般情况下不会手动去调用+load
方法,都是让系统自动调用。