背景说明
iOS开发中,分类是我们经常会用到的一种方法,我们可以为系统的类添加一些自定义的方法,这篇文章就来梳理解析分类的实现原理。
定义
Category是Objective-C 2.0之后添加的语言特性,是对装饰模式的一种具体实现,主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。
使用场景
1 给现有的类添加方法, 声明私有方法。
2 分解体积庞大的类文件,将一个类的实现拆分成多个独立的源文件。
优点
1 不需要通过增加子类而增加现有类的方法,且分类中的方法与原始类方法基本没有区别。
2 通过分类可以将庞大的一个类的方法进行划分,从而便于代码的日后的维护、更新以及提高代码的阅读性。
缺点
分类中的方法与原始类以及父类方法相比具有更高优先级,如果覆盖父类的方法,可能导致super消息的断裂。因此,最好不要覆盖原始类中的方法。
区别
在Swift中,并没有Category这样一个概念,但是可以用Swift的Extensions来实现。
原理解析
_category_t
先到Apple GitHub,下载源码,然后打开objc.xcodeproj,搜索category_t
接下来,我们自己创建一个category,然后编译成c++文件,来看下真正编译后的category源码
首选创建以下三个文件
Person_OC
// Person_OC.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person_OC : NSObject {
int _age;
}
- (void)run;
@end
NS_ASSUME_NONNULL_END
// Person_OC.m
#import "Person_OC.h"
@implementation Person_OC
- (void)run {
NSLog(@"--__--|| Person_OC run");
}
@end
Person_OC+Test1
// Person_OC+Test1.h
#import "Person_OC.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person_OC (Test1)<NSCopying>
@property (nonatomic, assign) int age;
- (void)test;
+ (void)aMethod;
@end
NS_ASSUME_NONNULL_END
// Person_OC+Test1.m
#import "Person_OC+Test1.h"
@implementation Person_OC (Test1)
- (void)test {
}
+ (void)aMethod {
}
- (void)setAge:(int)age {
}
- (int)age {
return 20;
}
@end
Person_OC+Test2
// Person_OC+Test2.h
#import "Person_OC.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person_OC (Test2)
@end
NS_ASSUME_NONNULL_END
// Person_OC+Test2.m
#import "Person_OC+Test2.h"
@implementation Person_OC (Test2)
- (void)run {
NSLog(@"--__--|| Person_OC (Test2) run");
}
@end
然后cd到项目目录下,通过下面命令行将Preson_OC+Test1.m文件转化为c++文件,查看其中的编译过程。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person_OC+Test1.m
编译后,会在目录下生成一个Preson_OC+Test1.cpp的文件。
用Xcode打开Preson_OC+Test1.cpp,然后搜索category_t,可以看到category最后被编译生成一个_category_t的结构体,源码和注释如下
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;
};
由此可看出,category中可以添加属性,但是不能直接添加成员变量,如果要添加成员变量,得通过关联对象的技术来添加,后续会讲到。
_method_list_t
接下来我们分析_method_list_t类型的结构体,在文件中搜索_method_list_t,找到有三个地方相关的源码如下
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_OC_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
3,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_OC_Test1_test},
{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_Person_OC_Test1_setAge_},
{(struct objc_selector *)"age", "i16@0:8", (void *)_I_Person_OC_Test1_age}}
};
从_OBJC__CATEGORY_INSTANCE_METHODS_Person_OC__Test1中可以看出,这个结构体是存储对象方法列表。
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_OC_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"aMethod", "v16@0:8", (void *)_C_Person_OC_Test1_aMethod}}
};
从_OBJC__CATEGORY_CLASS_METHODS_Person_OC__Test1中可以看出,这个结构体是存储类方法列表。
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};
从_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying中可以看出,这个结构体是存储NSCopying协议中的对象方法列表。
从源码中可以看出,上面三个结构体中,还存储了方法占用的内存,方法数量,以及方法列表。
_protocol_list_t
接下来我们分析_protocol_list_t类型的结构体,在文件中搜索_protocol_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_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};
struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
0,
"NSCopying",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
0,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_OC_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};
通过上述源码可以看到先将协议方法通过_method_list_t结构体存储,之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS__Person_OC__Test1中同_protocol_list_t结构体一一对应,分别为protocol_count 协议数量以及存储了协议方法的_protocol_t结构体。
_prop_list_t
最后我们分析_prop_list_t类型的结构体,在文件中搜索_prop_list_t,找到相关的源码如下
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_OC_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"age","Ti,N"}}
};
属性列表结构体_OBJC__PROP_LIST_Person_OC__Test1同_prop_list_t结构体对应,存储属性的占用空间,属性数量,以及属性列表。
赋值过程
分析完了_category_t结构体,接下来就来看下结构体中的具体赋值过程,源码如下
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
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;
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person_OC;
static struct _category_t _OBJC_$_CATEGORY_Person_OC_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person_OC",
0, // &OBJC_CLASS_$_Person_OC,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_OC_$_Test1,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_OC_$_Test1,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_OC_$_Test1,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_OC_$_Test1,
};
static void OBJC_CATEGORY_SETUP_$_Person_OC_$_Test1(void) {
_OBJC_$_CATEGORY_Person_OC_$_Test1.cls = &OBJC_CLASS_$_Person_OC;
}
对比两个struct_category_t,可以看到,这里是定义_class_t类型的OBJC_CLASS_Person_OC结构体,然后在static void OBJC_CATEGORY_SETUP_Person_OC_Test1(void )方法中将_OBJC_CATEGORY_Person_OC_Test1的cls指针指向OBJC_CLASS_Person_OC结构体地址。我们这里可以看出,cls指针指向的应该是category的主类类对象的地址。
存储过程
通过以上分析我们发现分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。接下来我们在回到runtime源码查看catagory_t存储的方法,属性,协议等是如何存储在类对象中的。
搜索_objc_init,也就是runtime初始化函数,相关源码如下
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Old ABI: called by dyld as a library initializer
* New ABI: called by libSystem BEFORE library initialization time
**********************************************************************/
#if !__OBJC2__
static __attribute__((constructor))
#endif
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();
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_2_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
经过一系列的初始化,然后会调用map_2_images,搜索map_2_images函数相关的源码,最后在objc-runtime-new.mm中发现了map_2_images函数相关的源码,其内部是调用map_images_nolock函数,然后搜索map_images_nolock函数,在objc-os.mm文件中找到相关的源码,其函数内部经过各种计算,最后调用_read_images函数,而这几个函数的源码对于category的原理分析无关,所以就不展示相关的相关的源码。
搜索_read_images,在objc-runtime-new.mm中发现了_read_images函数相关的源码,也是在这个函数中,发现了category相关的操作代码,其源码和注释如下
/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked
* list beginning with headerList.
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount)
{
*************中间省略跟category无关的代码*************
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
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
/* || 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);
}
}
}
}
ts.log("IMAGE TIMES: discover categories");
*************中间省略跟category无关的代码*************
}
从上述代码中我们可以知道这段代码是用来查找有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。在遍历中,使用到addUnattachedCategoryForClass这个函数为类添加未附加类别,其源码如下
/***********************************************************************
* addUnattachedCategoryForClass
* Records an unattached category.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
static void addUnattachedCategoryForClass(category_t *cat, Class cls,
header_info *catHeader)
{
runtimeLock.assertWriting();
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
通过代码可以看到,这里主要是NXMapInsert(cats, cls, list)这个方法,其实也就是把当前分类,和原类建立起一个绑定关系(其实就是通过数据结构映射起来)为下面的事情做准备。
回到_read_images函数里,其最终都调用了remethodizeClass(cls)函数。搜索remethodizeClass,找到源码如下
/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
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);
}
}
函数内部最后调用attachCategories函数,而这个函数接收了类对象cls和分类数组cats,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。
搜索attachCategories,找到相关源码如下
// 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)
{
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);
if (proplist) {
proplists[propcount++] = proplist;
}
// 获取分类中的所有协议
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// 类对象的class_rw_t结构体rw, class结构体中用来存储对象方法, 属性, 协议的结构体
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);
}
上述源码中可以看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。
之后倒序遍历分类数组,将每个分类里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中。
之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,class_rw_t中存放着类对象的方法,属性和协议等数据。
之后分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去,在attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并。attachLists函数源码如下
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]));
}
}
函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝,memmove和memcpy定义如下
// memmove :内存移动。
/* __dst : 移动内存的目的地
* __src : 被移动的内存首地址
* __len : 被移动的内存长度
* 将__src的内存移动__len块内存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :内存拷贝。
/* __dst : 拷贝内存的拷贝目的地
* __src : 被拷贝的内存首地址
* __n : 被移动的内存长度
* 将__src的内存拷贝__n块内存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
attachLists函数调用中的memmove说明
// array()->lists 原来方法、属性、协议列表数组
// addedCount 分类数组长度
// oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
经过memmove方法之后,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。
attachLists函数调用中的memcpy说明
// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。
存储过程总结
1 在运行时,读取Mach-O可执行文件,加载其中的image资源时(也就是map_2_images方法中的_read_images),去读取编译时所存储到__objc_catlist的section段中的数据结构,并存储到category_t类型的一个临时变量中。
2 遍历这个临时变量数组,依次读取。
3 将catlist和原先类cls进行映射。
4 调用remethodizeClass修改method_list,property_list,protocol_list结构,将分类的内容添加到原本类中。
分类存储方式的原因
通过上面的源码分析,我们知道了分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。 其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。
如何保证分类方法的调用一定是原类方法调用的前面
OC查找方法流程中,当查找类方法的方法列表时,是采用了一个二分查找的方式的。而我们类方法的分类方法是添加到了原类的方法列表中前面位置的,当方法遍历二分查找时,后面的方法查找到,同样会往前查找一遍看看有没有同名(方法编号)方法,如果有,则返回的是前面的方法。以此来保证了其优先级顺序,也就是说方法列表中前面的方法会有高优先级执行权限。在findMethodInSortedMethodList函数中可以看到这个查找过程
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
// 倒带寻找这个值的*第一次*出现。
// 这是正确的类别覆盖所必需的。
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
总结
1 Category的实现原理是将Category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表,属性列表,协议列表拷贝到类对象对应的列表中。
2 Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。而成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法在程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。