前言

一、回顾readClass

二、附加分类信息 - attachCategories

三、rwe

四、attachLists流程(分类是如何添加到类的)

五、attachCategories 加载时机


了解类是如何从macho加载到内存中,以及从rw->ro的流程之后。我们继续从“分类的加载”开始探索。


一、回顾readClass

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized) {
    ....
}

我们在readClass打个断点,通过x/4gx 打印cls的内存。

(lldb) x/4gx cls
0x100008920: 0x00000001000088f8 0x00000001000088d0
0x100008930: 0x00000001003663b0 0x0000000000000000

这里有一个误区:0x100008938 对应的内容为0x0000000000。开始我以为这是bits,但是bit是通过首地址平移32个位置来确定的。所以bits对应的地址指针为0x100008940,bits并不为空的。


二、附加分类信息 - attachCategories

分类的结构和性质基本了解了之后,分类的信息是如何添加到类里面的呢?

我们回到methodizeClass方法中,看下面的代码:

//....
 // Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

通过断点调试,不会走到if(previously)的判断中去,会直接来到 objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS)

 void attachToClass(Class cls, Class previously, int flags)
    {
        
        const char *mangleName = cls->mangledName();
        const char *MyClassTest = "MyClassTest";
        if (strcmp(mangleName, MyClassTest) == 0) {
            auto ro = (const class_ro_t *)cls->data();
            auto isMeta = ro->flags & RO_META;
            if (isMeta) {
                printf("classLoad Meta - attachToClass: %s\n",MyClassTest);
            } else {
                printf("classLoad noMeta - attachToClass: %s\n",MyClassTest);
            }
        }
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));

        auto &map = get();
        auto it = map.find(previously);

        if (it != map.end()) {
            category_list &list = it->second;
            if (flags & ATTACH_CLASS_AND_METACLASS) {
                int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
                //给类附加分类信息
                attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
                //给元类类附加分类信息
                attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
            } else {
                attachCategories(cls, list.array(), list.count(), flags);
            }
            map.erase(it);
        }
    }

继续断点调试,根本没有进入if (it != map.end())这个判断条件里面去。
此时打印cls中ro的baseMethods,发现分类的方法已经被添加进去了。
这里说明分类有可能是在编译的时候就被添加到类里面去了(这里未来会通过探索llvm的源码来证明)。

那么这时候,可以发现,如果运行时要加载分类的信息,必将会走attachCategories这个方法,所以就先来研究这个方法,然后再反推是怎么进入这个方法的。
由于分类里面的协议、属性、方法处理的流程基本是一样的,所以只关注方法:
(核心函数)

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    /* * 只有少数类在启动时有超过 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循环里面获取数据
    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();
        }
            //...
    }
    //拿到for循环里面获取数据,设置rwe数据
    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){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }
    //...
}

基本流程如下:

1、定义了一个方法列表(二维数组mlists),其容量为64,因为很少类会在启动的时候有超过64个分类。

2、开辟rwe (auto rwe = cls->data()->extAllocIfNeeded())

2、先看循环的部分,这里是遍历一个分类,cats_count=1

3、获取到分类entry,通过entry.cat->methodsForMeta(isMeta)获取到分类的方法列表(我们这里先看实列方法列表,类方法同理)。

4、判断mcount(方法列表的数量)是否等于64,如果等于,进行方法排序,插入rwe,插入完之后将mcount=0,继续遍历。如果小于64,则进行倒叙插入( mlists[ATTACH_BUFSIZ - ++mcount] = mlist;)。




通过打印可以看到0x0000000100008498 被插入到了mlists的最后。

5、遍历结束之后,进行方法排序,插入rwe。

基本流程分析清楚之后,有个问题,一个是rwe的开辟时机,一个是attachLists的流程是什么。

三、rwe

通过以上流程,可以看到 auto rwe = cls->data()->extAllocIfNeeded(),开辟了rwe。

 //判断rwe是否存在,不存在就开辟
 class_rw_ext_t *extAllocIfNeeded() {
        auto v = get_ro_or_rwe();
        if (fastpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
        } else {
            return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
        }
    }

看下rwe的结构:

struct class_rw_ext_t {
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    class_ro_t_authed_ptr<const class_ro_t> ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
};

通过结构我们就可以知道,可以从中直接获取ro, 以及核心的成员就是methods、properties、protocols。
在attachCategories中去创建rwe,是因为要对本类进行数据添加,为了不去污染ro,所以单独开辟rwe来存放运行时添加的方法、属性、协议。
那么除了这里还有什么地方会进行rwe的创建呢?

我们全局搜索一下extAllocIfNeeded();
出现的地方如下:

1、attachCategories

2、获取demangledName的时候,那么demangledName是什么呢?

C++中,编译变量,类,及全局函数,编译后的名称,已经发生了改变,我们称之为添加修饰。那么,我们有什么方法将这种修饰,擦除掉呢? 通过demangled就可以实现。

3、class_setVersion,这是底层设置类的版本的时候,这个可以忽略,因为我们无法操控。

4、addMethods_finish 添加方法的时候

5、class_addProtocol 添加协议的时候

6、_class_addProperty 添加属性的时候

7、objc_duplicateClass 复制类的时候,这里也是我们无法操控的。

那么主要就是在分类加载、添加方法、添加协议、添加属性的时候会创建rwe。

四、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;
            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; // count 变成新的大小
            if (oldList) array()->lists[addedCount] = oldList;   //将原来的list放在最后面
            for (unsigned i = 0; i < addedCount; i++) //在前面插入新的list
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

1 、当list为空并且添加的长度为1时,将添加的list赋值给当前的list。

这种情况一般发生在创建完rwe,给rwe添加本类的方法、属性、协议。

class_rw_ext_t *
class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy)
{
    runtimeLock.assertLocked();
    

    auto rwe = objc::zalloc<class_rw_ext_t>();

    rwe->version = (ro->flags & RO_META) ? 7 : 0;

    method_list_t *list = ro->baseMethods();
    if (list) {
        if (deepCopy) list = list->duplicate();
        rwe->methods.attachLists(&list, 1);
    }

    // See comments in objc_duplicateClass
    // property lists and protocol lists historically
    // have not been deep-copied
    //
    // This is probably wrong and ought to be fixed some day
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }

    set_ro_or_rwe(rwe, ro);
    return rwe;
}

2、如果有一个list的时候,会开辟一个新的数组,然后把老的list放后面,新的list插入到最前面。这也就是为什么总会执行最后编译的分类方法。

3、hasArray()为true的时候(当继续添加分类的方法、属性、协议的时候),和第二种情况类似,同样是将之前的list放后面,新的list插入到最前面。

打印一下array()

可以看到array()里面保存了第一个分类的方法。

新加的方法都会在前面插入,苹果这样设计的目的我感觉就是LRU的思想。我们在平时添加分类方法就是因为有用价值高才去用。


五、attachCategories 加载时机

通过上面的分析,我们知道了加载分类的核心函数(attachCategories)以及分类如何添加到本类的核心函数(attachLists),那么attachCategories的加载时机是什么呢?
全局搜索 attachCategories函数在哪调用。

发现只有两个地方:attachToClass 和 load_categories_nolock

(1)、realizeClassWithoutSwift -> methodizeClass -> attachToClass

(2)、load_images -> loadAllCategories -> load_categories_nolock

load_categories_nolock在read_images中也存在,但是这里并没有调用

 if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }

经过测试,总结一下:

1、懒加载类 + 懒加载分类 :消息第一次调用的时候,加载类的数据 (编译时期就完成分类的加载)

2、非懒加载类 + 懒加载分类:read_image时,加载类的数据 (编译时期就完成分类的加载)

3、非懒加载类 + 非懒加载分类 :read_image时,加载类的数据 (分类的加载在运行时,流程为:load_images -> loadAllCategories -> load_categories_nolock -> attachCategories)

4、懒加载类 + 非懒加载分类: read_image时,加载类的数据,迫使类成为非懒加载类样式来提前加载数据。

分类加载有2种情况:
(1)当只有一个分类为非懒加载分类时:分类是在编译期完成加载的。
(2)当大于一个分类为非懒加载分类时:分类是在运行时完成加载的(流程是:read_images -> realizeClassWithoutSwift ->methodizeClass -> attachToClass -> attachCategories)