基于runtime理解Category

2019-05-21  本文已影响0人  平常心_kale

Category一般又叫分类,它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。当我们需要为一个类额外的增加方法属性时,分类便是我们的首选。我们都知道 对象方法是存放在类中,类方法是存放在元类对象中,那么如果在category增加了方法属性等,它们又存放在哪呢?

1.Runtime中Category的底层结构:

打开<objc/runtime.h>

typedef struct objc_category *Category;   Category 是表示一个指向分类的结构体的指针:

//runtime1880行显示如下
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;  //分类所实现的协议列表
}  
现在在分类中申明一个成员变量看看会发生什么?
{
    NSInteger _sex;  
    报错 Instance variables may not be placed in categories  //实例变量不能放在类别中
}
通过关联技术 实现set和get方法
- (void)setWeight:(NSInteger)weight {
    objc_setAssociatedObject(self, @"weightKey", @(weight), OBJC_ASSOCIATION_ASSIGN);
}
- (NSInteger)weight {
    return [objc_getAssociatedObject(self, @"weightKey") integerValue];
}

那么现在给一个分类添加一个方法,这个方法是怎么添加进去的呢?带着这个问题先看看runtime的源码。

首先新建一个Student继承与NSObject,再新建一个Student+Category 的类别,然后在终端中输入 clang -rewrite-objc Student+Category .m 编译成C++代码,看看其结构。

//编译成  C++代码
struct _category_t {
    const char *name;
    struct _class_t *cls;
    
    //结构体前面的name和class,我们可以猜测这个是类名和分类的名称相关的内容
    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”可以看到,一个分类在编译后转成C++文件,就是以上图这种结构体的方式存在。
 我们每增加一个分类,编译后就会多一个这样的结构体,然后在运行时阶段,把所有分类的结构体全部动态合并到我们类里面去
 */
问题:Category为什么能添加方法不能添加成员变量

每一个Category都会编译然后存储在一个_category_t类型的变量中,
通过这个_category_t的结构结构我们也可以看出,属性存储在_prop_list_t,这里并没有类中的objc_ivar_list结构体,所以Category的_category_t结构体中根本没有储存ivar的地方,所以不能添加成员变量。

2.源码分析

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

// runtime加载入口
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();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

先从map_images读取模块,再到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到分类相关代码。

 // 发现和处理所有Category
    for (EACH_HEADER) {
        // 外部循环遍历找到当前类,查找类对应的Category数组
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        // 内部循环遍历当前类的所有Category
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            
            if (!cls) {
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }
 
            // 首先,通过其所属的类注册Category。如果这个类已经被实现,则重新构造类的方法列表。
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                // 将Category添加到对应Class的value中,value是Class对应的所有category数组
                addUnattachedCategoryForClass(cat, cls, hi);
                // 将Category的method、protocol、property添加到Class
                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" : "");
                }
            }

            // 这块和上面逻辑一样,区别在于这块是对Meta Class做操作,而上面则是对Class做操作
            // 根据下面的逻辑,从代码的角度来说,是可以对原类添加Category的
            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);
                }
            }
        }
    }

    ts.log("IMAGE TIMES: discover categories");

继续看看 remethodizeClass函数

/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/

// 将Category的信息添加到Class,包含method、property、protocol
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();

    // 从Category哈希表中查找category_t对象,并将已找到的对象从哈希表中删除
    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);
    }
}

继续看看 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.

// 获取到Category的Protocol list、Property list、Method list,然后通过attachLists函数添加到所属的类中
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);
  //是否为元类对象
    bool isMeta = cls->isMetaClass();

    // 按照Category个数,分配对应的内存空间
    // 方法列表二维数组
    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;
    
    // 循环查找出Protocol list、Property list、Method list
    while (i--) {
        auto& entry = cats->list[i];
        //根据传入是否元类对象来取出对应的类方法或者实例方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
          //1.将所有分类中的所有方法存入到 mlists中
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
          //2.将所有分类中的所有属性存到proplists中
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            //3.将所有分类中所有协议存入protolists中
            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);
}

继续看下attachLists函数内部。

 void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists

            //array() ->list :原来的列表数组
            //addedList:分类的列表数组
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            
            //array() - >:原方法列表,把原方法列表往后移动,前面增加新方法列表需要的空间
            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]));
        }
    }

    void tryFree() {
        if (hasArray()) {
            for (uint32_t i = 0; i < array()->count; i++) {
                try_free(array()->lists[i]);
            }
            try_free(array());
        }
        else if (list) {
            try_free(list);
        }
    }
总结

1.分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
2.Category可以添加属性,但是并不会自动生成成员变量及set/get方法。
3.因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。
4.而分类是在运行时才去加载的。那么我们就无法在程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。

3.load 和 initialize相关

对于这个区别,我们从两个角度出发分析,调用方式和调用时刻。

load方法会在程序启动就会调用,当装载类信息的时候就会调用。
现在看看runtime源码我们发现是优先调用类的load方法,之后调用分类的load方法。

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. 反复调用class +load,直到没有更多
        while (loadable_classes_used > 0) {
            call_class_loads();   //1.先调用类的load 方法
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();  //2.之后再调用分类的load方法

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}
通过打印发现:
2019-05-21 16:51:55.008427+0800 BaseProject[689:181934] AnimalFather, +[AnimalFather load]
2019-05-21 16:51:55.014857+0800 BaseProject[689:181934] Animal, +[Animal load]
2019-05-21 16:51:55.017435+0800 BaseProject[689:181934] Animal, +[Animal(fun) load]

问题

为什么写了分类,子类中的load方法也调用了呢?难道分类不会覆盖子类中的方法吗?

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}


/***********************************************************************
* call_category_loads
* Call some pending category +load methods.
* The parent class of the +load-implementing categories has all of 
*   its categories attached, in case some are lazily waiting for +initalize.
* Don't call +load unless the parent class is connected.
* If new categories become loadable, +load is NOT called, and they 
*   are added to the end of the loadable list, and we return TRUE.
* Return FALSE if no new categories became loadable.
*
* Called only by call_load_methods().
**********************************************************************/
static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    
    /*
     我们看到load方法中直接拿到load方法的内存地址直接调用方法,不在是通过消息发送机制调用。
     
     我们可以看到分类中也是通过直接拿到load方法的地址进行调用。因此正如我们之前试验的一样,
     分类中重写load方法,并不会优先调用分类的load方法,而不调用本类中的load方法了。
     */
    
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

注意
接下来为 <AnimalFather>、 <Animal> 、<Animal+fun> 添加initialize方法。

通过打印发现如下:

2019-05-20 16:47:34.611819+0800 BaseProject[489:58023] AnimalFather, +[AnimalFather initialize]
2019-05-20 16:47:34.611933+0800 BaseProject[489:58023] Animal, +[Animal initialize]

由此可见分类中的方法覆盖了子类中的方法
首先还是先看下runtime中initialize的源码吧

void callInitialize(Class cls){
    //通过消息发送机制
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}
注意
2019-05-20 16:51:18.487864+0800 BaseProject[503:59074] AnimalFather, +[AnimalFather initialize]
2019-05-20 16:51:18.488139+0800 BaseProject[503:59074] AnimalFather, +[AnimalFather load]
2019-05-20 16:51:18.495708+0800 BaseProject[503:59074] Animal,d +[Animal(fun) initialize]
2019-05-20 16:51:18.495755+0800 BaseProject[503:59074] Animal, +[Animal load]
2019-05-20 16:51:18.498858+0800 BaseProject[503:59074] Animal, +[Animal(fun) load]
    Animal *a = [[Animal alloc]init];
    
    [a loadName2];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        Animal *a1 = [[Animal alloc]init];
        NSLog(@"再次初始化Animal 若再次调用初始化实例,initialize 不再调用 ");
    });
总结:

最后

如果有不对的地方请指正,十分感谢。

上一篇 下一篇

猜你喜欢

热点阅读