美文网首页
OC底层原理(五):Category

OC底层原理(五):Category

作者: 跳跳跳跳跳跳跳 | 来源:发表于2021-01-07 22:21 被阅读0次

    分类是我们平时很常用的一种技术,可以为现有类按模块划分添加方法。

    分类的基本使用

    首先创建ZJPerson类

    @interface ZJPerson : NSObject
    - (void)run;
    @end
    
    @implementation ZJPerson
    - (void)run {
        NSLog(@"run");
    }
    @end
    

    再创建两个ZJPerson分类

    @interface ZJPerson (Study)
    - (void)read;
    @end
    
    @implementation ZJPerson (Study)
    - (void)read {
        NSLog(@"read");
    }
    @end
    
    @interface ZJPerson (Life)
    - (void)sleep;
    @end
    
    @implementation ZJPerson (Life)
    - (void)sleep {
        NSLog(@"read");
    }
    @end
    

    然后在main文件里导入头文件

    #import "ZJPerson.h"
    #import "ZJPerson+Study.h"
    #import "ZJPerson+Life.h"
    

    然后在main函数中创建ZJPerson对象,调用分类里的方法

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            ZJPerson *person = [ZJPerson new];
            [person run];
            [person read];
            [person sleep];
        }
        return 0;
    }
    

    分类的底层结构

    我们使用命令行cd到ZJPerson+Study.m文件路径下,使用

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ZJPerson+Study.m
    

    这个命令将study分类转换成C/C++语言,然后将在ZJPerson+Study.m文件路径下生成的cpp文件拖到项目中

    截屏2021-01-05 21.10.52.png 截屏2021-01-05 21.11.03.png

    然后在cpp文件中搜索_category_t

    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 _category_t _OBJC_$_CATEGORY_ZJPerson_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "ZJPerson",
        0, // &OBJC_CLASS_$_ZJPerson,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_ZJPerson_$_Study,
        0,
        0,
        0,
    };
    
    • ZJPerson+Study.m转换成来的结构体就是struct _category_t _OBJC_$_CATEGORY_ZJPerson_$_Study
      在编译的时候,一个分类对应一个结构体,比如
      study分类对应struct _category_t _OBJC_$_CATEGORY_ZJPerson_$_Study
      life分类对应struct _category_t _OBJC_$_CATEGORY_ZJPerson_$_Life

    • 分类里的实例方法存放在结构体里的instance_methods数组中

    • 类方法和协议方法还有属性同理

    category添加方法的底层原理

    首先我们打开runtime源码objc4
    选中objc-os.mm类

    //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方法

    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方法,然后再点击map_images_nolock里的_read_images方法

    void 
    map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                      const struct mach_header * const mhdrs[])
    {
    ……
    if (hCount > 0) {
            _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    ……
    }
    
    void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
    {
    ……
    if (didInitialAttachCategories) {
            for (EACH_HEADER) {
                load_categories_nolock(hi);
            }
        }
    ……
    }
    

    继续点击进入load_categories_nolock方法

    static void load_categories_nolock(header_info *hi) {
    ……
    attachCategories(cls, &lc, 1, ATTACH_EXISTING);
    ……
    }
    

    attachCategories这个方法就是合并分类方法、属性、协议的核心方法了,继续点进去

    attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                     int flags)
    {
    ……
        constexpr uint32_t ATTACH_BUFSIZ = 64;
        //cls : [ZJPerson class]
        //cats_list : [category_t(Study), category_t(Life)]
        //cats_count : 分类数量2
        //方法大数组
        method_list_t   *mlists[ATTACH_BUFSIZ];
        //属性大数组
        property_list_t *proplists[ATTACH_BUFSIZ];
        //协议大数组
        protocol_list_t *protolists[ATTACH_BUFSIZ];
    
        uint32_t mcount = 0;
        uint32_t propcount = 0;
        uint32_t protocount = 0;
        bool fromBundle = NO;
        bool isMeta = (flags & ATTACH_METACLASS);
        auto rwe = cls->data()->extAllocIfNeeded();
    
        for (uint32_t i = 0; i < cats_count; i++) {
            //按照编译顺序取出category
            //i = 0 : category_t(Study)
            //i = 1 : category_t(Life)
            auto& entry = cats_list[I];
            //取出category里的方法数组
            //如果是category_t(Study),则对应[read]
            //如果是category_t(Life),则对应[sleep]
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                if (mcount == ATTACH_BUFSIZ) {
                    prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                    rwe->methods.attachLists(mlists, mcount);
                    mcount = 0;
                }
                //将方法列表倒序插入方法数组中
                //第一次循环i = 0,mlists里的数据为[[read]]
                //第二次循环i = 1,mlists里的数据为[[sleep], [read]]
                mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
                fromBundle |= entry.hi->isBundle();
            }
    
            //属性逻辑同方法
            property_list_t *proplist =
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                if (propcount == ATTACH_BUFSIZ) {
                    rwe->properties.attachLists(proplists, propcount);
                    propcount = 0;
                }
                proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
            }
    
            //协议逻辑同方法
            protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
            if (protolist) {
                if (protocount == ATTACH_BUFSIZ) {
                    rwe->protocols.attachLists(protolists, protocount);
                    protocount = 0;
                }
                protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
            }
        }
        if (mcount > 0) {
            prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                               NO, fromBundle, __func__);
            //将所有分类的方法合并到类对象里去
            rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
            if (flags & ATTACH_EXISTING) {
                flushCaches(cls, __func__, [](Class c){
                    // constant caches have been dealt with in prepareMethodLists
                    // if the class still is constant here, it's fine to keep
                    return !c->cache.isConstantOptimizedCache();
                });
            }
        }
        //将所有分类的属性合并到类对象里
        rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
        //将所有分类的协议合并到类对象里
        rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
    }
    

    我们继续点进attachLists方法

    void attachLists(List* const * addedLists, uint32_t addedCount) {
            if (addedCount == 0) return;
            //addedLists : [
            //              [read],
            //              [sleep]
            //             ]
            //addedCount : 2
            if (hasArray()) {
                // many lists -> many lists
                
                //array : 类对象里的方法列表[[run]]
                uint32_t oldCount = array()->count;
                uint32_t newCount = oldCount + addedCount;
                //根据分类的数量对array扩容
                array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
                newArray->count = newCount;
                array()->count = newCount;
    
                for (int i = oldCount - 1; i >= 0; I--)
                //将类对象里的方法数组放在newArray的最后面
                //newArray : [null, null, [run]]
                    newArray->lists[i + addedCount] = array()->lists[I];
                for (unsigned i = 0; i < addedCount; I++)
                //将分类里的方法数组依次插入到数组里
                //第一次循环
                //newArray : [[read], null, [run]]
                //第二次循环
                //newArray : [[read], [sleep], [run]]
                    newArray->lists[i] = addedLists[I];
                free(array());
                setArray(newArray);
                validate();
            }
    ……
    }
    

    到这里就将分类的里的方法,属性,协议都合并到类对象里去了

    category的加载处理过程总结

    1. 首先会通过runtime加载某个类的所有category数据
    2. 将所有的category方法、属性、协议数据分别合并到一个大数组中
    3. 分别将方法、属性、协议大数组插入到原来类对象数据的前面

    分类方法覆盖原有的方法

    我们更新ZJPerson和它的分类代码

    @interface ZJPerson : NSObject
    - (void)run;
    - (void)play;
    @end
    
    @implementation ZJPerson
    - (void)run {
        NSLog(@"run");
    }
    
    - (void)play {
        NSLog(@"ZJPerson play");
    }
    @end
    
    @interface ZJPerson (Study)
    - (void)read;
    - (void)play;
    @end
    
    @implementation ZJPerson (Study)
    
    - (void)read {
        NSLog(@"read");
    }
    
    - (void)play {
        NSLog(@"ZJPerson (Study) play");
    }
    @end
    
    @interface ZJPerson (Life)
    - (void)sleep;
    - (void)play;
    @end
    
    @implementation ZJPerson (Life)
    - (void)sleep {
        NSLog(@"read");
    }
    
    - (void)play {
        NSLog(@"ZJPerson (Life) play");
    }
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            ZJPerson *person = [ZJPerson new];
            [person play];
        }
        return 0;
    }
    

    ZJPerson类和它的分类里都有一个play方法,那么如果调用这个方法,会调用那个play方法呢?
    首先,根据前面的逻辑,分类的方法最后会合并在原有方法的前面,所以可以肯定的是,这个play方法会调用分类的的方法,然后,再根据前面的逻辑,先编译的分类数据会放在大数组的后面,后编译的分类的数据放在大数组的前面,所以这个play方法就是后编译的分类的play


    截屏2021-01-07 22.03.18.png

    可以看到life分类是后编译的,所以上面会输出life的play方法,我们运行看一看


    截屏2021-01-07 22.13.17.png
    我们在把study和life的编译顺序换一换
    截屏2021-01-07 22.14.09.png
    在运行一次看输出
    截屏2021-01-07 22.15.08.png

    面试题

    1. category的实现原理
      参见上面的category的加载处理过程总结
    2. category和class extension的区别
      extension在编译的时候,它的数据就已经合并在类信息中
      category是程序在运行时将数据合并到类信息中

    相关文章

      网友评论

          本文标题:OC底层原理(五):Category

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