浅谈Category

前几天在看Objective-C Category相关的内容,主要了解了下其底层如何实现,说的通俗点就是category中定义的内容如何绑定到类上的,关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?

本文大部分内容主要参考 深入理解Objective-C:Category,没有这篇文章那么详细,文本可以认为是概要版,自己做个笔记,性子比较急的同学可以看我这篇。

Category是什么?

category是Objective-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。除此之外,apple还推荐了category的另外两个使用场景1

  • 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,a)可以减少单个文件的体积 b)可以把不同的功能组织到不同的category里 c)可以由多个开发者共同完成一个类 d)可以按需加载想要的category 等等。
  • 声明私有方法

剖析Category

剖析某样事物,那么必不可少的一个步骤就是知道他的定义,然后再去了解他的实
现。那么他的定义是什么呢。我们先去写一个category

MyClass.h:

#import <Foundation/Foundation.h>

@interface MyClass : NSObject

- (void)printName;

@end

@interface MyClass(MyAddition)

@property(nonatomic, copy) NSString *name;

- (void)printName;

@end

MyClass.m:

#import "MyClass.h"

@implementation MyClass

- (void)printName
{
    NSLog(@"%@",@"MyClass");
}

@end

@implementation MyClass(MyAddition)

- (void)printName
{
    NSLog(@"%@",@"MyAddition");
}

@end

经过clang编译之后,我们看到一个c++文件,其中我们截取最主要的一部分代码来讲解

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; //属性列表
};
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_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_MyClass_MyAddition_printName}}
};

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_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"name","T@\"NSString\",C,N"}}
};

extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_MyClass;

static struct _category_t _OBJC_$_CATEGORY_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MyClass",
    0, // &OBJC_CLASS_$_MyClass,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition,
    0,
    0,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MyClass_$_MyAddition,
};
static void OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition(void ) {
    _OBJC_$_CATEGORY_MyClass_$_MyAddition.cls = &OBJC_CLASS_$_MyClass;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
    (void *)&OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition,
};
static struct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
    &OBJC_CLASS_$_MyClass,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_MyClass_$_MyAddition,
};
我们可以看到编译阶段,编译器帮忙生成了方法列表_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition 和属性列表_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition, 从上面解析出来的命名规则和static修饰,我们也可以推测出为何分类名要不一致,不然会报错的原因。最后一点是L_OBJC_LABEL_CATEGORY_$,我们看到其是一个数组,存储在Data段下的__objc_catlist,regular section中,某个类有几个分类,那么该数组的长度就是几

了解category结构之后,我们来看下其是如何绑定的
我们在objc-runtime-new.mm文件可以看到如下代码段

// Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        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  
                ||  (hasClassProperties && 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);
                }
            }
        }
    }

其中catlist是编译产生的分类列表,我们看到通过addUnattachedCategoryForClassremethodizeClass来实现分类到类的绑定。通过上述的代码,我们也看到了 category的实例方法、协议以及属性是添加到类上 的,category的类方法和协议是添加到类的metaclass上的,因为该块内容不属于本章节范围,就不展开了。回到正题,addUnattachedCategoryForClass其实只是做了分类和类的关联,那么我们把重心放到remethodizeClass方法上

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

    runtimeLock.assertLocked();

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

看到remethodizeClass之后,我们看到其又再次调用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, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        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函数,我们看下该函数,这真的是最后一次调用了...

/// 方法列表数组长度增加,newCount = oldCount + addedCount,新加的方法列表添加到原有的方法列表中
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(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
这两句其实等价于如下语句
    for (i = 0; i < oldCount; i++) {
        newLists[newCount++] = oldLists[i];
    }

void *memmove( void* dest, const void* src, size_t count );由src所指内存区域复制count个字节到dest所指内存区域。
void *memcpy(void *dest, const void *src, size_t n);从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中

到此为止,整个的流程走完了,从中我们发现了几个事情。划重点了

  • 我们常态的认为分类的方法是覆盖了原先类的方法,其实该说法并不正确,准确的讲,假如分类和类都有methodA,真正的方法列表中其实存在2个methodA。上述源码很好阐述了这一观点
    *category的方法放在类的方法前面,又因为runtime机制找到方法之后直接返回,所以有了这个错觉类的方法被覆盖了

假如有多个分类,那么最终执行的方法是哪个分类的呢?我们从上述代码剖析中知道是最后一个被编译的分类即最新的被调用。通过实验我们看到是根据编译顺序决定的。其实我们可以用一个环境变量OBJC_PRINT_REPLACED_METHODS来验证一下这个观点,该环境变量的作用是log methods replaced by category implementations

671666-cbd4eea6cb42f423.png
OBJC_PRINT_REPLACED_METHODS

log输出:

objc[31548]: REPLACED: +[Student printName]  by category LKAdd1
objc[31548]: REPLACED: +[Student printName]  by category LKAdd2  

其实有很多环境变量的值,每个值有不同的用处,比如我们常用到的DYLD_PRINT_STATISTICS就是来输出main之前各个部分所占用的时长。关于运行时环境变量的可以看我的另一篇文章Environment Variables,这边就不展开了...

关联对象

综上所述,我们知道分类是无法添加实例变量的,但是很多场景,我们可能需要用到。那么就引出后面阐述的关键先生关联对象。随之而来的是问题:关联对象存在什么地方? 如何存储? 对象销毁时候如何处理关联对象?

@interface MyClass(MyAddition)

@property(nonatomic, copy) NSString *name;

@end
@implementation MyClass(MyAddition)
- (NSString *)name {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

通过查询源码,在runtime.h,runtime.mm文件中找到了定义和实现

OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

顺藤摸瓜,我们在objc-references.mm文件找到了具体的函数

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

我们看到关联对象是都是由AssociationsManager管理

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

我们看到所有对象的关联对象都存储在静态的AssociationsHashMap中。以object为key,得到另外一个AssociationsHashMap,里面存储着key-value键值对

上面的问题似乎只剩下一个了,关联对象是何时销毁的呢?
通过检索关键词,我们在objc-runtime-new.mm文件中找到了我们期望的内容

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

通过上面的注释和代码,我们清晰的了解了释放的顺序。关联对象其实被放在了最后一步,说明关联对象释放比对象释放要晚得多。

到此为止,关于Category的话题告一段落,里面涉及比较多的源码内容,所以自己也花了比较多的时间。最后,再次感谢 深入理解Objective-C:Category,讲的非常详细。

猜你喜欢

转载自blog.csdn.net/weixin_34026484/article/details/90968586