写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
以上内容的总结专栏
细枝末节整理
前言
上一篇章我们探索了类的加载过程,在最后有探索到分类的加载过程,但是并没有探索完,我们结合了编译后的cpp文件,探索了分类的底层结构以及它的_method_list_t ,接着对于 rwe 的操作我们探索来到了 extAllocIfNeeded 方法的调用。今天,我们接着 关于 rwe 部分,开始今天的内容。
ro - rw - rwe
首先我们从 为什么要有 ro、rw、rwe 来开始今天的内容。
在 类的底层原理结构 系列探索文章中,我们有探索过类的结构和它的底层原理,今天,我们先复习下这部分的知识。
来自苹果WWDC2020关于 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_nolock、attachToClass。
继续向上推到 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 方法的实现与否有关。
那么,接下来会分为几种情况,我们分别断点调试看下调用流程:
// 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(有多个分类会依次调用)
向前查找之后来到了
load_categories_nolock 方法,
好的,我们继续在 attachCategories 中的流程:
attachLists
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
attachLists 分为三部分:
- 0 lists -> 1 list 一维数组
- 1 list -> many lists
- 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 的赋值部分:
因为我们有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();
看代码有点枯燥,我们画个图吧:
到了这一步后,我们继续向下进行,因为我们分类多,这里并没有完成全部的添加, 接着是来到的类方法的添加流程:
我们继续:这次是分类方法的添加,我们来到的是
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 lists ,oldCount = 2。
之后的流程会一次走到此分支,知道分类全部加载完毕。
分类实现了 load 方法 类没有
加载流程:
_read_images --> realizeClassWithoutSwift --> methodizeClass --> attachToClass
调试信息:
虽然 SMPerson 没有实现 load 方法,但是,依然来到了非懒加载的流程中:
而且,我们是在 Like 分类中实现的 load 方法,在这里把 Thind 分类中的方法也能打印出来,说明只要有一个分类实现了load方法,那么整个类的所有分类就都会被加载。
类实现了 load 方法 分类没有
加载流程:
_read_images --> realizeClassWithoutSwift --> methodizeClass --> attachToClass
这里的加载过程和分类实现了load方法,类没有实现是一样的,同样在 methodizeClass 中,所有的分类和类的方法全部已经加载好了。
类和分类都没实现了 load 方法
加载时机别推迟到第一次发送消息的时候进行初始化,且也是将类和分类中的方法全部加载。
总结
类和分类本身就联系很紧密,所以其加载的流程必然也是联系在一起的,通过今天的探索学习,我想大家和我一样对于类的加载以及分类的加载,以及两者之间加载的相互联系能有一个清晰的认识。大家,加油!