美文网首页
hello category

hello category

作者: litt1err | 来源:发表于2017-12-26 16:56 被阅读18次

    事由:今年去面试,然后面试官问了我一些关于runtime的用法,我有说到Method Swizzling。通过在categoryload中去修改我们调用的方法,来达到全局修改的目的。随后面试官问到关于category的实现,哇! 尴尬,我好像从来没有想过这个问题。现在有时间就给整理一下,水平有限,肯定会有很多不足。希望大家多多指点!多谢 zzz

    </br>

    categoryObjective-C 2.0之后添加的语言特性. 一般我们使用它有以下两种场景

    • 给系统类添加方法和属性
    • 通过组合的设计模式把类的实现分开成多个category在几个不同的文件里面
      • 可以减少单个文件的体积
      • 可以把不同的功能组织到不同的category
      • 可以由多个开发者共同完成一个类
      • 可以只加载自己想要的category,达到业务分离

    </br>

    关于问题(因为确实不知道该从什么地方开始看起,所以强迫试的给自己定了几个问题。让自己去弄明白)

    • category是什么东西
    • category是怎样加载的
    • category方法为什么可以覆盖宿主类的方法
    • category的属性跟方法是怎么添加到宿主类的
    • category为什么可以添加属性,方法,协议。缺不能添加成员变量

    </br>

    category是什么东西

    objc所有类和对象都是c结构体,category也一样。我们可以通过clang去看一下

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

    _category_t里面有名字、宿主类的对象、实例方法列表、类方法列表、协议方法列表、属列表性。可以看到是没有成员变量列表的,因为category是依赖runtime的,而在运行时对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。这就是为什么我们没有在_category_t里面找到成员变量列表和category不可以添加成员变量的原因

    </br>

    category是怎样加载的

    上面我们提到过category是依赖runtime的。那我们来看一下runtime的加载过程。下面用到的runtime源码都来自于点这! 我下载的是723最新的版本

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

    </br>

    开始是一些初始化方法

    map_images方法表示将文件中的二进制文件映射到内存,category被添加到宿主类就发生在这个方法里面。我们来看一下这个方法实现

    void
    map_images(unsigned count, const char * const paths[],
               const struct mach_header * const mhdrs[])
    {
        // 加锁操作 保证在映射到dyld过程调用ABI是安全的
        rwlock_writer_t lock(runtimeLock);
        //函数在加锁后就转向了 map_images_nolock 函数
        return map_images_nolock(count, paths, mhdrs);
    }
    

    map_images_nolock方法代码太长,我就不粘过来了。它主要做的操作是在函数中,它检查传入的每个 image,如果 image有需要的信息,就将它记录在hList中,并将hCount 加一,最终判断 hCount>0来调用_read_images读取 image 中的数据 。

    </br>

    我们再来看_read_images,方法有点长。我给跟category相关代码截取出来了

    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;
                // 从这开始,正式对category开始处理
                if (cat->instanceMethods ||  cat->protocols  
                    ||  cat->instanceProperties) 
                {
                    //为类添加未依附的分类,把Category和类关联起来
                    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)) 
                {
                    //为类添加未依附的分类,把Category和metaClass关联起来。因为类方法是存在元类中的
                    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);
                    }
                }
            }
        }
    

    </br>

    哇! 终于 终于 到了最关键的方法了</br>
    </br> 首先。我们调用remethodizeClass来调用category的幕后大佬----attachCategories方法,从名字就可以看出来它的作用,添加category

    </br>

    plz show me the code !!!

    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
        /*
         #warning attachCategories
         
         category的加载顺序是通过编译顺序决定的
         
         这样倒序遍历,保证先将数组内元素(category)的方法从后往前添加到新数组
         
         这样编译在后面的category方法会在数组的前面
         
         */
        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);
    }
    

    </br>

    得到重新组合的数组在调用attachLists方法

    </br>

    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;
            
            //C 库函数 void *realloc(void *ptr, size_t size) 尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            /*
             1.memmove
             
             函数原型:void *memmove(void *dest, const void *source, size_t count)
             
             返回值说明:返回指向dest的void *指针
             
             参数说明:dest,source分别为目标串和源串的首地址。count为要移动的字符的个数
             
             函数说明:memmove用于从source拷贝count个字符到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中。
             
             
             
             2.memcpy
             
             
             
             函数原型:void *memcpy(void *dest, const void *source, size_t count);
             
             返回值说明:返回指向dest的void *指针
             
             函数说明:memcpy功能和memmove相同,但是memcpy中dest和source中的区域不能重叠,否则会出现未知结果。
     
             3.两者区别
             
             函数memcpy()   从source  指向的区域向dest指向的区域复制count个字符,如果两数组重叠,不定义该函数的行为。
             而memmove(),如果两函数重叠,赋值仍正确进行。
             
             memcpy函数假设要复制的内存区域不存在重叠,如果你能确保你进行复制操作的的内存区域没有任何重叠,可以直接用memcpy;
             如果你不能保证是否有重叠,为了确保复制的正确性,你必须用memmove。
             */
            
            /*
             这样就完成将category方法列表里面的方法 加到 class的方法列表里面而且是前面。等到我们再去调用class的方法时候,我们通过去遍历class的方法列表去查到SEL,找到就会调用相应方法。由于category的方法在前面---导致所以会覆盖宿主类本来的方法(这就是为什么category方法的优先级高于宿主类方法)
             属性和协议同理!!!!!!!!!!!
             */
            
            //相当于给array()->lists 重新放在起始地址 = array()->lists的起始地址 + addedCount
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            
            //相当于给addedLists 这个category新数组加到array()->的起始地址
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
            
        }
    

    </br>

    以上应该可以回答,之前提的所有问题。如有疑问,可以联系我。




    笔者是一个刚入门iOSer

    这次关于category的管中窥豹,一定有很多的不足,希望大家不吝赐教!

    有任何问题可以留言,或者直接联系QQ:346658618

    希望可以相互学习,一起进步!

    相关文章

      网友评论

          本文标题:hello category

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