本章内容
- 分类的本质
- rwe开辟赋值
- 分类加载流程
- 五种情况分析类与分类实现load方法
本章目的
知道分类的底层结构,以及知道分类加载流程。
分类的本质
分类在OC底层的原型是什么?其实我们能很容易理解的,想想看如果说分类在底层没有实现的话又是靠什么进行存储呢?所以研究底层也是很有必要的,分类在底层实现就是结构体。我们可以通过还原CXX文件进行查看,甚至也可以看objc源码进行探究。但是为了更加直观的查看我推荐第一种方式,clang还原方式,当然我们也会展示objc源码
先看源文件,然后看下面两种方式对比
clang还原
- 查看分类本质结构体
category_t
- 查看CXX 文件分类的实现。看到下面或许会有疑惑为什么是name被clang为Person。其实你大可不必有次疑惑,因为我们是clang还原,而clang是编译时LLVM的东西它在此时只是给个匿名并不是真实名字。
分类objc源码
可以看到与clang还原的基本一致,但是我所标出的类属性
肯定会有人疑惑,什么是类属性,OC还有类属性?其实是有的,但是对于类属性的意义是什么?你可以理解为 调用为Person.name
,但是类属性就与其他语言一样虽说调用权限是该类的,但是实际内存是全局的(实例属性的实现方式:ivar+setter+getter,而类属性其实是调用的setter+getter方法为全局变量赋值)。如果说想了解可以看我的补充。
补充类属性
rwe的开辟
其实这块并没有什么看的,但是为了解决上一章遗留的问题所以单独拎出来。先看下rwe的结构是怎样的。很简单只有一些成员变量,我们既然要找rwe的开辟,首先肯定需要知道它的初始化方法
rwe初始化
我们知道rw包含了rwe,而我们可以在其rw中找找看看,果然有一个方法class_rw_ext_t *extAllocIfNeeded()
是去初始化rwe的。
rwe初始化的条件
既然我们已经找到其初始化方法,就可以全局搜索objc在那些函数中调用,就可以明白了。然后我们发现有7个函数调用其初始化,但是我们所关心的是什么?也就是说有分类,添加方法,协议,属性等时候rwe就会自行开辟,这也与WWDC2020(推荐看)所符合
attachCategories
分类相关demangledName
看其源码不重要class_setVersion
类设置版本号api,不重要,里面只是对rwe进行version赋值addMethods_finish
添加方法class_addProtocol
添加协议_class_addProperty
添加属性objc_duplicateClass
这个api是苹果自行调用的,用户不能自行调用,KVO相关。或许探究KVO底层的时候你会有所发现?这里并不多说,提一句KVO系统自行创建一个子类
分类加载流程
从上面我们已经得知了一个重要因素,rwe的开辟条件之一是由于分类的缘故,而我在类的加载中说过,类的加载与分类加载也有一定的渊源。不知道你看过这篇文章是否注意到attachToClass
这个函数。但是我们先不这么去探索,我们从attachCategories
逆向去探索,就是谁调用了它。发现其中之一有attachToClass
,还有一个load_categories_nolock
函数不知道你是否注意到过在load_images流程中。也就是说我们已经确定了两个
load_categories_nolock
就是load_images流程中的loadAllCategories
函数调用。attachToClass
就是在map_images流程中的methodizeClass
函数调用。
提示:如果说你对我上面的分析感觉迷惑,那么先记得load_images和map_images流程都有对分类的处理。也就是说我们加载类(map_images非懒加载类)的时候可能会有某种情况对于分类有所处理。而调用load方法的时候也会有此处理。(该描述不准确,希望你继续往下看)
attachToClass 分析
在map_images流程中该函数是必进的。但是我后来运行时测试几次发现从这个函数根本没有进入过attachCategories
函数,但真的是这样吗?其实我又进行测试发现它在某一种情况会进入。总体来说这个函数不重要,跟函数名一样,附加到类中,中间方法而已
void attachToClass(Class cls, Class previously, int flags)
{
runtimeLock.assertLocked();
ASSERT((flags & ATTACH_CLASS) ||
(flags & ATTACH_METACLASS) ||
(flags & ATTACH_CLASS_AND_METACLASS));
auto &map = get();
auto it = map.find(previously);
/** 是为了做测试用的,证明来的是我们创建的类
bool isMeta = cls->isMetaClass();
const char * clsName = cls -> nonlazyMangledName();
const char *person = "person";
if (strcmp(clsName, person) == 0)
{
if (!isMeta)
{
printf("-----%s-----%s\n", __func__, clsName);
}
}
*/
// 其实测试的时候会发现,没有从这里面直接进入过attachCategories
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);
}
}
load_categories_nolock 分析
且先不说这个方法的作用,发现调用这个方法有两个入口。我们只用研究loadAllCategories
(这个函数就是调用load_categories_nolock方法,就不展示源码了)
-
loadAllCategories
(load_images)流程。但是对于执行这个函数有一个重要条件didInitialAttachCategories 启动时出现的类别的初始附件是否已经完成
。对于启动时候map_images流程是不执行的。 -
_read_images
(map_images)流程。而对于load_images它的执行条件是!didInitialAttachCategories && didCallDyldNotifyRegister
,didCallDyldNotifyRegister就是是否调用dyld的一个函数。对于启动时候是执行的
static void load_categories_nolock(header_info *hi) {
// hi是mach-o的头,表头
// 是否有类属性
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
// 这个count的数量是下面hi->catlist(&count)
size_t count;
// 相当于一个闭包,在结尾才是调用
auto processCatlist = [&](category_t * const *catlist) {
// 例如我们类Person,分类有A,B,C三个,且都有方法实现。则数量就是3。
// 当然只是举个例子,实际情况需要根据load方法等,不一样
for (unsigned i = 0; i < count; i++) {
category_t *cat = catlist[i];
// 获取class,也就是Person
Class cls = remapClass(cat->cls);
// 结构体locstamped_category_t 赋值
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());
}
}
}
}
};
// 就是从mach-o中获取分类,但是为什么分两个?不必关心,没意义,是编译时的东西
// _getObjc2CategoryList
processCatlist(hi->catlist(&count));
// _getObjc2CategoryList2
processCatlist(hi->catlist2(&count));
}
attachCategories 分析
这个函数就是分类加载的处理了,对于分类的处理就在这里面,我们从上面已经得知了这个结果。可以看出,确实是在这里将分类的东西处理给类的,但是改变类的结构指的是rwe。可不是ro,希望你能知道。
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)" : "");
}
/*
* 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.
*/
//其实是限制分类的各种数量 64个。
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);
// 开辟rwe,验证我们的猜想
auto rwe = cls->data()->extAllocIfNeeded();
// 从load_categories_nolock 过来的cats_count是1
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) {
// 方法排个序,注意看什么条件下进来ATTACH_BUFSIZ
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
// 对rwe的方法列表(这个列表其实是数组指针,该数组是方法列表)进行处理
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) {
// 对分类的方法进行排序,然后添加到rwe里面
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();
});
}
}
// 对分类的属性添加到rwe里面
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
// 对分类的协议添加到rwe里面
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
总结:
我知道,上面的源码分析不足以让我们清楚分类加载的具体流程,但是我们通过源码会知道分类加载的时候系统都做了哪些事。而对于类和分类的加载具体流程需要你继续往下看。
类与分类实现load方法,几种情况分析
提示:为什么说实现load方法启动耗时,就是因为在load_images流程中,你类实现load方法已经导致类成为非懒加载类了,而分类如果同时实现load方法,就打破了mach-o已经分配好的方法列表需要走load_images流程重新加载
要探索分类,与类具体加载流程,其实两者是相互影响的。举例子说明:
类实现load,分类也实现load。
会打破原有机制,分类自己去加载
1. 分类只有一个的情况实现load方法。(其实可以这样说,至少一个分类实现了load方法)
可以看到先走map_images流程去加载类,再走load_images流程加载分类。在load_categories_nolock
函数中就直接读取了类的全部分类
2. 分类有两个及以上实现load方法。
可以看到先走map_images流程去加载类,再走load_images流程加载分类。在load_categories_nolock
函数中就直接读取了类的全部分类。与1一致
类实现load,分类没有实现load
并没有走 分类额外去加载流程。而且经测试,我第一次测试rw值,发现分类的方法已经包含在内。我知道以为有分类的可能会伴随rwe的开辟,然后我去看rwe,发现为nil。然后又去看ro的值,发现了ro里面的baseMethods竟然包含了分类方法
。那么是不是可以推断出,在编译阶段,系统就已经完成了分类的处理。是不是人家说分类运行时加载有问题了?
类没有实现,分类实现load
该情况比较复杂一些,类有可能被当做非懒加载类,有可能没有
1. 只有一个分类实现,其他分类没实现
并没有走 分类额外去加载流程。而且经测试,发现了ro里面的baseMethods包含了分类方法。与类实现load,分类没实现
一致
2. 两个及以上分类实现
该情况很复杂,对于类的实现并没有走map_images流程而是走的,load_images流程进行了被动加载。也就是说在之前系统并没有将该类加到非懒加载类表中
。而因为分类的load方法导致了类的加载。对于这种情况的ro并没有包含分类的方法,可以这样说分类在运行时加载
提示1:类没有实现所以需要将分类注册到目标类中,看load_categories_nolock
源码
提示2:去准备load方法,但是发现类没有实现去实现,看源码请看prepare_load_methods。而且还记得上面我所提的attachToClass
方法在某一种情况去执行了附加分类的方法就是这一种情况。
类与分类均没有实现
我们都知道这种情况类的实现是在第一次消息发送的时候,所以我去查看后发现此时ro已经包含了分类的方法。也就是分类在编译时就已经将方法注册到主类中了
总结
对于分类的加载我们可以做如下总结
对于运行时加载的分类情况:1.类与分类都实现load方法;2.分类有两个以上实现load方法
对于编译时加载的分类情况:1.类实现load方法,分类没有;2.类没有实现,分类仅有一个实现;3.类与分类均没实现
所以希望不要动不动就实现load方法,很耗时,除了一些必要的操作。我们可以去执行initialize
方法。
补充
对于类的ro,肯定会有疑惑。这个值是怎么来的,clean Memory,为什么有那么几种情况分类在应用启动前就会被加载到类中。其实我们能够想象到LLVM就可以解答我们的疑惑。在LLVM层也就是编译时你就会发现有个跟objc一样的class_ro_t结构体,只是成员不一样。然后去看结构体的一个方法Read,就是对ro的集中处理。这也就是我在类加载篇章说类的加载其实是在编译时,而类的实现是在运行时。