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

2018-03-02  本文已影响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);
}
上一篇下一篇

猜你喜欢

热点阅读