Category的本质(二)运行时如何将方法和属性进行捆绑

上一篇文章中,我们了解到,在编译阶段,每一个Category都是一个独立的结构体,其中包含实例方法、类方法、属性和遵循的协议。具体内容可参看
Category的本质(一)编译阶段都做了什么?
今天,我们来看一下在运行时,是如何将方法、属性和协议绑定到原有的类中,供我们调用。苹果官方给我们提供了objc源码,可以通过这个链接进行下载:ojbc源码
注意标号最大的是最新版本,写这篇文章时,最新的编号为756.2,也就是最新版本,大家可以下载阅读。
打开源码后,整个项目其实是不能直接运行的,因为牵涉到很多依赖库,配置起来可能需要个把小时左右,但是这并不影响我们阅读Category在运行时所做的事情。

  • 首先我们通过runtime源码,来验证一下我们在上篇文章,通过clang编译后看到的Category的结构体,是否和runtime的实现是一致的。
    由于我们了解到Category的实现是一个category_t结构体,在项目中搜索 Category_t关键字,发现在objc-runtime-new.h中,有如下实现:
struct category_t {
    const char *name; ///类名
    classref_t cls;       ///类指针
    struct method_list_t *instanceMethods;  ///实例方法列表
    struct method_list_t *classMethods;      ///类方法列表
    struct protocol_list_t *protocols;          ///遵守的协议
    struct property_list_t *instanceProperties; /// 实例属性列表
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;    /// 类属性列表

    method_list_t *methodsForMeta(bool isMeta) { ///获取实例方法列表或类方法列表
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);   ///获取实例属性列表和类属性列表。
};

可以看到结构体中的成员和我们通过clang编译器得到的结构体是完全一致的,只是多了两个方法,一个是获取方法列表,一个是获取属性列表。通过对比,以后有什么疑问的话,就可以放心大胆地通过clang来编译OC代码,查看对应的C++代码,剖析本质。
记得我们的命令哦:

xcrun -sdk -iphoneos clang -arch arm64 -rewrite-objc xxx.m  -o ---.cpp
  • 下面我们沿着runtime的执行顺序,解读一下Category的实现。
  1. 在Source目录下,找到objc-os.mm文件,这是入口文件,对应的objc_init方法就是引导初始化,向dyld(动态链接库)注册镜像文件。
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注册镜像文件
    _dyld_objc_notify_register(&map_2_images, load_images, unmap_image);
}

然后查看注册方法,如果是初次阅读runtime源码,可能会有很多疑问,为了减少不必要的疑虑,我把查找顺序梳理了一下,大家可以按照这个顺序,直接找到Category的执行方法:


查找Category实现.png

关键代码:

// Discover categories. 
    for (EACH_HEADER) {
        ///二维数组
        ///【【instance_method_list,class_method_list,property_list,protocol_list】
        ///   【instance_method_list,class_method_list,property_list,protocol_list】
        ///   【instance_method_list,class_method_list,property_list,protocol_list】
        ///   【instance_method_list,class_method_list,property_list,protocol_list】
        ///   【instance_method_list,class_method_list,property_list,protocol_list】】
        ///由于一个类可以有多个Category,
        ///每个Category中包含属性列表、实例方法列表、类方法列表和协议列表。
        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.
            ///向原有类注册Category方法、属性、协议
            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);
                }
            }
        }
    }

代码中的关键部分,已经逐行加了注释,大家可以慢慢阅读。
到了这里,就开始对实例方法和类方法进行分别处理。类方法附加在元类对象(MetaClass)中,实例方法附加在类对象(Class)中。
我们下面以将实例方法添加到类对象为例,进行讲解。属性、协议可以类比。
关键代码在remethodizeClass中,将Category中的方法、属性、协议附加到原来的类中,同时更新缓存列表。

/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    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()方法,将Category附加到cls中,按照编译顺序,最后参与编译的方法将会放在最前面。同时,在将方法添加到类中以后,更新方法缓存列表。

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
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;
    ///从最后一个元素开始遍历,如果多个Category中含有同名方法时,
    ///最终导致的结果就是,最后参与编译的方法优先执行
    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);
}

其中还有一个比较重要的方法 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(dest, source, count)
        //将array-list()即第一个元素开始的oldCount个元素移动到array->list+addedCount的位置
        //即将队首的元素移动到队尾,开头空出addedCount个元素
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        //赋值
        //把addedList移动到队首,占据addedCount个位置
        //即Category中的方法/属性/协议添加到列表开始的位置
        //这就是我们使用Category时,如果添加和原有方法同名方法时,会优先执行我们通过Category添加的方法的原因了。
        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]));
    }
}

关键部分都有注释,希望大家可以看得明白。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容