iOS 升级打怪 - Category

1,035 阅读4分钟

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 加载流程示例图:

category 加载流程.png

以下是具体的代码:

  • _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

示例图: c.png

使用场景

  • 通过 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)
  • 底层实现

关联对象底层实现.png

与 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 循环?是改成这样效率更高?高在哪呢?还请知道的大佬评论区告知,不胜感激!!!