OC底层 - 类的加载(3)
前言
一、回顾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)