iOS:Category中的方法都在App启动时才添加吗?

前言:Category在Objc中非常重要,在平时的iOS的面试中针对Category的问题更是层出不穷,如:1)Category中的方法加载顺序?2)Category中的方法“覆盖”的原理?3)Category能添加属性吗?等等。更深入的可能会问Category中的方法是怎么加到方法列表中的?今天我们再来看看Category,还有没有新的发现。

一、从runtime看Category的加载

_objc_init 作为OC的入口,主要的工作是注册dyld的回调,当dyld中的image(镜像)卸载,加载状态(符号绑定完,静态初始化完)发生变化时给与OC回调。

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

    // 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_2_images);
    dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}

Category的加载发生在map_2_images回调函数中,即动态链接器(dyld)完成符号绑定时的dyld_image_state_bound的回调。这里我们省略掉中间的调用,最后在void _read_images(header_info **hList, uint32_t hCount) 中读取:

void _read_images(header_info **hList, uint32_t hCount)
{
    //...
    // Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        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;
            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  
                /* ||  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);
                }
            }
        }
    }    
   //...
}

对于Category_read_images做了如下事情:

1.通过_getObjc2CategoryList获取Categorylist;
2.将Category中的实例方法,协议,属性通过addUnattachedCategoryForClass映射到对应的类上,然后通过remethodizeClass添加到对应的类;
3.同上,将Category中的类方法,协议添加到类的元类上;
后续添加方法的实现在void attachLists(List* const * addedLists, uint32_t addedCount)中,大家应该比较熟悉了。

我们发现,Category是从_getObjc2CategoryList拿到的,我们看下它的实现:

#define GETSECT(name, type, sectname)                                   \
    type *name(const headerType *mhdr, size_t *outCount) {              \
        return getDataSection<type>(mhdr, sectname, nil, outCount);     \
    }                                                                   \
    type *name(const header_info *hi, size_t *outCount) {               \
        return getDataSection<type>(hi->mhdr, sectname, nil, outCount); \
    }

//      function name                 content type     section name
//...
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");
//...

很显然,_getObjc2CategoryList的作用就是从Mach-O文件的__objc_catlist段获取Category的数据。
好了,通过Runtime的源码我们不难理解Category的"加载"过程:当动态链接器(dyld)完成符号的绑定后,通过dyld_register_image_state_change_handler的回调到Objc,Objc通过_getObjc2CategoryListMach-O文件的__objc_catlist段获取Category的数据,然后先通过addUnattachedCategoryForClassCategory映射到对应的类上,最后通过remethodizeClass添加到对应的类。

二、就这?

从本文的题目不能猜出,Category应该还不止这些,实际上关于Category的原理我在N年前就读过源码。甚至产生过疑虑:Category中的方法在启动时添加明明会消耗性能,为什么不能在编译/链接期间就完成呢,苹果没这么傻吧...直到在研究Mach-O文件格式时有了点新的发现,下面我们通过Demo来看下,为了减少其他因素的干扰,我们创建一个最简单的MacOS的控制台程序,工程中创建一个Person类和它的分类。

//Person.h
@interface Person : NSObject
- (void)go;
@end

//Person+test.h
@interface Person (test)
- (void)goTest;
@end

运行下,使用MachOView看下Mach-O中的数据:

_objc_catlist

_objc_catlist中居然是空的,我们新增的Category方法goTest并没有保存在这里。Category的数据保存在哪里了呢?我们再看下保存类方法的__objc_const段:

__objc_const

从上图我们发现:Category中的方法已经和主类中的方法合并到方法列表中了并且内存地址连续,同时Category中的方法还"规规矩矩"的放在了主类方法的前面。如果是个同名方法呢?

同名方法

嗯...没错,和Category的特性一致...方法"覆盖"这时已经发生。
那么,这个优化发生在编译期还是静态链接期间呢?很简单,我们只需要把工程的类型Mach-O Type改成Static Libaray,因为静态库是经过编译,但还没链接的中间产物,同样通过MachOView看下数据:

Static Libaray

上图可知:Category和它的主类分别生成了一个.o目标文件,Category的信息还没有合并。从这里我们可以看出优化过程发生在静态链接期间,了解静态链接的同学可能会更容易理解,因为那时候才是真正给符号分配虚拟内存地址的时候。

好了,既然Category的方法保存到了__objc_const段,那_objc_catlist是摆设?或者说什么情况下会保存到_objc_catlist呢?我们可以想一下:既然静态链接的时候会进行优化,那么有没有什么情况给类添加Category的时候已经过了静态链接的时期呢?这种情况会不会保存到_objc_catlist呢?
答案是:动态库
动态库是已经经过链接(静态链接)后的产物,如果给动态库中的某个类添加Category应该不能优化了吧...这个猜想非常好验证,我们只需要给系统库中的类添加Category即可。

@interface NSString (Trim)
- (NSString *)trim;
@end

我们给NSString类添加trim方法,然后看下Mach-O文件:

_objc_catlist

果然,_objc_catlist已经有数据了,但是_objc_catlist中没有直接保存Category,真正的数据保存在_objc_constObjc2 Caterory段中。

三、还有?

本以为这就结束了,多谢 @皮拉夫大王在此的提示:“分类未实现load和实现了load方法后保存是不一样的”。我们知道在load方法的调用时会先调用当前镜像中的所有类的load方法,然后再调用分类的load方法,调用是分开的。再者,load方法作为特殊的存在,如果像上文所述进行优化的话,比较困难:如果优化了怎么方便的调用呢?这种情况苹果索性新增了一块区域保存,我们看下runtime中获取分类中load方法的代码:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

_getObjc2NonlazyCategoryList的实现:

GETSECT(_getObjc2NonlazyCategoryList, category_t * const,    "__objc_nlcatlist"); 

实现了load方法的Caterory__objc_nlcatlist__objc_catlist中都保存一份:

__objc_nlcatlist

总结

从上面的分析我们可以看出苹果对Caterory的优化已经非常细致:对于非动态库中的类的Caterory中的方法会在静态链接期间优化,知道这个特性后我们可以在自己的模块内使用Caterory干更多的事情(比如功能解耦),不用担心会影响启动速度。最后在这里再抛出一个问题:苹果既然对Caterory中的方法进行了优化,那其他的特性(如"属性")呢?

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