Runtime--Category的加载

183 阅读6分钟

搜一下面试相关,发现都会问到分类,也就是Category的知识点,整理一下,以备使用。😄

基本信息

分类,英文名Category。Objective-C 2.0之后添加的语言特性。

  • 作用

    动态的为已有类扩充对象方法、类方法、协议和属性。

  • 优点

    减少单个文件的体积

    将不同的功能组织到不同的Category

    按需加载需要的Category

如何添加?

创建文件时,File Type选择Category。创建完成后文件名称为AA+B.h

Category的加载

在App启动一章中指出,Category是在main之前,runtime层进行加载的。下面开始深扒代码。 深扒之前,先列出category的结构

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

现在打开runtime源码,先找到runtime初始化入口_objc_init

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(); //【C++静态构造函数】
    lock_init(); //【锁】
    exception_init(); //【异常】
    //【重点在这里】map_images,用于加载资源模块
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

进入到map_images

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

继续。

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    .....
    //【前面的内容不展示】,直接来到重点地方
    if (hCount > 0) {
        //【读取镜像模块】
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    firstTime = NO;
}

现在进入到加载部分,只展示主要代码。_read_images的作用是把对应section里的数据取出,加工,然后添加到缓存中。

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    //【直接来到分类加载的位置】
    for (EACH_HEADER) {
        //【这里是个二维数组】
        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) {
                .....省略代码
            }

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

remethodizeClass方法中,具体调用的是attachCategories,我们直接看attachCategories方法

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;
    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方法中,查看。addedLists中的数据,来自category

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(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            //!!!【重点】【复制分类的内容】
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        .....
    }

Q&A

问题一:两个分类中都实现了A,最后调用的是谁的?

如果有两个分类均实现了同样的方法A,对A的调用,取决于分类的编译顺序,最后编译的那个分类的同名方法最终生效。【代码中倒序取分类】

问题二:分类和原有类都有的A,调用的是谁的?

分类,因为分类的方法在原有类的方法之前,原有类的方法并没有被覆盖,只是顺序问题。

问题三:关于分类中添加属性

分类中可以添加属性,但是,属性并不能自动生成成员变量,也没有set和get方法。因为分类是在编译后运行时加载,此时对象的内存布局已经确定,无法将成员变量添加到实例对象的结构体中。这就引申出下面一个问题。

问题四:能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?

不能。编译后,类已经注册在runtime中,类结构体中的实例变量的内存大小已经确定。

可以。运行时创建的类,添加实例变量,是在objc_allocateClassPair之后,objc_registerClassPair之前。

问题五:分类能添加成员变量吗?

不能。只能通过关联对象(objc_setAssociatedObject)来模拟实现成员变量。

在objc2之前的结构体objc_class中,成员变量和方法列表定义如下:

struct objc_ivar_list *ivars; //【成员变量列表指针】           
struct objc_method_list **methodLists; //【二维数组】

ivars是objc_ivar_list(成员变量列表)指针;methodLists是指向objc_method_list指针的指针。在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。

在objc2之后,查看class_rw_t中的class_ro_t,class_ro_t中存放的是编译时就确定的内容,可以看到

const ivar_list_t * ivars;

问题六:如果声明了两个同名的分类会怎样?

会报错,所以第三方的分类,一般都带有命名前缀

问题七:分类和扩展的区别

扩展只能以声明的形式存在,多数情况下寄生在宿主类的.m中。一般用于声明私有方法、私有属性、私有成员变量。

举个常用的例子:

.m文件中

@interface ViewController ()
@end
@implementation ViewController
@end

这就是一个扩展,编译期就已经决定。

补充

_read_images方法中,有一个重要的方法realizeClassWithoutSwift,用于初始化class的结构体。 详细见文章iOS 底层探索 - 类的加载