sheet中没有getcolumns()方法吗_iOS:Category中的方法都在App启动时才添加吗?

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-
            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-                }
                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-                ||  cat-            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls-                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls-                                 classExists ? "on existing class" : "");
                }
            }

            if (cat-                /* ||  cat-) 
            {
                addUnattachedCategoryForClass(cat, cls-                if (cls-                    remethodizeClass(cls-                }if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls-                }
            }
        }
    }    //...
}

对于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>typenil, outCount);     \
    }                                                                   \
    type *name(const header_info *hi, size_t *outCount) {               \
        return getDataSection>typenil, 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中的数据:

f767824b8619e13ad7b4add928750967.png

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

aada6293c163228b7ce03905dc099b7c.png

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

d70bd6871a04a31113e27cfa3083d583.png

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

32e3690d0ffeb620e3be0e1a082c365e.png

上图可知: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文件:

c83b392f42043963ad92b45feebf51dd.png

果然,_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-        if (!cls) continue;  // category for ignored weak-linked class
        if (cls-            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls-        add_category_to_loadable_list(cat);
    }
}

_getObjc2NonlazyCategoryList的实现:

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

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

c7743e5c02109cd7d88a4c32ed896644.png

总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值