Objective-C之Category的底层实现原理

作者: RUNNING_NIUER | 来源:发表于2019-04-18 11:12 被阅读104次

    Category的使用场景

    我个人粗浅理解,就是将一个类的实现,拆解成小的模块,便于管理和维护。因为实际项目中,有些类的功能可能会非常复杂,导致一个类的代码过多,这对后期修改和维护是比较不利的,所以category方便了程序员,可以根据功能,业务等形式的划分,将类的一大堆方法分组放置以及调用。

    有趣的思考

    先来看一个最简单的category结构,一下代码定义了一个CLPerson类 和它的一个category CLPerson+Test

    // ******************** CLPerson
    #import <Foundation/Foundation.h>
    @interface CLPerson : NSObject
    -(void)run;
    @end
    
    #import "CLPerson.h"  
    @implementation CLPerson
    -(void)run
    {
        NSLog(@"CLPerson Run");
    }
    @end
    
    // ******************** CLPerson+Test
    #import "CLPerson.h"
    @interface CLPerson (Test)
    -(void)test;
    @end
    
    #import "CLPerson+Test.h"
    @implementation CLPerson (Test)
    -(void)test{
        NSLog(@"Test");
    }
    @end
    
    // ******************** CLPerson+Eat
    #import "CLPerson.h"
    @interface CLPerson (Eat)
    -(void)eat;
    @end
    
    #import "CLPerson+Eat.h"
    @implementation CLPerson (Eat)
    -(void)eat{
        NSLog(@"Eat");
    }
    @end
    

    请问❓❓❓:以下的两个方法调用,底层到底发生了什么,它们本质是否相同?

    CLPerson *person = [[CLPerson alloc]init];
    [person run]; //类的实例方法调用
    [person test];//分类的实例方法调用
    [person eat];//分类的实例方法调用

    我们都知道,[实例对象 方法]这种写法,经过底层转换之后,实际上就是,objc_msgSend(类对象, @selector(实例方法)),也就我们oc的一个基本概念,消息发送机制。因此,我们可以推定,[person run]这句代码,在消息发送机制下,首先会根据 personisa指针找到CLPerson的类对象,然后在类对象的方法列表(method_list_t * methods)里面找到该方法的实现,然后进行调用。
    接下来,你肯定会想

    • 那么[person test][person eat]呢?它的消息是发送给谁呢?
    • 是发送给person的类对象吗?
    • 还是说,对于CLPerson+Test.hCLPerson+Eat.h来说,也有其独立对应的分类对象呢?
      带着这些思考和问题,我们接下来一步一步地进行拆解。



    Category的实现原理

    底层结构——所有一切始于编译

    要想知道原理,不要猜,也不要轻易相信别人说的东西,自己验证一下才是最靠谱的。在命令行下,进入CLPerson+Test.m文件所在路径执行以下命令-->

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc CLPerson+Test.m

    得到编译后的c++文件CLPerson+Test.cpp,将其拖入xcode项目中进行查看,但是不要加入编译列表,否则程序跑不起来。直接查看文件底部,就可以找到category相关的底层信息,请看下图剖析

    上图比较粗糙,请谅解,但比文字描述来的更加直观,上面基本上分析清楚了在编译结束之后,category是以何种形式存在的,现在用文字来总结一下:
    category经过编译过程之后,系统为其定义了如下的一个结构体

      //注意,编译后的cpp文件一般比较长,会有好几万行,
      //一般我们关注类结构相关的信息,都在最后,
      //所以可以直接把文件拖到底,便可以找到这些信息
      struct _category_t {
      const char  *name; //用来存放类名
      struct _class_t *cls;
      const struct _method_list_t *instance_methods;//用来存放category里面的实例方法列表
      const struct _method_list_t *class_methods;//用来存放category里面的类方法列表
      const struct _protocol_list_t *protocols;//用来存放category里面的协议列表
      const struct _prop_list_t *properties;//用来存放category里面的属性列表
      }; 
    

    这个struct _category_t结构体,就是在程序在编译之后,被用来存放category的相关信息(instance methods, class methodsprotocolproperty)的。

    反过来描述,编译的时候,系统会给每一个category生成一个对应的结构体变量,而且他们都是struct _category_t类型的,然后把category里面的信息存到这个变量里面。

    在我的示例里面,这个变量的名称叫_OBJC_$_CATEGORY_CLPerson_$_Test,这个名字很清晰的表明,它存储的是Objective-c下的CLPerson类的Test分类的信息。

    struct _category_t中定义了六个成员变量,除去其中的第二个,我个人还没搞明白有什么用,其他的五个作用则非常清晰了

    • const char *name;
      上图中的a部分,其值表示category所对应的类的名字。
    • const struct _method_list_t *instance_methods;
      上图中的b部分,其值就是实例方法列表,可以看到里面正好放了我们定义的实例方法 -test
    • const struct _method_list_t *class_methods;
      上图中的c部分,其值就是类方法列表,可以看到里面放了我们定义的类方法 -classTest
    • const struct _protocol_list_t *protocols;
      上图中的d部分,其值就是协议列表,可以看到里面存放了 NSCoping协议
    • const struct _prop_list_t *properties;
      上图中的e部分,其值就是属性,可以看到里面有我们定义的age属性
    源码分析

    上面的篇章,我们通过查看编译后的cpp文件,了解了category在编译阶段完成后的存在形式,以CLPerson+Test为例,它所对应的struct _category_t变量中,第一个成员变量name的值为"CLPerson"(CLPerson+Eat对应的name也是"CLPerson",可以自行验证),而且根据我在对象的本质(上)——OC对象的底层实现中所讨论所得出的结果可以知道,一个OC类XXX在底层都存在一个对应的C++结构体实现struct XXX_IMPL,但我们在CLPerson+Test.cpp文件中,并没有发现 struct CLPerson+Test_IMPL/struct CLPerson+Eat_IMPL,因此,我猜想CLPersoncategory中的信息,应该还是存储在CLPerson所对应的class对象和meta-class对象中,category自己并没有独立的class对象和meta-class对象。CLPerson旗下的所有category里面的信息,应该是在某个阶段被合并到了类的CLPersonclass对象和meta-class对象中。从编译的结果看,我们并没有发现有合并的操作,仅仅是给每个category生成了对应的struct _category_t类型的变量,存放其信息。所以我合理怀疑,合并操作应该是发生在Runtime阶段。

    为了证明以上猜想,我们还是要挖掘Runtime的源码。我们先去苹果官网下载一份objc4的最新源码。然后我们直接寻找objc-os.mm文件,这个文件可以看作是Runtime进行初始化的地方。然后找到_objc_init()方法,这个方法是Runtime被加载后执行的第一个方法,可以理解成Runtime的入口方法。

    /***********************************************************************
    * _objc_init
    * Bootstrap initialization. Registers our image notifier with dyld.
    * Called by libSystem BEFORE library initialization time
    **********************************************************************/
    
    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);
    }
    

    _objc_init() 中前面的一堆方法,跟本文的主题不相关,不入坑,且看最后一个方法_dyld_objc_notify_register(&map_images, load_images, unmap_image)。这个函数里面的三个参数分别是另外三个函数:

    • map_images -- Process the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件)
    • load_images -- Process +load in the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件中的+load方法)
    • unmap_image -- Process the given image which is about to be unmapped by dyld.(处理那些将要被dyld进行去映射操作的镜像文件)

    我们查看一下map_images方法,点进去

    void
    map_images(unsigned count, const char * const paths[],
              const struct mach_header * const mhdrs[])
    {
       mutex_locker_t lock(runtimeLock);
       return map_images_nolock(count, paths, mhdrs);
    }
    

    这里面看不出啥,返回了map_images_nolock(count, paths, mhdrs),感觉像是一层转换,继续点进该方法看一下。好家伙,这个方法就比较丰富了,为了节约纸张,这里就不贴完整代码了,有兴趣自己上源码看。经过牛人指点,找到里面一个关键方法_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);。从方法名字可以看出,意思是要读取镜像,也就处理系统动态库以及我们写过的代码中的各种自定义类文件。这个方法也比较长,就截取关键的一段

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

    一句// Discover categories真的是对读者非常友好,这立马使我明白,接下来的代码是处理category相关内容的。这个read_images方法从上倒下,分好几大块,每大块头部都有类似的注释,说明该部分所做的事情,将作者的思路描述的非常清晰,不愧是苹果的源码。下面通过图解来说明一下category处理部分的大致思路

    这里注意我一个细节,上图的第一部分我已经画出来了,一开始的那个catlist是一个二维数组,里面的成员也是一个一个的数组,也就是代码里面的cat所指向的数组,它的类型是category_t *,说明cat数组里面装的就是category_t,(有点绕,慢慢来:-)一个cat里面装的就是某个class所对应的所有category。

    那么什么决定了这些category_t在cat数组中的顺序呢?

    答案是category文件的编译顺序决定的。先参与编译的,就放在数组的前面,后参与编译的,就放在数组后面。我们可以在xcode-->target-->Build Phases-->Compile Sources列表查看和调整category文件的编译顺序



    在上面的category先编译,下面的category后编译。可以鼠标拖拽进行调整。

    然后我们继续往下看,进入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,肯名字就知道,附着分类,也就是把分类的内容添加/合并到class里面,貌似快接近真相了,小鸡动🐔

    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--) {//这里i--说明,是从
            //取出某个分类变量
              entry = cats->list[i];
            //提取分类中的对象方法/类方法
            /* mlists最终会是以下形式
             [
                [method_t, method_t],
                [method_t, method_t]
             ]
             
             */
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;
                fromBundle |= entry.hi->isBundle();
            }
            //提取分类中的属性
            /* proplists最终会是以下形式
             [
             [property_t, property_t],
             [property_t, property_t]
             ]
             
             */
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
            
            //提取分类中的协议
            /* protolists最终会是以下形式
             [
             [protocol_t, protocol_t],
             [protocol_t, protocol_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);
        
        //搞定,结束
    }
    

    ⚠️以上这一部分代码中的注解引用自MJ大神在腾讯平台的相关分享⚠️

    这里注意一个地方,这里面用了while (i--) {entry = cats->list[i]; ......},entry可以简单理解成 category_t,(里面还有一些其他内容,不影响我们的理解),那么list里面就装了一堆的category_t,他们都对应着同一个class,这些category_t在数组中的顺序,和前面我们讨论的category文件的编译顺序是相同的,也就是先编译的category在前,后编译的category在后。 在while循环里面进行处理的时候,是从下标 cats->count-1(也就是i--)开始的,也就是从数组的尾部向前一个一个的处理。处理过程主要就是把category的方法列表添加到mlists里面,mlists[mcount++] = mlist;,而mcount是从0开始的,所以结果就是最终,放到mlists里面的方法列表顺序是倒过来的,最前面的方法列表,对应着最后编译的cetegory(协议和属性的处理过程和这里一样)

    上述方法里面的最后一个操作rw->methods.attachLists我们再进一步分析一下,看一看,最终分类中的方法和class中的方法,最终是以怎么样的顺序合并存放到最后的方法列表里面的,进入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]));
            }
        }
    

    这个函数的两个参数分别代表

    • addedLists--将要被添加的category中的方法列表组成的的数组,
    • addedCount--addedLists数组的元素数量。
      这个方法是今天讨论的问题里面最有趣的地方,将会解释我们在使用category中所碰到的各种现象。请看下图分解
      category的合并过程

    如此看来,最终类的方法列表里面,如果class有自己对应的category,那么category中的方法列表会被合并放置在class的方法列表的前部,类本身的方法则会被往列表尾部挪,当我们通过[obj method]的方式调用方法的时候,系统会到在类的方法列表里面,从前往后遍历查找。

    因此,如果category里面如果重写了class里面的方法,那么,最终会调用category的方法实现,就是因为它被放在了列表前面,先被找到,就被调用了,其实class里面的同名方法还是在的,并没有被覆盖,只不过看起来像是覆盖了。

    另外,我们在上面分析attachCategories方法的时候得知,该方法实际上将category的方法列表按照编译顺序倒过来存到了一个数组里,供后续方法使用。

    那么过程走到这里,便可以知道,最最最终,在class的方法列表里面,最后参加编译的category的方法会出现在方法列表的最前面,先参加编译的category的方法会出现在方法列表的后面,列表的最后存着class自己的方法[对于meta-class也是一样的],好,分析结束。

    回答开篇的几个问题

    ✈️✈️✈️✈️

    • [person test][person eat]的消息是发送给谁呢?

    发送给 CLPerson的类对象

    • 还是说,对于CLPerson+Test.h来说,也有其独立对应的分类对象呢?

    不存在所谓的 分类的类对象,一个类以及它的所有分类,都只对应一个类对象,它们所有的实例方法(-方法),属性(@property),协议(@protocol)都被合并到了这一个类对象里面,它们所有的类方法(+方法),都被合并到了这个类的元类对象里面。上面所说的合并,都是发生在程序运行阶段,运用了Objc的Runtime机制完成。

    ✈️✈️✈️✈️





    *****************砍瓜切菜*****************

    (1)category里面的方法存放在哪里?
    • 一个类所对应的分类下的对象方法,存放在该类的类对象的方法列表里面。
    • 一个类所对应的分类下的类方法,会存放在该类的元类对象的方法列表里面
    (2)category里面的方法,是什么时候被放到类的类对象/元类对象的方法列表里面的?(编译阶段 or 运行阶段)
    • 结论:是程序运行的时候进行的。通过runtime动态地将分类的方法,合并到类对象、元类对象中。

    所有的category结构是一样的,只不过里面存储的具体数据不同,每一个category都有自己对应的一个变量,类型为 struct _category_t ,在编译过程中,会完成对struct _category_t类型变量的赋值。

    (3)程序运行过程中,分类中的方法是如何合并到类的方法列表中的?

    面试官要问,就直接画图改他看吧,文字描述感觉弱爆了:)

    (4)分类方法会覆盖类里面的方法吗?

    不会

    (5)如果有多个分类有同名的方法A,那么实际哪一个方法A会被调用?

    最后参加编译的category里面的A方法会被调用

    (6)如何控制分类的编译顺序?

    在Build Phase->Compile Sources里面调整,直接拖拽

    (7)category和extension的区别是什么?
    • extension的内容是在编译完成后,就存在于类对象里面,extension只不过是将原本.h文件里面的内容挪到了.m文件里面,不让外界看见,实质上它就是class.h的一部分,
    • category的内容,是在编译的时候,保存到了struct _category_t 结构体变量中,然后在程序运行阶段(runtime机制)才动态合并到类对象当中的。

    相关文章

      网友评论

        本文标题:Objective-C之Category的底层实现原理

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