美文网首页
Cateogry底层学习

Cateogry底层学习

作者: 朝夕向背 | 来源:发表于2018-11-08 20:49 被阅读0次

    一、概述

    Category,也就是分类,能够在不修改原类代码的基础上,为类增加一些方法和属性。

    Category的好处:

    • category可以在原类的基础上扩展类的方法;可以实现私有类的方法扩展以及一些源码类的扩展。也就是说不用修改原类的代码结构,比继承更为轻量级;
    • category可以将类的实现分散到多个文件。减少类的冗余;
    • category可以创建私有方法的前向引用:在类别里提供一个类的私有方法的声明(不需要提供实现),当实例调用的时候,编译器不会报错,运行时会调用类的私有方法,继承不能继承私有方法的实现。
    • category可以向类添加非正式协议:因为Object-C里面所有类都是NSObject的子类,所以NSObject的类别里定义的函数,所有对象都能使用。

    注意:

    • 1.category里面不能添加成员变量。但是可以通过关联对象间接的实现成员变量效果。这种方式在关联对象学习。会讲到。
    • 2.category定义的方法如果和类里面的方法同名,则会覆盖原来类定义的方法。
    • 3.如果一个类有多个category,每个category定义了同一个方法,则类调用的方法是按照类的编译顺序调用,即先调用最后编译的分类的方法。

    二、category分类的原理

    • category编译之后的底层原理是struct category_t,里面存储着分类的对象方法、类方法、属性信息、协议信息。
    • 在程序运行的时候,runtime会动态的将分类的数据合并到类对象中。

    在验证这个问题之前,我们先分析下分类中生成的底层结构。我们先看一段代码

    @interface Person (Test)
    - (void)test;
    @end
    
    @implementation Person (Test)
    - (void)test{
        NSLog(@"%s",__func__);
    }
    @end
    

    反编译成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;
    };
    

    在这个结构体内,const char *name;是分类名;
    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;是属性列表。

    在C++文件中,在struct _category_t后面有这么一段代码

    static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "Person",
        0, // &OBJC_CLASS_$_Person,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
        0,
        0,
    };
    

    这段代码是按照顺序给struct _category_t 结构体内赋值。
    其中_CATEGORY_INSTANCE_METHODS_Person_方法是test实例方法:如下

    _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_Test_test}}
    };
    

    _CATEGORY_CLASS_METHODS_Person_方法是test2类方法,如下:

     _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_Person_Test_test2}}
    };
    

    接下来看下category方法合并的原理
    runtime的源码可以直接在苹果官网上下载,这里看的是最新版objc4-723,按照

    objc-os.mm
    _objc_init
    map_images
    map_images_nolock
    _read_images
    

    轨迹,在objc-runtime-new.mm先找到_read_images函数:

    这里只贴出加载category的源代码(不必细看)
    void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
     ...  
     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);
                    }
                }
            }
     ... }
    

    上面代码提到了比较重要的函数:
    remethodizeClass(cls)

    remethodizeClass方法的实现:

    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
    
        runtimeLock.assertWriting();
    
        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);
        }
    }
    

    attachCategories方法的实现

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

    remethodizeClass函数调用attachCategories(cls, cats, true ) 方法,并给cls传递参数(类或者元类对象),给cats传递参数(分类列表)。cats分类列表中,包括类对象所有的分类。

       方法数组
        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));
    

    while (i--)循环中

    • auto& entry = cats->list[i]方法取出分类列表中的某个分类;
    • entry.cat->methodsForMeta(isMeta)方法取出分类中的方法列表放到二维数组method_list_t中;
    • entry.cat->propertiesForMeta(isMeta, entry.hi)方法取出分类中的属性列表放到二维数组property_list_t中;
    • entry.cat->protocols方法取出分类中的协议信息列表,放到protocol_list_t二维数组中。
      经过循环,就把所有category里的方法,属性,协议列表都添加相应的二维数组中。
      objc_class结构
    • rw->methods.attachLists(mlists, mcount);是将所有分类的对象方法,附加到类对象的方法列表中;
    • rw->properties.attachLists(proplists, propcount);是将所有分类的属性,附加到类对象的属性列表中
    • rw->protocols.attachLists(protolists, protocount);是将所有分类的协议,附加到类对象的协议列表中。
      经过这些方法的调用,把所有分类中的信息合并到objc_class结构中。
    合并过程
     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]));
            }
    ......
    }
    
    • memmove先会扩容,扩容的大小为newCount = oldCount + addedCount。然后把原来的内存单元中的内容,往后移动。移动的距离为addedCount数量。
    • memcpy把分类中的方法,属性或者协议,移动到上个步骤中的空置单元格。

    因为移动后,分类中的方法或者属性或者协议在内存的最前面,所有在类对象调用方法时,先调用分类中的方法或者属性。

    三、Category中的load方法

    • +load方法会在runtime加载类、分类时调用,根据函数地址直接调用。
    • 每个类、分类中的+load方法,在程序运行过程中只会调用一次。

    调用顺序

    • 1、先调用类的+load
      按照编译顺序调用(先编译,先调用)
      调用子类的+load方法之前,会先调用父类的+load
    • 2、在调用分类的+load
      按照编译顺序调用(先编译,先调用)
    void call_load_methods(void)
    {
    ...
        do {
            // 1. Repeatedly call class +loads until there aren't any more
            while (loadable_classes_used > 0) {
                call_class_loads();
            }
    
            // 2. Call category +loads ONCE
            more_categories = call_category_loads();
    
            // 3. Run more +loads if there are classes OR more untried categories
        } while (loadable_classes_used > 0  ||  more_categories);
    ...
    }
    

    在苹果给的源码中可以看出,call_class_loads();先是调用类的+load;
    call_category_loads()再调用分类的+load

    static void call_class_loads(void)
    {
    ...
        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);
        }
    ...
    }
    

    在上面苹果给的源码中可以看出,load_method_t load_method = (load_method_t)classes[i].method;经过循环调用,找到classes存储的类的地址,直接调用+load方法。分类方法调用+load,也是如此。

    四、Category中的initialize方法

    • +initialize方法会在类第一次接收到消息时调用;是通过objc
      调用顺序
    • 先调用父类的+initialize,再调用子类的+initialize
      如果子类没有实现+initialize,则再次调用父类的+initialize。即父类的+initialize可能调用多次。

    在苹果给的源码中,根据以下过程

    objc-runtime-new.mm
    class_getInstanceMethod
    lookUpImpOrNil
    lookUpImpOrForward
    _class_initialize
    callInitialize
    objc_msgSend(cls, SEL_initialize)
    

    来分析

    IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                           bool initialize, bool cache, bool resolver)
    {
    ...
        if (initialize  &&  !cls->isInitialized()) {
            runtimeLock.unlockRead();
            _class_initialize (_class_getNonMetaClass(cls, inst));
            runtimeLock.read();
        }
    ...
    }
    

    上述方法中_class_initialize()传入相应的类,然后调用

    void _class_initialize(Class cls)
    {
    ...
      supercls = cls->superclass;
        if (supercls  &&  !supercls->isInitialized()) {
            _class_initialize(supercls);
        }
    }
    ...
         {
                callInitialize(cls);
                if (PrintInitializing) {
                    _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                                 pthread_self(), cls->nameForLogging());
                }
            }
    

    在上述方法中,调用callInitialize(cls);并传入类

    void callInitialize(Class cls)
    {
        ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
        asm("");
    }
    
    • 上述方法是一个递归函数,如果父类的+initialize没有初始化,就先调用父类的objc_msgSend方法,如果父类有分类,并且分类中实现了+initialize,就调用分类的+initialize
    • 如果父类的+initialize方法已经调用,就调用子类的+initialize,如果子类有分类,并且分类中实现了+initialize,就调用分类的+initialize

    相关文章

      网友评论

          本文标题:Cateogry底层学习

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