老青菜

iOS Category 底层分析

2017-09-12

Category 即分类、类别,主要作用是给类添加方法,一般使用场景如下:

  1. 按功能划分到不同文件,按需加载
  2. 制作sdk,声明私有方法
  3. 增加属性(通过方法访问关联属性)

使用

比如我们想要给一个 class 加一个属性的时候,一般我们会这样写:

// NSObject+KDTest.h 
@interface NSObject (KDTest)
@property (nonatomic, assign) NSInteger bc_age;
@end

// NSObject+KDTest.m
@implementation NSObject (KDTest)

#pragma mark - bc_age
-(NSInteger)bc_age {
    return [objc_getAssociatedObject(self, @selector(bc_age)) integerValue];
}
-(void)setBc_age:(NSInteger)bc_age {
    objc_setAssociatedObject(self, @selector(bc_age), @(bc_age), OBJC_ASSOCIATION_ASSIGN);
}
@end

当然也有比较好的方案 DProperty ,不用重复的写 getAssociated、setAssociated,大致原理是替换了 resolveInstanceMethod 方法,动态添加set、get 方法。有兴趣的可以看下源码,这里就不详细讨论了。

结构

我们知道通过 Category 添加的属性,和一般的 属性(ivar + setter + getter) 不一样,category 的属性是通过 Associated 建立关联实现的。我们来看下 class 和 category 结构上有什么区别?

//class 结构体
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

// ivar
struct objc_ivar {
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

// ivar list
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

//category 结构体
struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

对比两个结构体,可以发现:

  1. category 结构体没有 ivars 成员,可以理解共用 class 的 ivars。
  2. 而我们知道 class_addIvar 可以给一个 class 动态添加 ivar,但是也有有局限性的,官方解释如下:
1.This function may only be called after objc_allocateClassPair and before objc_registerClassPair. 

2.Adding an instance variable to an existing class is not supported.

大致意思是 class_addIvar 在 class 创建之后,注册 class 之前调用;对已经生成的 class 无效,因为 struct 大小已经固定了,无法修改 ivars 成员。
详细注释如下:

/** 
 * Adds a new instance variable to a class.
 * 
 * @return YES if the instance variable was added successfully, otherwise NO 
 *         (for example, the class already contains an instance variable with that name).
 *
 * @note This function may only be called after objc_allocateClassPair and before objc_registerClassPair. 
 *       Adding an instance variable to an existing class is not supported.
 * @note The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
 * @note The instance variable's minimum alignment in bytes is 1<<align. The minimum alignment of an instance 
 *       variable depends on the ivar's type and the machine architecture. 
 *       For variables of any pointer type, pass log2(sizeof(pointer_type)).
 */
OBJC_EXPORT BOOL
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size, 
              uint8_t alignment, const char * _Nullable types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

attach

Modern Runtime ,我们介绍了 runtime 初始化流程,大致如下:

  1. _read_images 更新 ivars ofsset
  2. 加载类信息 methodpropertyprotocol
  3. 加载 category
    可以看出,category 的加载是在最后,在这之前,类的结构体大小都已经确定好了,我们再来具体分析一下 category 的加载。

attachCategories

回到 attachCategories,这一步是来修正整合 classcategorymethod listprotocol listproperty list,继续看代码:

//加载 category
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
     //准备 category 的所有 method_list_t、property_list_t、protocol_list_t
    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;
        }
    }
    auto rw = cls->data();
    //更新 method
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
     //更新 proplists
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
     //更新 protolists
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

这里需要注意的是 attachLists 这个方法,主要作用往 category 里添加 mlistsproplistsprotolists,为什么说需要注意一下呢?继续看代码喽:

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;
            //把 old 移到数据后面
            memmove(array()->lists + addedCount, array()->lists,oldCount * sizeof(array()->lists[0]));
            //把 addedLists ,也就是 category 的 list 拷贝到前面
            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;
            //把 old 指向数组尾部
            if (oldList) array()->lists[addedCount] = oldList;
            //把 addedLists ,也就是 category 的 list 拷贝到数据头部
            memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
        }
    }

附加 List 的时候,这里分了三种情况:

  1. 多个 old + 多个 new:把 old 移到数据尾部,把 new 拷贝数据头部。
  2. 0个 old + 1个 new:把 new 指向数组头部。
  3. 1个 old + 多个 new:把 old 移到数据后面,把 new 拷贝数据头部。

虽然调用区分了三种 case,但是宗旨只有一个:

categorymethodListpropertyListprotocolList 移动到头部,类的旧列表放在尾部。

这样在 objc_msg_send 发消息,runtime 寻找方法的时候, category 的方法就会优先 class 的方法被找到,从而达到覆盖 class 的效果。

load

我们经常会被问道,category load 调用时机、顺序等等问题,其实并不难, iOS APP Launch 这篇文章说的很详细了,这里就不重复赘述了,总结一下:

  1. runtime 收到镜像加载的通知时,开始执行 load images
  2. load images 准备 load 方法,先处理 superclass load,再处理子类;最后准备 category load,单独存储。
  3. load images 调用 load 方法的时候,先调用 class load,也就是会先调用 superclass load,再调用子类的;最后调用 category load

参考链接

Apple opensource
DProperty

Tags: objc
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章