Links

17.iOS应用启动(四):分类的加载

前言

我们在12.类的实现与初始化中实现类 realizeClass 的流程最后一步发现是处理分类 methodizeClass。本文就将以此为入口,深入探索 什么是分类

一、 methodizeClass

处理方法列表协议列表属性列表,添加分类
重要部分见注释:
methodizeClass01
methodizeClass02

1.1 attachToClass 将分类添加到类

attachToClass
继续调用 attachCategories,通过初步阅读源码,发现此处为核心步骤,下面进行具体分析

二、 attachCategories

这里对源码中两处重要的注释做翻译:
  • 添加分类的方法列表、属性、协议到类
  • 最早加载的分类在最顶部
  • 只有很少的类在运行时会有超过64个分类
  • 这里使用一个小容量的
  • 分类必须按照合适的顺序添加,这里会从前向后读取,从后向前创建本地缓存,并调用 attachLists。所以最后的顺序是对的。
// 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)
{
。。。
/*
* 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);
// 创建 rwe
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){
// 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);
}

2.1 流程

  • extAllocIfNeeded 创建 rwe
  • 循环添加方法、属性、协议

三、 分类加载的时机

我们直到类添加分类的核心逻辑是 attachCategories。我们全局搜索一下 attachCategories
attachToClass
load_categories_nolock
有上面两处进行了调用。

3.1 测试代码准备

自定义类
@interface RYCat : NSObject
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) NSInteger age;
- (void)yeah;
- (void)yeah2;
- (void)yeah3;
@end
分类
@interface RYCat (CateA)
- (void)funcA;
- (void)funcA2;
- (void)funcA3;
@end
main函数
RYCat *cat = [RYCat alloc];
[cat funcA];

3.1 添加断点及日志

attachToClass
attachCategories
realizeClassWithoutSwift
load_categories_nolock

3.2 场景一: 分类有Load✅ 主类有Load✅ (非懒加载)

Log如下:
_read_images -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
load_categories_nolock -- RYCat -- CateA
attachCategories -- RYCat -- CateA
attachCategories -- RYCat -- CateA
+[RYCat load]
+[RYCat(CateA) load]
-[RYCat(CateA) funcA]
Program ended with exit code: 0
流程:
  • _read_images
    • realizeClassWithoutSwift
      • attachToClass
        • load_categories_nolock
          • attachCategories

3.3 场景二: 分类有Load✅ 主类无Load❌ (非懒加载)

Log如下:
_read_images -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
+[RYCat(CateA) load]
-[RYCat(CateA) funcA]
Program ended with exit code: 0
流程:
  • _read_images
    • realizeClassWithoutSwift
      • attachToClass
注意到:并没有调用 attachCategories

非懒加载

非懒加载
虽然主类未实现+load方法,但是分类实现了,那么主类也是非懒加载的。

3.4 场景三: 分类无Load❌ 主类有Load✅ (非懒加载)

Log如下:
_read_images -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
+[RYCat load]
-[RYCat(CateA) funcA]
Program ended with exit code: 0
流程:
  • _read_images
    • realizeClassWithoutSwift
      • attachToClass
注意到:和 分类有Load✅ 主类无Load❌ 一样,没有调用 attachCategories

3.5 场景四: 分类无Load❌ 主类无Load错❌ (懒加载)

懒加载
Log如下:
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
Program ended with exit code: 0
注意到:和 3.3和3.4 一样,没有调用 attachCategories

思考: 为什么没有调用 attachCategories

编译器优化了,直接把分类的数据合并到了主类中。

四、 多分类 +load 执行顺序

如果有多个分类,执行的顺序是什么样的呢?
如图创建两个分类,分别实现 +load 方法。思考一下执行的顺序是什么样的呢?为什么呢?
12

4.1 顺序

按顺序输出如下:
  • +[RYCat load]
  • +[RYCat(CateA) load]
  • +[RYCat(CateB) load]
我们先看下编译顺序:
13
调整一下编译顺序看有没有影响:
14
调整编译顺序后输出如下:
  • +[RYCat load]
  • +[RYCat(CateB) load]
  • +[RYCat(CateA) load]

4.2 结论

多分类的情况下,+load 的执行顺序是和编译的顺序有关的。

五、 多分类同名方法执行顺序

如果多个分类由同名的方法,又会是怎样的顺序呢?
在两个分类中声明实现同名的 funcA
15
多分类 +load 执行顺序 中我们知道,编译的顺序影响了各个分类的表现,这里我们也进行一下区分

5.1 CateA 比 CateB 先编译

  • +[RYCat load]
  • +[RYCat(CateA) load]
  • +[RYCat(CateB) load]
  • -[RYCat(CateB) funcA]

5.2 CateB 比 CateA 先编译

  • +[RYCat load]
  • +[RYCat(CateA) load]
  • +[RYCat(CateB) load]
  • -[RYCat(CateA) funcA]

5.3 在主类中也声明并实现 funcA (CateB 比 CateA 先编译)

  • +[RYCat load]
  • +[RYCat(CateA) load]
  • +[RYCat(CateB) load]
  • -[RYCat(CateA) funcA]

5.4 总结

可见:多个分类拥有同名方法时,先编译的分类的方法会被后编译的分类覆盖掉。如果主类中也有同名方法也会被覆盖掉。

思考

对启动速度的影响
3.2 主类分类都有实现 Load方法 的时候可见整个流程流程会复杂一下,所以减少这种场景的出现,是有助于提升应用启动速度。