美文网首页
iOS - Category 的探究

iOS - Category 的探究

作者: valentizx | 来源:发表于2019-04-07 23:36 被阅读0次
    image

    Category 主要的功能是给现有的类增加新的方法,Category 的优点是:

    • 可以“分解”庞大的逻辑,进行业务分离
    • 可以实现多继承
    • 声明私有方法
    • ...

    基本使用

    现有类 Valenti,以及 Valenti 的两个分类:

    #import "Valenti.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Valenti (Purchase)
    
    - (void)purchase;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    和:

    #import "Valenti.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Valenti (Listen)
    
    - (void)listen;
    + (void)listen;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    以上显然再熟悉不过,在外部引入两个分类的头文件即可通过 Valenti 的对象或者 Velenti 类来调用:

    Valenti* v = [[Valenti alloc] init];
    [v listen];
    [Valenti listen];
    

    内部探究

    Objetive-C 对象的分类以及 isa、superclass 指针 中已经知道,实例方法放在类对象中,类方法放在元类对象中。

    同样,分类的实例方法和类方法同样放在该类的类对象和元类对象中。

    一个类,有且只有一个类对象和元类对象。

    我们将 Valenti+Listen.m 通过命令:

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Valenti+Listen.m
    

    得到 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;
    };
    

    也就是当程序在编译的时候,分类的实例方法和类方法都会先放到该结构体里。另:

    • name 表示类名,也就是 Valenti
    • instance_methods 表示实例方法列表
    • class_methods 类方法列表
    • protocols 协议信息
    • properties 属性相关

    在 C++ 实现中还可以发现:

    static struct _category_t _OBJC_$_CATEGORY_Valenti_$_Listen __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "Valenti",
        0, // &OBJC_CLASS_$_Valenti,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Valenti_$_Listen,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Valenti_$_Listen,
        0,
        0,
    };
    

    这个 _OBJC_$_CATEGORY_Valenti_$_Listen 就表示分类 Listen,内部第三、第四个参数就对应 instance_methodsclass_methods由于我的分类没有遵守任何协议,也没有增加属性,所以最后两个参数为 0。
    若增加协议以及属性:

    #import "Valenti.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Valenti (Listen)<NSCopying, NSCoding>
    
    @property (nonatomic, copy) NSString* albumName;
    @property (nonatomic, assign) NSInteger disc;
    
    - (void)listen;
    + (void)listen;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    用 C++ 重写后的源码为:

    static struct _category_t _OBJC_$_CATEGORY_Valenti_$_Listen __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "Valenti",
        0, // &OBJC_CLASS_$_Valenti,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Valenti_$_Listen,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Valenti_$_Listen,
        (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Valenti_$_Listen,
        (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Valenti_$_Listen,
    };
    

    借助 objc 源码探究

    首先找到 objc-os.mm 文件,该文件为运行时的入口文件,该文件的 _objc_init 方法为运行时的初始化方法:

    void _objc_init(void)
    {
        static bool initialized = false;
        if (initialized) return;
        initialized = true;
        ...
    
        _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    }
    

    &map_images 为方法地址,可看到 void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) 方法中调用了 map_images_nolock 方法。而该方法又会调用 _read_images 方法:

    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    

    该方法从方法名可得知内部是实现一些读取加载的功能。其中 totalClasses 这个参数表示项目中所有的类,再看 _read_images 内部有:

    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_t **catlist 内部存储了所有的分类信息。有关分类的核心处理在 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);
        }
    }
    

    这里的 attachCategories 方法格外引人注意,它接受的 clscats 参数正是类对象和分类,attachCategories 内部实现为:

    static void 
    attachCategories(Class cls, category_list *cats, bool flush_caches)
    {
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
        
        // 判断是否是元类对象
        bool isMeta = cls->isMetaClass();
    
        // ** 二维数组
        // 方法数组
        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--) {
        
            // cats 中存放着例子中的 Listen 和 Purchase 分类
            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);
    }
    

    所以这个方法就是实现了分类中方法、属性、协议的所有信息向类对象中整合的过程。
    attachLists 方法的内部实现为:

    void attachLists(List* const * addedLists, uint32_t addedCount) {
            if (addedCount == 0) return;
    
            if (hasArray()) {
                uint32_t oldCount = array()->count;
                // 扩容
                uint32_t newCount = oldCount + addedCount;
                // 给数组重新分配内存空间
                setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
                array()->count = newCount;
                
                // array()->lists 原来的方法列表
                // 移动内存
                memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
                
                // addedLists 为所有分类的方法列表     
                memcpy(array()->lists, addedLists, 
                       addedCount * sizeof(array()->lists[0]));
            }
            else if (!list  &&  addedCount == 1) {
               ...
            } 
            else {
               ...
            }
    }
    

    为弄懂这段代码的关系首先明白 rw->methods 的指向如下图:


    image

    如图所示,中间的蓝色方块是一个二维数组,此时只有一个元素(因为此时数组中无任何分类的方法列表),存储的是初始方法列表的地址。然后执行了:

    memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
    

    进行移动内存,假如 newCount 为 2,那么执行完上句代码后新的指向为:


    image

    然后执行:

    memcpy(array()->lists, addedLists, 
                       addedCount * sizeof(array()->lists[0]));
    

    将所有的方法列表拷贝到 methods 中,新的指向为:

    image

    所以假如 Valenti 中和 Valenti+Listen 中同时出现了 listen 实例方法,那么一定是优先调用分类中的 listen 方法。

    那么 N 个分类同时都有 listen 方法优先调用那个?这完全取决于编译顺序,最后加进去的分类最先调用。

    Extension 和 Category

    Extension(扩展)和 Category 原理完全不同,扩展是在编译之前就已经将各种信息整合到类对象或者元类对象中了。

    Category 中的 load 和 initialize

    load 方法

    load方法会在 runtime 加载类、分类的时候调用。*假如在 Valenti 类以及 Valenti+Listen 分类中同时打印 load 信息,并且不调用任何 Valenti 和其任何分类的任何一个方法,仅仅是运行程序发现:

    Valenti -- load
    Valenti+Listen -- load
    

    就"自行"调用了 load 方法。看来,load 方法是无视你是否用到该类的,只要本类或者分类被加到内存就会调用 load 方法。

    假如,使用了 Valenti 类以及分类,运行,打印结果依然是:

    Valenti -- load
    Valenti+Listen -- load
    

    由上节得知,分类中的同名方法明明会覆盖本类的同名方法,但是为何两个函数都执行了,并且本类的方法优于分类的方法?借助:

    unsigned int count = 0;
    Method *methodArray = class_copyMethodList(object_getClass([Valenti class]), &count);
    unsigned int i;
    for(i = 0; i < count; i++) {
        NSLog(@"%@", NSStringFromSelector(method_getName(methodArray[i])));
    }
    free(methodArray);
    

    打印结果为:

    listen
    load
    load
    

    这说明分类的 load 方法合并到了本类中。那么 Valenti 的 load 方法会优先执行?看来还需要在 objc 源码中找答案。
    我们在 objc-os.mm_objc_init 方法中可注意到另一个函数 —— load_images

    void
    load_images(const char *path __unused, const struct mach_header *mh)
    {
        ...
        call_load_methods();
    }
    

    call_load_methods(); 为调用 load 方法,其内部为:

    void call_load_methods(void)
    {
        ...
        do {
            while (loadable_classes_used > 0) {
                // 先调用类的 load 方法
                call_class_loads();
            }
            // 调用分类的 load 方法
            more_categories = call_category_loads();
        } while (loadable_classes_used > 0  ||  more_categories);
        ... 
    }
    

    这里答案已经明确,这个顺序和编译顺序无关。无论谁先加载,类的 load 的方法总是先执行,那么类的 load 方法是如何调用的:

    static void call_class_loads(void)
    {
       ...
        for (i = 0; i < used; i++) {
            Class cls = classes[i].cls;
            // load_method 指针指向的是 load 的方法的内存地址
            load_method_t load_method = (load_method_t)classes[i].method;
            ...
        }
        ...
        // 调用
        (*load_method)(cls, SEL_load);
    }
    

    load_method 定义为:

    typedef void(*load_method_t)(id, SEL);
    

    在后面 (*load_method)(cls, SEL_load) 是找到类中的方法直接调用,而不是像其他的方法从各个分类中找然后调用。分类的 load 方法也是一样:

    static call_category_loads(void)
    {
       ...
        for (i = 0; i < used; i++) {
            Class cls = classes[i].cls;
            // load_method 指针指向的是 load 的方法的内存地址
            load_method_t load_method = (load_method_t)cats[i].method;
            ...
        }
        ...
        // 调用
        (*load_method)(cls, SEL_load);
        ... 
    }
    

    而且我们可看到无论是类还是其分类的 loadable_category 定义是这样的:

    struct loadable_class {
        Class cls;  // may be nil
        IMP method;
    };
    
    struct loadable_category {
        Category cat;  // may be nil
        IMP method;
    };
    

    可断定,两个结构体中的 mehod 就是 load 方法。这个结构体的作用就是加载类的,所以只放了 load 方法。

    到这里,其他方法和 load 的方法的本质区别已经很明显,load 方法是在运行时通过指针直接找到内存中的函数地址进行调用,而其他方法则是通过消息机制进行调用,先通过 isa 指针找到其类对象或者元类对象,然后遍历类对象和元类对象的方法列表,找到即调用。

    继承关系的 load 方法

    我们增加难度,新增 SubValenti继承自 Valenti 类,并给 SubValenti 添加分类 SubValenti+Listen,在各自的方法里添加打印,运行结果如下:

    Valenti -- load
    SubValenti -- load
    Valenti+Listen -- load
    SubValenti+Listen -- load
    

    这个顺序依然是无关编译顺序。那么这个顺序由什么决定?或者说有什么规律?在上面的介绍里,类方法是优先调用,在 call_class_loads 方法中:

    load_method_t load_method = (load_method_t)classes[i].method;
    

    系统会遍历 classes 中所有的类并进行 load 方法的调用。那么得知,数组的添加顺序遍决定了类的 load 调用顺序。此时我们就得搞清楚 call_class_loads 方法中 loadable_classes 这个数组,我们在 void load_images(const char *path __unused, const struct mach_header *mh) 方法中有:

    prepare_load_methods((const headerType *)mh);
    

    这个方法的作用是在类加载之前做一些准备操作。其内部:

    void prepare_load_methods(const headerType *mhdr)
    {
       ...
        for (i = 0; i < count; i++) {
            schedule_class_load(remapClass(classlist[i]));
        }
        ...
    }
    

    schedule_class_load 方法中我们看到了和 loadable_classes 有关联的影子:

    static void schedule_class_load(Class cls)
    {
        ...
        if (cls->data()->flags & RW_LOADED) return;
        schedule_class_load(cls->superclass);
        add_class_to_loadable_list(cls);
        cls->setInfo(RW_LOADED); 
    }
    

    add_class_to_loadable_list(cls) 内部便是将类放到 loadable_classes 中去。在这里我们发现一个递归调用:

    schedule_class_load(cls->superclass);
    

    假如 cls 是 SubValenti 那么,这句代码会先添加 SubValenti 的父类 —— Valenti 类,若 Valenti 没有父类,则直接添加到 loadable_classes 中,所以 Valenti 的 load 调用永远优于其子类的 load 调用。

    还有一个值得注意的地方是在 prepare_load_methods 方法内:

    classref_t *classlist = 
            _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
    

    类的调用顺序还取决于这里的 _getObjc2NonlazyClassList 方法,这个方法和编译顺序有关。
    新建 Rex 类继承 NSObject 打印 load 方法。并将 Rex 的编译顺序提前:

    image

    运行得到:

    Rex -- load
    Valenti -- load
    SubValenti -- load
    Valenti+Listen -- load
    SubValenti+Listen -- load
    

    得以验证。

    类的 load 方法调用顺序已经明确,再看分类的 load 方法调用顺序发现原理和类方法的调用过程是一样的。也是有一个 loadable_categories 存储着所有的分类。这个数组的顺序同样取决于 prepare_load_methods 方法中 categorylist 的顺序:

    oid prepare_load_methods(const headerType *mhdr)
    {
        size_t count, i;
        ...
        category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = categorylist[i];
            Class cls = remapClass(cat->cls);
            if (!cls) continue;  
            realizeClass(cls);
            assert(cls->ISA()->isRealized());
            add_category_to_loadable_list(cat);
        }
    }
    
    

    initialize 方法

    initialize 方法是在类第一次接收到消息的时候调用。也就是类第一次接收到消息转发的时候就调用 initialize 方法。最简单的,调用 alloc 方法的时候就会调用 initialize 方法。

    我们同时在 Valenti 以及 Valenti+Listen 这个分类中打印 initialize 方法,得到以下结果:

    Valenti+Listen -- initialize
    

    只打印了分类的 initialize 方法。
    可得知,initialize 和 load 方法不同,是通过 objc_msgSend 消息机制调用,通过 isa 找到类对象或者元类对象对应的方法调用,同名的方法分类中优先调用,并且只调用一次。

    运行:

    [Valenti alloc];
    [Valenti alloc];
    [Valenti alloc];
    [Valenti alloc];
    

    发现 initialize 也只调用一次。

    还有一点值得注意的是,当我们在 Valenti 的子类 —— SubValenti 类中打印 initialize 方法,并且只给 SubValenti 对象发消息,注释掉任何 给 Valenti 发消息的代码,会得到如下打印:

    Valenti+Listen -- initialize
    SubValenti -- initialize
    

    在给子类发消息的时候,系统会先调用一次父类的 initialize 方法。

    我们知道 [Valenti alloc] 方法会转成 objc_msgSend([Valenti class], @selector(alloc)); 这便是 Valenti 类第一次接收到消息,然后调用了 initialize 方法。

    换而言之 objc_msgSend 方法内部会调用 initialize 方法。

    我们在源码中搜寻 objc_msgSend 的影子可发现有关 objc_msgSend 的东西时通过汇编来实现的:

    ...
    #if SUPPORT_TAGGED_POINTERS
        b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    #else
        b.eq    LReturnZero
    #endif
        ldr p13, [x0]       // p13 = isa
        GetClassFromIsa_p16 p13     // p16 = class
    LGetIsaDone:
        CacheLookup NORMAL      // calls imp or objc_msgSend_uncached
    ...
    

    这里我们看到了老朋友 isa 指针和 class。但其他过多的消息无法便利的得知。于是只能换一种思路去探索 initialize。

    我们知道在类第一次接受消息的时候会调用 initialize 方法,而在消息转发的时候会通过 isa 指针寻找方法,找到则调用,那么 initialize 是在什么时机调用的?是在寻找方法的时候调用?还是在调用目标方法之前调用 initialize?我们在 obj_msgSend 的汇编实现的注释中看到有:

    IMP objc_msgLookup(id self, SEL _cmd, ...);
    

    其对应 C 语言实现为:

    Method class_getInstanceMethod(Class cls, SEL sel)
    

    遵循这样的调用顺序我们可找到 initialize 的影子:


    image

    其中:

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }
    

    这段判断类是否初始化,若未初始化则调用 _class_initialize 方法。在 _class_initialize 中:

    void _class_initialize(Class cls)
    {
        ...
        
        supercls = cls->superclass;
        // 递归,判断该类是否有父类,有父类的父类是否初始化?若未初始化调用本方法进行 initialize
        if (supercls  &&  !supercls->isInitialized()) {
            // 在这里,父类优于子类
            _class_initialize(supercls);
        }
        ...
    #if __OBJC2__
            @try
    #endif
            {
                // 若父类已经初始化,则本类 initialize
                callInitialize(cls);
                ...
            }
        ...
    }
    

    callInitialize 为初始化方法。其逻辑:

    void callInitialize(Class cls)
    {
        ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
        asm("");
    }
    

    得到验证,initialize 方法也是通过 objc_msgSend 调用的。
    那么可推导:
    假如存在更多层继承关系,最顶层的父类的 initialize 方法调用,然后是其子类的 …… 最后才是本类的。

    相关文章

      网友评论

          本文标题:iOS - Category 的探究

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