美文网首页
理解Category

理解Category

作者: MaZengyi | 来源:发表于2017-01-19 23:31 被阅读49次

category 类似于设计模式中的装饰器模式,可以在不改变当前类的情况下,给当前类扩展功能。category 可以添加方法和属性,但是不能添加实例变量,而在 runtime 层面是怎么处理 category 的呢,我们一步步往下探究。

category 数据结构

和类,方法等 runtime 对象一样,category 在底层一样是结构体,它的结构如下所示

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 有方法列表,类方法列表,协议列表,属性列表。这里可以看出,category的可以做的工作,添加实例方法、添加类方法、实现协议、添加属性,不能做的工作是添加实例变量,因为类对象在编译时候内存布局就已经确定,而 category 是工作在 runtime 时刻,这时候如果添加实例方法会破坏内存布局。

谈到内存模型,这里引用唐巧的一幅图片


好了回到正文,理解了 category 的结构我们回头看看 runtime 对 category 如何处理。这里分2部分介绍,首先从编译器的工作,到 runtime 的流程说开。

编译器的工作

在编译时刻编译器会把 OC 对象转换成 runtime 需要的结构体,category 也不例外。我们来做个测试,创建一个自定义类。

.h

@interface DLClass : NSObject
@end
@interface DLClass (DLCategory)<NSCopying>

@property (nonatomic, strong) DLClass *prop;
- (void)hello;
@end

.m

@implementation DLClass
@end

@implementation DLClass (DLCategory)
- (void)hello
{
}
@end

使用 clang -rewrite-objc DLClass.m使 OC 代码编译到 C++ 格式。我们会得到一个 cpp 文件。打开,搜索_category_t,在最后面我们看到这样的代码

static struct _category_t _OBJC_$_CATEGORY_DLClass_$_DLCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "DLClass",
    0, // &OBJC_CLASS_$_DLClass,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_DLClass_$_DLCategory,
    0,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_DLClass_$_DLCategory,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_DLClass_$_DLCategory,
};

看到这里,我们会奇怪我们的方法、属性和协议去哪了呢,我们来搜索看看,搜索_OBJC_$_CATEGORY_INSTANCE_METHODS_DLClass_$_DLCategory可以看到这么一段。

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_DLClass_$_DLCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"hello", "v16@0:8", (void *)_I_DLClass_DLCategory_hello}}
};

这里就是我们的方法啦,可以看到hello已经在里面了,
相应的属性和协议也可以这么找,

//协议
static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_DLClass_$_DLCategory __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[1];
} _OBJC_$_PROP_LIST_DLClass_$_DLCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"prop","T@\"DLClass\",&,N"}}
};

可以看出我们遵守了 NSCopying 协议,和定义了 DLClass 的属性。
至此编译器的工作也就完成啦。接下去交给 runtime

Runtime 的处理

在 dyld 启动了 APP 之后,会调用 runtime 的 _objc_init 来初始化 runtime。

#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);
}

首先进行一些初始化工作,如环境变量的初始化,锁的初始化。接下去看最后2行。绑定了2个回调方法,而 category 就在 map_2_images 里面中处理

const char *
map_2_images(enum dyld_image_states state, uint32_t infoCount,
             const struct dyld_image_info infoList[])
{
    recursive_mutex_locker_t lock(loadMethodLock);
    return map_images_nolock(state, infoCount, infoList);
}

map_2_images比较简单,只是调用了 map_images_nolockmap_images_nolock 这个方法比较长, 首先从 dyld_image_info 也就是可执行文件中获取 Objective-C 结构体元信息,如类的个数,SEL的个数等,之后调用 _read_images来读取这些东西。

_read_images这个方法也是非常长,这里贴一个简化版的代码

void _read_images(header_info **hList, uint32_t hCount)
{
   
    // 1、初始化工作,判断 GC, 是否要 禁用TaggedPointers等操作
    // 2、获取类的统计,命名类
    // 3、读取所有的类
    // 4、建立hash建立类名和类的映射
    // 5、注册 SEL
    // 6、加载 protocol 
    // 7、实现 class 在runtime 生成类对象 使用 realizeClass 函数

    // 8、处理 category 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            //寻找 category的类
            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) 
            {
                //把 category 映射到相应的 class
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    //重新生成method
                    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);
                }
            }
        }
    }

    //9、打印log
}

这里我忽略了其他的代码,把 category 部分保留了。
这里说一下 runtime 如何处理 category 的流程

获取所有的 category

使用 _getObjc2CategoryList 可以获取编译时生成的 category 的集合。

  category_t **catlist = 
            _getObjc2CategoryList(hi, &count);

判断 category 的类对应的类是否存在

使用 remapClass 在类的获取 hash 表中是否有相应的类,如果没有直接不处理此 category

 category_t *cat = catlist[i];
            //寻找 category的类
            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;
            }

处理实例方法,协议,属性

判断 category 的实例方法,协议,属性是否有存在,如果有存在,则去绑定这个 category 到类中。绑定之后,重新生成类的信息,将方法列表合并,把协议列表合并,把属性合并,然后输出 log

 bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                //把 category 绑定相应的 class
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    //重新生成 class 的信息
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

addUnattachedCategoryForClass函数的功能是将未绑定的 category 映射到 class 上,

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);
}

原理比较简单。在 map 中查找指定 class 的 category list,如果没找到,则创建一个。如果找到,扩容一个,之后把 category 加到这个 class 的category list 下面,之后插入 map

remethodizeClass函数则是用来重新生成类的实例方法列表,协议列表,和协议列表。

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();
    
    //取出class 的category的list,然后绑定
    // 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);
    }
}

实现思路为获取 class 的 category list ,addUnattachedCategoryForClass添加的,之后调用 attachCategories来完成 method list 等的合并。

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
    //生成 category 的 method_list_t, property_list_t, protocol_list_t
    
     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];
    
        //获取 category 的 method_list_t 
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        //获取 category 的 property_list_t 

        property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
    
            //获取 category 的 protocol_list_t 

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

//合并列表
    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);
}

原理比较简单,首先获取 category 的方法列表,属性列表,协议列表,之后使用 attachLists 来合并列表。
我们瞥一眼 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 将旧的 list 放到列表后边,将新的放到前面,所以这里也解释了,category 方法会覆盖了类的方法,因为 category 的方法总是在前面,会最先找到, runtime 找到就不继续找了。

处理类方法和协议

处理手法和上一步是一样的。不过这里是针对类方法,也就是 class 的 meta class。所以调用 addUnattachedCategoryForClass
remethodizeClass使用的是 cls->ISA(),类的 isa 就是指向 meta 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);
                }
            }

总结

  1. category 会在编译阶段编译成category_t 结构体。
  2. runtime 会将 class 和 category 做一个映射。
  3. category 的方法总会在 class 的方法之前,所以会造成覆盖,其实方法是保留的。
  4. category 可以处理实例方法,协议,属性,类方法,但是不能处理实例变量。

相关文章

网友评论

      本文标题:理解Category

      本文链接:https://www.haomeiwen.com/subject/clhwbttx.html