RunTime源码看分类

1,046 阅读9分钟

开始

分类在iOS中应用非常频繁,功能强大,对于方法自定义,解偶都挺有帮助. 虽然早就知道了其中的原理,源码也看过,但是人的大脑是有遗忘曲线的容易忘.写下来只有好处没有坏处

因为是看源码,代码居多,所以直接就在代码里添加注释了

分类显而易见的结论

iOS里面的东西一切都以运行时为准,直接上代码验证结果.

定义一个分类测试类,再创建两个分类,同时类和分类里面实现同一个方法,为了区别调用的是哪一个分类的方法,在log里面做了区分。

@interface XWCategoryTest : NSObject
- (void)test;
@end
@implementation XWCategoryTest
- (void)test {
    NSLog(@"XWCategoryTest cmd=%@",NSStringFromSelector(_cmd));
}

@end

@interface XWCategoryTest (Test1)
- (void)test;
@end
@implementation XWCategoryTest (Test1)
- (void)test {
    NSLog(@"XWCategoryTest (Test1) cmd=%@",NSStringFromSelector(_cmd));
}
@end

@interface XWCategoryTest (Test2)
- (void)test;
@end
@implementation XWCategoryTest (Test2)
- (void)test {
    NSLog(@"XWCategoryTest (Test2) cmd=%@",NSStringFromSelector(_cmd));
}
@end

Run! 顺便再贴上build phases里面的截图

1639204005008.jpg

运行结果如大家所料

1639204073494.jpg

看代码分析

使用clang命令看下Test1分类生成的c++代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XWCategory+Test1.m

在生成的cpp文件内搜索_category_t,找到以下几个看着跟分类相关的代码

// 分类结构体
struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

// 分类的方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_XWCategoryTest_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_XWCategoryTest_Test1_test}}
};

// 静态分类结构体变量
static struct _category_t _OBJC_$_CATEGORY_XWCategoryTest_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "XWCategoryTest",
    0, // &OBJC_CLASS_$_XWCategoryTest,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_XWCategoryTest_$_Test1,
    0,
    0,
    0,
};

static void OBJC_CATEGORY_SETUP_$_XWCategoryTest_$_Test1(void ) {
    _OBJC_$_CATEGORY_XWCategoryTest_$_Test1.cls = &OBJC_CLASS_$_XWCategoryTest;
}

主要看生成的静态结构体变量,里面的成员变量,第一个是const *char name 对应的XWcategoryTest,第二个0,看后面的注释, // &OBJC_CLASS_$_XWCategoryTest,,这里面存放的是分类的目标类的地址。因为这里只是使用clang编译了下文件,还没有到链接阶段分类内存地址,所以写了0。第三个是分类里面的方法列表地址,只有一个方法,所以unsigned int method_count 对应的数量是1

再把Test2文件转成cpp文件看一下

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_XWCategoryTest_$_Test2 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_XWCategoryTest_Test2_test}}
};

static struct _category_t _OBJC_$_CATEGORY_XWCategoryTest_$_Test2 __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
"XWCategoryTest",
0, // &OBJC_CLASS_$_XWCategoryTest,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_XWCategoryTest_$_Test2,
0,
0,
0,
};

static void OBJC_CATEGORY_SETUP_$_XWCategoryTest_$_Test2(void ) {
    _OBJC_$_CATEGORY_XWCategoryTest_$_Test2.cls = &OBJC_CLASS_$_XWCategoryTest;
}

找到了同样的东西,但还是给不出什么样的结论,只能说每一个分类在编译阶段都变成一个个静态结构体变量


接着去runtime中寻找最后的答案

我直接下载的最新objc4-818.2的源码,上次看还是7点几的,解压后打开 找到入口函数,在objc-os.mm文件中

1639206163558.jpg

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

因为编译runtime源码报错,点击跳不进去,所以直接全局搜索map_images

1639206364524.jpg

在objc-runtime-new.mm中找到了map_images,而且map_images里面只有一个map_images_nolock函数,点进去是个函数声明,老办法直接搜,在入口函数所在的文件中找到了实现 在这个函数最后位置找到函数

// 加载镜像
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);

这个函数里面代码挺多的,找到与分类有关的

    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }

didInitialAttachCategories 这个变量是个全局静态变量,只有在入口函数_dyld_objc_notify_register之后才会变成true,说明_read_images这个函数会调用多次。

这里顺带说明下,我们找这个方法的顺序不是代码运行的顺序

接着进入到load_categories_nolock

static void load_categories_nolock(header_info *hi) {

    // 是否有类属性
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    size_t count;

    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) {
            // 获取每一个分类
            category_t *cat = catlist[i];
            // 获取分类的目标类
            Class cls = remapClass(cat->cls);
            locstamped_category_t lc{cat, hi};

            // 目标类不存在
            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Ignore the category.
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category.
            if (cls->isStubClass()) {
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {

                // 首先,将分类注册到其目标类
                // 然后,如果类是存在的,则重建类的方法列表
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                // 处理对象方法列表
                // 是否存在对象方法列表,协议,属性
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    // 如果类目标类是存在的
                    if (cls->isRealized()) {
                        // 核心方法,将分类的信息附加到目标类中
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }

                // 处理类方法列表
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) {
                    // 核心方法,将分类的信息附加到目标元类中
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };

    // 处理所有的分类
    processCatlist(hi->catlist(&count));
    processCatlist(hi->catlist2(&count));
}

这个方法主要是获取一个目标类所有的分类并且遍历,为下一步的将分类方法合并到目标类中做准备,其中合并函数是attachCategories

// 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, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
    // 意思是在运行期间很少有类的分类数量会超过64个,分配内存就定了64这个size,如果项目中你建了超过64个分类,超过数量的会合并不过去,这是个很细的点
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];
    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    
    // 这是目标类所携带的信息
    auto rwe = cls->data()->extAllocIfNeeded();

    for (uint32_t i = 0; i < cats_count; i++) {
    // 取出分类
        auto& entry = cats_list[i];
    // 当前类如果是元类取出的就是类方法列表,反之取出来的是对象方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
        
        // 如果满64个
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                // 开始合并
                rwe->methods.attachLists(mlists, mcount);
                // 合并完成后置0,防止下面再走一遍
                mcount = 0;
            }
            
            // 没满64个的时候,将分类的方法列表按倒序存放在mlists中
            // 64-1, 64-2, 64-3,...
            // 注意这个地方,取出来的mlist是按cats_list中正序取出来的
            // 但是放在mlists中的时候是倒叙放进去的
            // 而cats_list中分类的存放顺序是根据编译的顺序决定的
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    // 如果没有64个分类
    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
                           
        //  mlists + ATTACH_BUFSIZ ,mlists 是这个数组首地址,向后偏移64
        //  mlists + ATTACH_BUFSIZ - mcount, 再向前偏移 mcount(分类的数量,一个分类有一个方法列表)
        // 开始合并
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

上面方法中auto rwe = cls->data()->extAllocIfNeeded();,cls->data()返回的是目标类的一些信息,这是在最新版的runtime中类的结构

这个函数中进入最后合并方法的时候,生成的所有分类方法列表的数组中,存放的顺序是倒序的,也就是说先编译的分类的方法列表在这个数组的后面,后编译的反而在前面


接着看合并函数 void attachLists(List* const * addedLists, uint32_t addedCount),传了两个参数,一个是将所有分类的方法列表数组传进来了,第二个是数量

    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;
            // 开辟一个newCount大小的内存的数组
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;
            
            // 假设原来的方法列表数量有5个,新增的分类方法列表数量也有5个
            // newArray->count就是10
            for (int i = oldCount - 1; i >= 0; i--)
                // newArray->lists[4 + 5] = array()->lists[4]; 
                // newArray->lists[3 + 5] = array()->lists[3]; 
                // newArray->lists[2 + 5] = array()->lists[2]; 
                // 将原来的方法列表在newArray中从最后一个依次放里面
                newArray->lists[i + addedCount] = array()->lists[i];
            for (unsigned i = 0; i < addedCount; i++)
                // newArray->lists[0] = addedLists[0];
                // newArray->lists[1] = addedLists[1];
                // newArray->lists[2] = addedLists[2];
                // 将分类的方法列表从第一个位置起放在newArray中
                newArray->lists[i] = addedLists[i];
            free(array());
            // 这步是将合并后新的方法列表设置为目标类的方法列表
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<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;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

到这里基本上是完了,结论正如大家所知道的那样,我们只不过是从源码的角度去看看苹果怎么实现的.

对比老版本的苹果是做了优化的,老版本的到_read_images前面都差不多,只不过要找remethodizeClass,点进去还是找attachCategories函数

// 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;
    // 也是采用倒序的方式,将所有的分类方法列表取出来重新放到mlists中
    // 注意,i--,倒序
    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;
            // 然后开辟内存,注意是realloc,是扩充内存
            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]));
        }
        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]));
        }
    }

老版本的runtime采用的是先将目标类原先的扩容,最后再将新增的分类方法列表拷贝到前面,因为前面函数中已经将分类方法列表倒叙排列了,所以最终结论还是一样。

新版本增加了一个类的分类数量的限制肯定是为了防止开发者滥用分类,内存管理方式也改变了,猜测可能性能上更好一点吧