category:一种通过 runtime 实现的技术,该技术可以使我们在没有源码的情况下,动态的给类添加方法、协议、属性。category 是在程序运行时将添加的代码动态合并到类对象或者元类对象中。
实现原理
为了梳理 category 的实现原理,首先我们来看一下 category 的底层数据结构。
给 Goods 创建一个 Goods (Test) 的 category,通过 clang -arch arm64 -rewrite-objc Goods+Test.m 编译成 C++ 代码,可以看到 category_t 的结构体,该结构体就是 category 的底层数据结构:
struct _category_t {
// 被添加代码的类名,对应的上述例子中的 Goods。
const char *name;
// 被添加代码的类对象,对应的上述例子中的 Goods。
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_t 结构体的内容,接下来看一下 runtime 是如何把 category 中的代码动态添加到类或元类中的。
查看 runtime 的源码,就需要看 objc4 的代码了,本篇的objc4 代码版本为:objc4-818.2。
category 加载流程示例图:
以下是具体的代码:
- _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();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
Objc2 以前是通过 _dyld_objc_notify_register(&map_images, load_images, unmap_image); 调用 map_images 方法,Objc2 是如何调到 map_images 方法的我没看出来😭,有知道的大佬希望评论区不吝赐教。。。
- map_images: 内部调用 map_images_nolock
- map_images_nolock:内部调用 _read_images
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
......
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
......
}
- _read_images:内部调用 realizeClassWithoutSwift
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
......
ts.log("IMAGE TIMES: discover categories");
// Category discovery MUST BE Late to avoid potential races
// when other threads call the new category code before
// this thread finishes its fixups.
realizeClassWithoutSwift(cls, nil); // 重新构建类的方法、属性、协议列表
......
}
- realizeClassWithoutSwift 相关代码:
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
......
// Attach categories
methodizeClass(cls, previously);
}
- methodizeClass 相关代码:
static void methodizeClass(Class cls, Class previously)
{
......
// Attach categories.
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
......
}
- attachToClass 相关代码:
void attachToClass(Class cls, Class previously, int flags)
{
......
if (it != map.end()) {
category_list &list = it->second;
if (flags & ATTACH_CLASS_AND_METACLASS) {
int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
// 相关代码
attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
} else {
attachCategories(cls, list.array(), list.count(), flags);
}
map.erase(it);
}
......
}
- attachCategories 相关代码:
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
// 将 category 中的方法列表添加到类对象的方法列表中
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 协议、属性逻辑类似
......
}
- void 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;
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
// 将类对象原始的方法列表添加到新方法列表的尾部
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
// 将 category 中的方法列表添加到新方法列表头部
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();
}
......
}
从上面的代码我们可以看到,主要的就是这两个 for 循环。第一个 for 循环的作用是把类的方法列表放在新的方法列表的尾部;第二个 for 循环的作用是把 category 中的方法列表添加新方法列表的头部。
相应逻辑的 OC 代码:
NSArray *categoryMethodList = @[@"test1", @"test2"];
NSArray *classMethodList = @[@"test3", @"test4"];
NSInteger oldCount = [classMethodList count];
NSInteger addedCount = [categoryMethodList count];
NSInteger newCount = oldCount + addedCount;
NSMutableArray *newMethodList = [NSMutableArray arrayWithObjects:@"", @"", @"", @"", nil];
for (int i = oldCount - 1; i >= 0; i--) {
newMethodList[i + addedCount] = classMethodList[i];
}
for (int i = 0; i < addedCount; i++) {
newMethodList[i] = categoryMethodList[i];
}
NSLog(@"%@", newMethodList); // test1, test2, test3, test4
示例图:
使用场景
- 通过 category 给没有源码的类添加方法:
@interface NSString (test)
- (void)testMethod;
@end
- 通过 category 给代码行多的文件进行拆分,比如按功能拆分,从而使代码可维护性更高。
关联对象
category 可以给类添加属性,但无法添加成员变量。如果想实现类似成员变量的效果,可以通过关联对象来实现。
#import <objc/runtime.h>
@implementation Goods (Test)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @selector(name));
}
-
objc_AssociationPolicy:关联对象的内存管理语义
- OBJC_ASSOCIATION_ASSIGN == (nonatomic, assign)
- OBJC_ASSOCIATION_RETAIN_NONATOMIC == (nonatomic, strong)
- OBJC_ASSOCIATION_COPY_NONATOMIC == (nonatomic, copy)
- OBJC_ASSOCIATION_RETAIN == (atomic, strong)
- OBJC_ASSOCIATION_COPY == (atomic, copy)
-
底层实现
与 extension(Objective-C) 的比较
- 加载时间不同:category 在程序运行时加载;extension 在程序编译时加载。
- 作用不同:category 一般用来给类添加方法,不需要类的源码;extension 一般用来添加私有方法和属性,需要类的源码。
- 可添加内容不同:extension 可以添加实例变量;category 则不可以。
方法重名
假如 category 中声明了与类中方法名一致的方法,调用方法的时候则会调用 category 中的方法实现。
原因是因为 category 中的方法列表是放在类的方法列表的头部,消息机制找到方法实现就不会再继续往后找了。
疑惑
objc4 当前版本的 attachLists 数组合并代码:
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
而老版本(比如objc4-779.1)的数组代码合并时用的 mommove 和 momcpy:
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
新版本为啥改成了 for 循环?是改成这样效率更高?高在哪呢?还请知道的大佬评论区告知,不胜感激!!!