在上一篇文章中我们基本了解了类是如何加载到内存的,但是我们仅仅探究了本类的方法加载,而对于分类的情况仍然没有探究,本文就对分类的方法时如何加载到内存的进行探究,另外也探究懒加载和非懒加载分类的加载做分开探究。
分类中有实现
load
方法即非懒加载分类
分类的本质
我们使用clang
命令编译分类,看看他经过编译后变成什么类型。
#import "Animal+Cate1.h"
@implementation Animal (Cate1)
- (void)instanceMethod1 {
NSLog(@"%s", __func__);
}
- (void)instanceMethod2 {
NSLog(@"%s", __func__);
}
- (void)instanceMethod3 {
NSLog(@"%s", __func__);
}
- (void)instanceMethod4 {
NSLog(@"%s", __func__);
}
@end
经过clang
命令
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.6.sdk Animal+Cate1.m -o Animal+Cate1.cpp
找到有关分类的代码:
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
#pragma warning(disable:4273)
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[4];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Cate1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
4,
{{(struct objc_selector *)"instanceMethod1", "v16@0:8", (void *)_I_Animal_Cate1_instanceMethod1},
{(struct objc_selector *)"instanceMethod2", "v16@0:8", (void *)_I_Animal_Cate1_instanceMethod2},
{(struct objc_selector *)"instanceMethod3", "v16@0:8", (void *)_I_Animal_Cate1_instanceMethod3},
{(struct objc_selector *)"instanceMethod4", "v16@0:8", (void *)_I_Animal_Cate1_instanceMethod4}}
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Animal;
static struct _category_t _OBJC_$_CATEGORY_Animal_$_Cate1 __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Animal",
0, // &OBJC_CLASS_$_Animal,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Cate1,
0,
0,
0,
};
可以看到,分类会被编译成一个_category_t
类型的结构体,这个结构体包含了分类实例方法、分类类方法、协议、属性等,所以分类本质是一个结构体。
接下来我们就通过探究分类的方法是如何加载到内存中来探究分类的加载。为了可以通过断点调试探究类的加载,我们仍然需要一份可以运行的runtime源码(objc4-7.8.1源码)进行探究。
非懒加载类和非懒加载分类
分类何时加载内存?
我们创建一个Animal
类,并且为该类添加两个分类,添加一些方法。在主类和分类都实现load
方法。
主类:
@implementation Animal
+ (void)load {
}
- (void)fun1 {
NSLog(@"%s", __func__);
}
- (void)fun2 {
NSLog(@"%s", __func__);
}
- (void)fun3 {
NSLog(@"%s", __func__);
}
- (void)fun4 {
NSLog(@"%s", __func__);
}
@end
分类1:
@implementation Animal (Cate1)
+ (void)load {
}
- (void)fun1 {
NSLog(@"%s", __func__);
}
- (void)fun2 {
NSLog(@"%s", __func__);
}
- (void)cate_func1 {
NSLog(@"%s", __func__);
}
- (void)cate_func2 {
NSLog(@"%s", __func__);
}
@end
分类2:
@implementation Animal (Cate2)
+ (void)load {
}
- (void)fun1 {
NSLog(@"%s", __func__);
}
- (void)fun2 {
NSLog(@"%s", __func__);
}
- (void)cate_func1 {
NSLog(@"%s", __func__);
}
- (void)cate_func2 {
NSLog(@"%s", __func__);
}
@end
在上一次探究过程中,我们探究到methodizeClass
的时候,苹果给他的注释是Attach categories
,而在methodizeClass
方法中,可以看到有调用一个和分类的有关的方法objc::unattachedCategories.attachToClass
。
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);
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);
}
}
这里有一个关键的方法attachCategories
,可以从名字猜测这是一个添加分类的关键方法,
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.
*/
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);
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);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
为了验证我的猜测,我在这段代码中添加了一个判断并打上断点,运行代码,看看有没有执行。
程序成功执行到断点,为了进一步验证,往下执行到method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
处,继续执行,查看mlist
内容。
(lldb) p *mlist
(method_list_t) $10 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 4
first = {
name = "fun1"
types = 0x0000000100000ea0 "v16@0:8"
imp = 0x0000000100000b90 (KCObjc`-[Animal(Cate1) fun1] at Animal+Cate1.m:16)
}
}
}
mlist
是个method_list_t
的数组,此时mlist
有4个元素,lldb
查看内容。
(lldb) p $10.get(0)
(method_t) $11 = {
name = "fun1"
types = 0x0000000100000ea0 "v16@0:8"
imp = 0x0000000100000b90 (KCObjc`-[Animal(Cate1) fun1] at Animal+Cate1.m:16)
}
(lldb) p $10.get(1)
(method_t) $12 = {
name = "fun2"
types = 0x0000000100000ea0 "v16@0:8"
imp = 0x0000000100000bc0 (KCObjc`-[Animal(Cate1) fun2] at Animal+Cate1.m:20)
}
(lldb) p $10.get(2)
(method_t) $13 = {
name = "cate_func1"
types = 0x0000000100000ea0 "v16@0:8"
imp = 0x0000000100000bf0 (KCObjc`-[Animal(Cate1) cate_func1] at Animal+Cate1.m:24)
}
(lldb) p $10.get(3)
(method_t) $14 = {
name = "cate_func2"
types = 0x0000000100000ea0 "v16@0:8"
imp = 0x0000000100000c20 (KCObjc`-[Animal(Cate1) cate_func2] at Animal+Cate1.m:28)
}
显然,这是Cate1
的方法列表,可以确定,此处便是分类添加的地方。
那么这个方法时何时调用的呢,我们可以通过bt
查看调用堆栈。
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
* frame #0: 0x00000001002f61da libobjc.A.dylib`attachCategories(cls=Animal, cats_list=0x00007ffeefbf6b40, cats_count=1, flags=8) at objc-runtime-new.mm:1355:13
frame #1: 0x00000001002f86f3 libobjc.A.dylib`load_categories_nolock(this=0x00007ffeefbf6bb0, catlist=0x0000000100001038)::$_4::operator()(category_t* const*) const at objc-runtime-new.mm:3095:25
frame #2: 0x00000001002e0982 libobjc.A.dylib`load_categories_nolock(hi=0x000000010070c2f0) at objc-runtime-new.mm:3114:5
frame #3: 0x00000001002df89a libobjc.A.dylib`loadAllCategories() at objc-runtime-new.mm:3122:9
frame #4: 0x00000001002c01b6 libobjc.A.dylib`load_images(path="/private/tmp/objc.dst/usr/lib/libobjc.A.dylib", mh=0x00000001002bb000) at objc-runtime-new.mm:3140:9
frame #5: 0x000000010000b26c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 418
frame #6: 0x000000010001efe9 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 475
frame #7: 0x000000010001d0b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #8: 0x000000010001d0de dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 230
frame #9: 0x000000010001d154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #10: 0x000000010000b662 dyld`dyld::initializeMainExecutable() + 129
frame #11: 0x0000000100010bba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
frame #12: 0x000000010000a227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
frame #13: 0x000000010000a025 dyld`_dyld_start + 37
可以看到,attachCategories
是dyld
的notifySingle
通知runtime
执行load_images
时,load_images
便会通过loadAllCategories
->load_categories_nolock
->attachCategories
,将分类加载到内存中。
类是如何加载到内存的?
了解了分类是何时开始加载到内存后,那分类的方法mlist
是如何加载到内存的呢?
继续往下执行断点,来到mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
,ATTACH_BUFSIZ
等于64,分类方法被添加到一个数组mlists
中,mlists
此时是一个二位数组,mlist
被放在最后一个。
断点来到prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
,在上次探究已经了解到,这个方法内部会调用fixupMethodList
对分类方法进行排序(
mlists + ATTACH_BUFSIZ - mcount
内存平移至当前的方法列表mlist
)
断点往下,来到rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
,这个方法显示时间分类方法添加到rwe
的methods
中,而在上次研究类的加载的时候,在没有分类的情况下,rwe
是一个空值,此时我们添加了两个分类,rwe
显然不再为空。
可以用lldb
输出查看
(lldb) p rwe
(class_rw_ext_t *) $23 = 0x0000000100639a10
那个rwe
是如何创建的呢,往上查找可以看到auto rwe = cls->data()->extAllocIfNeeded();
,我们进入到extAllocIfNeeded
看看它是如何创建的。
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 *>();
} else {
return extAlloc(v.get<const class_ro_t *>());
}
}
显然,如果当前data
有rwe
数据,那么就直接返回,如果没有,则通过extAlloc
方法,传入ro
,创建rwe
。
查看extAlloc
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;
}
这份代码完美验证了苹果在WWDC2020上关于内存优化的介绍,这个方法中,objc::zalloc
开辟了一个rwe
脏内存,并将传入的ro
拿到本类的方法、属性、协议列表,通过attachLists
方法添加到rwe
中,然后返回rwe
。
回到分类方法添加的部分,rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
,我们通过rwe
的创建知道知道此时rwe
是本类的方法的,那么分类的方法是如何添加的呢,本类和分类的方法是如何保存到rwe
的方法列表的呢。我们通过methods.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;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
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;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
这个方法有三个分支,我们一个个分析。
1. 当前list没有元素
这份代码是
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
显然这个时rwe
创建时本类方法添加会走的流程,addedLists
此时是一个数组指针地址,addedLists[0]
便是将本类的方法列表直接赋值给list
。
list
是list_array_tt
的成员
class list_array_tt {
...
private:
union {
List* list;
uintptr_t arrayAndFlag;
};
...
}
2. 当前list有一个元素
else {
// 1 list -> many lists
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;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
符合这个情况的便是Animal
的第一个分类Cate1
的方法列表添加进来,这里重新开辟了一个大小为原有方法列表数量(此时只有本类,数据为1)+新添加方法列表数量(此时只有一个分类添加进来,数据为1)的空间,并将原有的方法列表放在最后的位置,将新添加的方法列表整段复制到新的空间中。
3. 当前list有多个元素
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
符合这种情况的是Animal
第二个分类Cate2
方法添加,和第二种情况类似,这里也是重新开辟了一个大小为原有方法列表数量(此时为2)+ 新添加方法列表数量(此时为1)的空间,并将旧的方法列表数组平移到新创建数组的末尾,将新添加的方法添加添加到新数组的最前面。
这份代码也完美解释了为什么分类和本类有同名方法的情况下,会优先执行分类的同名方法。
贴上消息慢速查找的一个关键方法,是不是茅塞顿开。
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
小结
对于非懒加载类和非懒加载分类,主类的方法加载流程为:
map_images
->_read_images
->_read_images
->realizeClassWithoutSwift
->methodizeClass
->prepareMethodLists
分类方法的加载流程为:
load_images
->loadAllCategories
->load_categories_nolock
->attachCategories
->prepareMethodLists
->attachLists
懒加载类和非懒加载类
我们知道懒加载的类不会在map_images
的时候开始加载,只有当给类第一次发送消息时,类才会开始加载到内存中,那如果主类没有实现load
方法,而分类中实现了load
方法呢?
我们同样用Aniamal
类做研究,并在类加载的关键方法realizeClassWithoutSwift
和分类的加载关键方法load_categories_nolock
加入针对性研究代码打上断点。
运行程序,程序先来到了load_categories_nolock
加载分类,往下走断点,来到addForClass
查看addForClass
的实现
void addForClass(locstamped_category_t lc, Class cls)
{
runtimeLock.assertLocked();
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: found category %c%s(%s)",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), lc.cat->name);
}
auto result = get().try_emplace(cls, lc);
if (!result.second) {
result.first->second.append(lc);
}
}
try_emplace
的实现:
// Inserts key,value pair into the map if the key isn't already in the map.
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.
// Otherwise, insert the new element.
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
这个方法中,通过get()
获取一张map,以cls
为key,将传入的分类lc
封装为BucketT
作为vlue保存起来,此时类还没有完成加载。
查看lc
的内容
(lldb) p lc
(locstamped_category_t) $0 = {
cat = 0x00000001000020a0
hi = 0x0000000100738000
}
(lldb) p $0->cat
(category_t *) $1 = 0x00000001000020a0
Fix-it applied, fixed expression was:
$0.cat
(lldb) p *$1
(category_t) $2 = {
name = 0x0000000100000e5e "Cate1"
cls = 0x00000001000022c8
instanceMethods = 0x0000000100002018
classMethods = 0x0000000100002080
protocols = 0x0000000000000000
instanceProperties = 0x0000000000000000
_classProperties = 0x0000000000000000
}
Animal
类的load_categories_nolock
执行次数和非懒加载分类的个数有关。Animal
类有两个非懒加载分类,所以load_categories_nolock
会进来两次。
执行下个断点,程序来到了realizeClassWithoutSwift
,这是加载类的方法,当时不同的是,之前realizeClassWithoutSwift
是map_images
一步步调用的,但map_images
已经执行完成,此时realizeClassWithoutSwift
是由load
调起的。
关于realizeClassWithoutSwift
加载类的过程我们在上一篇文章已经谈论,我们直接来到methodizeClass
看看分类是如何加载到类的。
因为rwe
从始至终没有创建,所以methodizeClass
流程大体和没有分类的类加载的情况一致,直到objc::unattachedCategories.attachToClass
进入attachToClass
的实现,
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);
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);
}
}
同样打上断点
在这个方法里,通过get()
拿出了我们在load_categories_nolock
阶段存放分类的map
,并通过类(previously
此时为Animal
)拿到了对应的BucketT
类型的it
,并通过it->second
拿到存放在it
第二个位置的category_list
,也就是分类列表,通过attachCategories
方法将category_list
添加到类的rwe
中。之后的流程,便和非懒加载类和非懒加载分类一致。
我们可以通过lldb
验证。
(lldb) p list.array()
(const locstamped_category_t *) $15 = 0x0000000100738c70
(lldb) p *$15
(const locstamped_category_t) $16 = {
cat = 0x00000001000020a0
hi = 0x0000000100738000
}
(lldb) p $16->cat
(category_t *const) $17 = 0x00000001000020a0
Fix-it applied, fixed expression was:
$16.cat
(lldb) p *$17
(category_t) $18 = {
name = 0x0000000100000e5e "Cate1"
cls = 0x00000001000022c8
instanceMethods = 0x0000000100002018
classMethods = 0x0000000100002080
protocols = 0x0000000000000000
instanceProperties = 0x0000000000000000
_classProperties = 0x0000000000000000
}
(lldb) p $15[1]
(const locstamped_category_t) $20 = {
cat = 0x0000000100002168
hi = 0x0000000100738000
}
(lldb) p $20->cat
(category_t *const) $21 = 0x0000000100002168
Fix-it applied, fixed expression was:
$20.cat
(lldb) p *$21
(category_t) $22 = {
name = 0x0000000100000e64 "Cate2"
cls = 0x00000001000022c8
instanceMethods = 0x00000001000020e0
classMethods = 0x0000000100002148
protocols = 0x0000000000000000
instanceProperties = 0x0000000000000000
_classProperties = 0x0000000000000000
}
推断正确。
小结
对于懒加载类和非懒加载的加载,类不会因为分类实现了load
方法而成为非懒加载类,在map_images
时候类并没有加载,而是会因为分类中实现load
方法提前在load_images
时候完成加载并添加分类(正常是第一次发送消息的时候加载)。
整个加载流程为
load_images
->(加载分类)loadAllCategories
->prepare_load_methods
->(主类加载)realizeClassWithoutSwift
->methodizeClass
->(开始添加分类)objc::unattachedCategories.attachToClass
->attachCategories
->attachLists
懒加载类和懒加载分类
这是我们最常见的情况,毕竟我们不是每个类都在本类或分类中实现load
方法,同样我们在realizeClassWithoutSwift
和load_categories_nolock
打上断点探究,当然我们得调用一次Animal
的对象方法,否则Animal
不会加载。
运行程序后发现断点并不会执行到load_categories_nolock
,而是在第一次发送消息时执行realizeClassWithoutSwift
,这与我们之前探究的类的加载过程一致,那么分类方法加载加载进来了吗?我们来到methodizeClass
断点,看看类的方法列表中有哪些方法。
执行断点到list
处,用lldb
看看list
的信息
(lldb) p *list
(method_list_t) $0 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 12
first = {
name = "fun1"
types = 0x0000000100000e9b "v16@0:8"
imp = 0x0000000100000c80 (KCObjc`-[Animal(Cate2) fun1])
}
}
}
list
有12个方法,这正好和Animal
本类和分类的方法数量总数一致,且list
的第一个方法就是分类的方法,显然此时分类1和分类2的方法都已经在类的data
中了,所以我们可以下结论:
懒加载类和懒加载分类在编译时期就已经完成分类方法的添加。
此时的方法是没有经过排序的,需要经过prepareMethodLists
排序。
对比排序前后的方法列表
可以看到经过排序后,同名方法会依次排序,并且后编译的分类方法会排在前面,而我们在消息慢速查找的时候,二分查找到方法名一致的方法后,为往前再查找方法,所以分类的方法仍然是比本类的优先执行,后编译的分类方法比其他同名方法优先执行。
小结
懒加载类和懒加载分类,分类方法在编译时就已经添加到data
中,不需要再通过运行时添加到类中,类会在类第一次消息发送时开始加载。
非懒加载类和懒加载分类
同样在realizeClassWithoutSwift
和load_categories_nolock
打上断点探究。
和懒加载类懒加载分类的情况一样,load_categories_nolock
并没有被调用,断点来到realizeClassWithoutSwift
,不同的是,这里的realizeClassWithoutSwift
是map_images
->map_images_nolock
->_read_images
->realizeClassWithoutSwift
流程调用的,是在main
函数之前就开始调用。
同样来到methodizeClass
,看看分类方法是否已经添加
(lldb) p *list
(method_list_t) $0 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 12
first = {
name = "fun1"
types = 0x0000000100000ea0 "v16@0:8"
imp = 0x0000000100000c90 (KCObjc`-[Animal(Cate2) fun1])
}
}
}
同样是12个方法,同样在编译时,分类方法就已经添加到data
中。
总结
- 非懒加载类会在
_read_images
阶段就完成加载 - 懒加载类一般会在第一次方法调用时开始加载,除非他有非懒加载分类
- 非懒加载分类会在
load_images
阶段开始加载,如果主类是懒加载分类会迫使它提前加载 - 懒加载分类会在编译时就添加到类的
data
中,在类加载时再进行排序