美文网首页
带着问题深入了解Category底层实现

带着问题深入了解Category底层实现

作者: 费宇超 | 来源:发表于2018-03-02 21:01 被阅读88次

    引子

    有道常见的面试题:为什么分类中无法定义实例变量?
    答案很简单:每个类的内存布局在编译时期就已经确定了,运行时才加载的category无法添加实例变量,如果添加实例变量就会破坏类的内部布局...说是那么说,
    但是问题来了。。。
    1:为什么说category是在运行时加载的?
    2:不能添加实例变量,那为什么能添加属性?(关键对象)
    总不能人云亦云吧,那么我们怎么来验证它?记住一句话:在runtime源码面前一切秘密无所遁形。

    先看catagory的在runtime里的结构体长什么样子

    struct _category_t {
        const char *name; // 1
        struct _class_t *cls; // 2
        const struct _method_list_t *instance_methods; // 3
        const struct _method_list_t *class_methods; // 4
        const struct _protocol_list_t *protocols; // 5
        const struct _prop_list_t *properties; // 6
    };
    

    1:category小括号里写的名字
    2:要扩展的类对象,编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象
    3:这个category所有的-方法
    4:这个category所有的+方法
    5:这个category实现的protocol,比较不常用在category里面实现协议,但是确实支持的
    6:这个category所有的property,这也是category里面可以定义属性的原因,不过这个property不会@synthesize实例变量,一般有需求添加实例变量属性时会采用objc_setAssociatedObject和objc_getAssociatedObject方法绑定方法绑定,不过这种方法生成的与一个普通的实例变量完全是两码事。

    其实看到这里已经能回答第二个问题了。那么第六结论是如何得出的呢?就需要继续往下看。
    OC的编译器是clang 并非gcc 我们先将一段category的代码用clang 重写一下

    clang -rewrite-objc Gog.m
    

    精简之后的关键代码

    static struct _category_t _OBJC_$_CATEGORY_Gog_$_Extention __attribute__ ((used, section ("__DATA,__objc_const"))) =
    {
      "Gog",
      0, // &OBJC_CLASS_$_Gog,
      (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Gog_$_Extention,
      0,
      0,
      0
    };
    
    
    static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
        &_OBJC_$_CATEGORY_Gog_$_Extention,
    };
    

    我们可以看出
    1: OBJC$CATEGORY_Gog$Extention category+类+扩展名的特地样式组合。
    2:{}中的method_list_t存放了我们添加的方法。其他的列表若有也会存放对于的数据。
    3:最后,编译器在DATA段下的objc_catlist section里保存了category_t的数组L_OBJC_LABELCATEGORY$(当然,如果有多个category,会生成对应长度的数组^
    ^),用于运行期category的加载。

    再看catagory如何添加进runtime

    之前遇到过一个需求:AOP一个category的方法,那AOP需要在+load方法里写,load函数又在main之前。那么category必然也是在main之前起作用的。
    事实上,在main函数之前,将runtime通过dyld动态加载进来的时候生效的。怎么验证,再来看runtime源码:
    先从objc_init开始,其中大量出现的image并不是图片,而是一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,所以 ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。

    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();
        lock_init();
        exception_init();
    
        // Register for unmap first, in case some +load unmaps something
        _dyld_register_func_for_remove_image(&unmap_image);
        dyld_register_image_state_change_handler(dyld_image_state_bound,
                                                 1/*batch*/, &map_images);
        dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
    }
    

    在load_images之后调用_read_images方法初始化map后的image,这里面干了很多的事情,像load所有的类、协议和category。
    再仔细看category的初始化:

    // Discover categories. 
        for (EACH_HEADER) {
            category_t **catlist =
                _getObjc2CategoryList(hi, &count);
            for (i = 0; i < count; i++) {
                category_t *cat = catlist[i];
                class_t *cls = remapClass(cat->cls);
                // 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 (isRealized(cls)) {
                        remethodizeClass(cls);
                        classExists = YES;
                    }
                }
    
                if (cat->classMethods  ||  cat->protocols 
                    /* ||  cat->classProperties */)
                {
                    addUnattachedCategoryForClass(cat, cls->isa, hi);
                    if (isRealized(cls->isa)) {
                        remethodizeClass(cls->isa);
                    }
                    if (PrintConnecting) {
                        _objc_inform("CLASS: found category +%s(%s)",
                                     getName(cls), cat->name);
                    }
                }
            }
        }
    

    __objc_catlist,就是上面category存放的数据段。
    以上代码做的事:
    1把category的实例方法、协议以及属性添加到类上。
    2把category的类方法和协议添加到类的metaclass上。
    具体怎么做,主要是两个方法addUnattachedCategoryForClass和remethodizeClass。
    addUnattachedCategoryForClass实现映射,remethodizeClass去做具体操作。
    再往下看category的各种列表是怎么最终添加到类上的。
    点开attachCategoryMethods方法可以看到它将所有category的实例方法列表拼成了一个大的实例方法列表,再通过attachMethodLists去加到方法列表里

    static void 
    attachCategoryMethods(class_t *cls, category_list *cats,
                          BOOL *inoutVtablesAffected)
    {
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
    
        BOOL isMeta = isMetaClass(cls);
        method_list_t **mlists = (method_list_t **)
            _malloc_internal(cats->count * sizeof(*mlists));
    
        // Count backwards through cats to get newest categories first
        int mcount = 0;
        int i = cats->count;
        BOOL fromBundle = NO;
        while (i--) {
            method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;
                fromBundle |= cats->list[i].fromBundle;
            }
        }
    
        attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected);
    
        _free_internal(mlists);
    
    }
    

    结论:
    1)category的方法没有“完全替换掉”原来类已经有的方法,而是将扩展的方法插到方法列表的前头,比如原方法列表<4,5,6,>,扩展的方法<1,2,3>,会变成<1,2,3,4,5,6>。
    2)这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会结束。

    最后为什么通过关联对象的方式能添加属性?

    一句话解释就是:关联对象添加的属性并不是加到这个一个对象的内存中的,关联对象的内存存储有一个专门的地方统一管理,它的作用就是添加“伪属性“。如我们在项目中如此添加:

    static NSString *imgNameKey = @"imgNameKey";
    
    @implementation UIImageView (Attchment)
    
    @dynamic imgName;
    -(void)setImgName:(NSString *)imgName
    {
        objc_setAssociatedObject(self, &imgNameKey, imgName, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)imgName
    {
        return objc_getAssociatedObject(self, &imgNameKey);
    }
    

    相关文章

      网友评论

          本文标题:带着问题深入了解Category底层实现

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