iOS 底层原理探索 之 分类的加载

1,256 阅读9分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载

以上内容的总结专栏


细枝末节整理


前言

上一篇章我们探索了类的加载过程,在最后有探索到分类的加载过程,但是并没有探索完,我们结合了编译后的cpp文件,探索了分类的底层结构以及它的_method_list_t ,接着对于 rwe 的操作我们探索来到了 extAllocIfNeeded 方法的调用。今天,我们接着 关于 rwe 部分,开始今天的内容。

ro - rw - rwe

首先我们从 为什么要有 rorwrwe 来开始今天的内容。

类的底层原理结构 系列探索文章中,我们有探索过类的结构和它的底层原理,今天,我们先复习下这部分的知识。

来自苹果WWDC2020关于 Objective-C 运行时做出的更改

  1. Objective-C运行时的进步
  2. Objective-C运行时的进步 - 译文

ro代表只读

在磁盘上,在您的应用程序二进制文件中, 首先是类对象本身,它包含最常访问的信息:指向元类、超类和方法缓存的指针。它还具有指向存储附加信息的更多数据的指针,称为 class_ro_t

这包括类的名称以及有关方法、协议和实例变量的信息。Swift 类和 Objective-C 类共享此基础结构,因此每个 Swift 类也具有这些数据结构。当类第一次从磁盘加载到内存中时,它们也是这样开始的,但是一旦使用它们就会改变。

rw用于读/写数据

因此,当一个类第一次被使用时,运行时会为其分配额外的存储空间。这个运行时分配的存储是 class_rw_t,用于读/写数据。 在这个数据结构中,我们存储仅在运行时生成的新信息

由于 class_ro_t只读的,我们需要在 class_rw_t 中跟踪这些东西。 现在,事实证明这占用了相当多的内存。我们在 iPhone 的整个系统中测量了大约 30 MB 的这些 class_rw_t 结构。

那么,我们怎样才能缩小这些呢? -- rwe

请记住,我们在读/写部分需要这些东西,因为它们可以在运行时更改。 但是检查在真实设备上的使用情况,我们发现只有大约 10% 的类实际上改变了它们的方法。 并且这个 demangled name 字段只被 Swift 类使用,甚至不需要 Swift 类,除非有什么要求他们的 Objective-C 名称。

因此,我们可以将不常用的部分拆分出来,这样可以将 class_rw_t 的大小减少一半。 对于确实需要附加信息的类,我们可以分配这些扩展记录之一并将其滑入以供类使用。 大约 90% 的类从不需要这些扩展数据,节省了大约 14 兆字节的系统宽度。

更多的内容详见 Objective-C运行时的进步 - 译文

attachCategories

rwe 的数据是在运行时为了节省内存 系统对于 rw 中不常用的部分拆了出来,所以必然是运行时动态的对其进行操作。结合我们要探索的分类, 所以,目标来到这里。

// 将类别中的方法列表、属性和协议附加到类中
// 假设分类中的类别都已加载并按加载顺序排序 
// 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个类别
     * 这使用了一个小堆栈,并避免了malloc
     *
     * 类别必须按正确的顺序添加,返回
     * 前面,为了使用分块实现这一点,我们迭代cats_list
     * 从前面到后面,向后建立本地缓冲区
     * 并在块上调用attachLists。attachLists突出显示的
     * 列表,因此最终结果按照预期的顺序
     */
    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) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            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;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        // *******
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        // *******
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // 常量缓存已在prepareMethodLists中处理
                // 如果这个类在这里仍然是常量,那么保持不变是可以的
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

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

到现在我们并不知道分类是什么时候加载的,所以,通过反推法看都是在哪里有对 这里 的调用。来推断出 分类的加载时机。

全局有两处调用 :load_categories_nolockattachToClass

继续向上推到 attachToClass:

    // Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // 当一个类重新定位时,用类方法分类
            // 可以注册在课程本身而不是
            // 元类,告诉attachToClass寻找这些
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

previously参数的影响 methodizeClass方法的参数来自realizeClassWithoutSwift方法的参数 (看到这里的两个方法,就可以断定分类的加载和类的加载有十分着紧密的联系),realizeClassWithoutSwift 方法调用的时候参数 previously 传的 nil (previously 参数 为系统内部调用的方便自己调试用)。

上一篇的分析我们知道,类的加载时机与其是否是懒加载类也就是 +load 方法的实现与否有关。 那么,接下来会分为几种情况,我们分别断点调试看下调用流程:

image.png

// SMPerson

@protocol SMPersonDelegate <NSObject>

@optional
- (void)say666;

@end

@interface SMPerson : NSObject <SMPersonDelegate> {
    NSString *subName;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *like;

- (void)doWork:(NSString *)content;
- (void)gotoTalk;
- (void)likeGirls;

- (void)likeGirls1;
- (void)likeGirls53234213;
- (void)likeGirls312342351231;
- (void)likeGirls43453456341;
- (void)likeGirls51345123;
- (void)likeGirls6123421352136;
- (void)likeGirls7123412341234;

+ (void)doIT;

@end
// ( Like )
// 几个分类方法类似不在逐个贴出来

@interface SMPerson (Like)

@property (nonatomic, copy) NSString *like_name;
@property (nonatomic, assign) int like_time;

- (void)likeThing1231242134;
- (void)likeThing242134;

+ (void)likeThing_ksdanf;

@end

类和分类都实现了 load 方法

分类加载流程:

load_images --> loadAllCategories --> load_categories_nolock --> attachCategories(有多个分类会依次调用)

image.png 向前查找之后来到了 load_categories_nolock 方法,

image.png

好的,我们继续在 attachCategories 中的流程:

image.png

attachLists

rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);

attachLists 分为三部分:

  1. 0 lists -> 1 list 一维数组
  2. 1 list -> many lists
  3. many lists -> many lists
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;
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            for (unsigned i = 0; i < addedCount; i++)
                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();
        }
    }

第一次,我么来到 了 0 lists -> 1 list 分支,随后来到 rwt 的赋值部分:

image.png

因为我们有5个分类,所以,第二次,来到了 1 list -> many lists 分支:

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

看代码有点枯燥,我们画个图吧:

1 list -> many lists2.001.jpeg

到了这一步后,我们继续向下进行,因为我们分类多,这里并没有完成全部的添加, 接着是来到的类方法的添加流程:

image.png 我们继续:这次是分类方法的添加,我们来到的是 many lists -> many lists 分支:

// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;

for (int i = oldCount - 1; i >= 0; i--)
    newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++)
    newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();

流程操作类似我们上一个分支 1 list -> many listsoldCount = 2

image.png

之后的流程会一次走到此分支,知道分类全部加载完毕。

分类实现了 load 方法 类没有

加载流程:

_read_images --> realizeClassWithoutSwift --> methodizeClass --> attachToClass

调试信息: 虽然 SMPerson 没有实现 load 方法,但是,依然来到了非懒加载的流程中:

image.png 而且,我们是在 Like 分类中实现的 load 方法,在这里把 Thind 分类中的方法也能打印出来,说明只要有一个分类实现了load方法,那么整个类的所有分类就都会被加载。 image.png

类实现了 load 方法 分类没有

加载流程:

_read_images --> realizeClassWithoutSwift --> methodizeClass --> attachToClass

这里的加载过程和分类实现了load方法,类没有实现是一样的,同样在 methodizeClass 中,所有的分类和类的方法全部已经加载好了。

类和分类都没实现了 load 方法

加载时机别推迟到第一次发送消息的时候进行初始化,且也是将类和分类中的方法全部加载。

image.png

总结

类和分类本身就联系很紧密,所以其加载的流程必然也是联系在一起的,通过今天的探索学习,我想大家和我一样对于类的加载以及分类的加载,以及两者之间加载的相互联系能有一个清晰的认识。大家,加油!