本文我们研究分类的加载流程。
分类的本质
在研究对象、类的本质的时候,我们都用clang命令将main.m转换成main.cpp文件查看其本质,分类也不例外,我们使用相同的方法分析。
首先我们定义一个JSPerson的分类:
@interface JSPerson (TestLoad)
- (void)sayCategory;
@end
@implementation JSPerson (TestLoad)
- (void)sayCategory{
NSLog(@"JSPersonCategpry say : %s",__func__);
}
+ (void)load{
NSLog(@"JSPersonCategpry load");
}
@end
然后在main.m文件中使用:
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *p = [JSPerson alloc];
[p sayCategory];
NSLog(@"Hello, World!");
}
}
我们执行:
clang -rewrite-objc main.m -o main.cpp
我们打开main.cpp文件,找到分类相关的代码。
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_JSPerson_$_TestLoad,
};
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;
};
我们从源码里也能搜索到它的定义:
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
可以看到分类的本质是结构体:category_t。
我们继续回到_read_images函数探索。
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 *>(&ro_or_rw_ext);
} else {
return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
}
}
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;
}
在auto rwe = cls->data()->extAllocIfNeeded();是进行rwe的创建,那么为什么要在这里进行rwe的初始化??因为我们现在要做一件事:往本类中添加属性、方法、协议等,即对原来的 clean memory要进行处理了
- 进入
extAllocIfNeeded方法的源码实现,判断rwe是否存在,如果存在则直接获取,如果不存在则开辟 - 进入
extAlloc源码实现,即对rwe 0-1的过程,在此过程中,就将本类的data数据加载进去了 - 其中关键代码是
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);即存入mlists的末尾,mlists的数据来源前面的for循环 - 在调试运行时,发现
category_t中的name编译时是JSPerson(参考clang编译时的那么),运行时是TestLoad即分类的名字 - 代码
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;,经过调试发现此时的mcount等于1,即可以理解为倒序插入,64的原因是允许容纳64个(最多64个分类)
小结
本类 中 需要添加属性、方法等,所以需要初始化rwe,rwe的初始化主要涉及:分类、addMethod、addProperty、addprotocol , 即对原始类进行修改或者处理时(运行时),才会进行rwe的初始化
attachCategories
通过attachCategories反推分类的加载,我们libobjc源码全局搜索attachCategories的调用,发现有两个地方调用:attachToClass和load_categories_nolock。
attachToClass
调用attachToClass的方法只有一个methodizeClass
static void methodizeClass(Class cls, Class previously)
{
///省略代码
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);
///省略代码
}
这里有一个previously执行条件,我们依次网上查找调用链,发现previously==nil,previously参数只是方便动态化调试,所以实际调用的只有下面这一处代码:
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
load_categories_nolock
static void load_categories_nolock(header_info *hi) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
size_t count;
auto processCatlist = [&](category_t * const *catlist) {
for (unsigned i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
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());
}
}
}
}
};
processCatlist(hi->catlist(&count));
processCatlist(hi->catlist2(&count));
}
全局搜素load_categories_nolock,发现调用load_categories_nolock的地方有两处
-
loadAllCategories -
_read_images但是经过调试发现,是不会走
_read_images方法中的if流程的,而是走的loadAllCategories方法中的。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;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
这个函数一共三部分我们分别看:
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
把addedLists[0]赋值给list,list是一维数组。
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();
}
这个情况是list不为空,新建一个扩容的数组,将之前的数据放在lists[addedCount]位置,新元素放到之前list的前面。
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();
}
这段其实和上次类似,依然将新数组插入到前面,新数组中的新元素在数组首部。
分类加载的四种情况
根据类和分类是否实现+load()方法分为四种情况。
我们先定义JSPerson及它的分类:
@interface JSPerson : NSObject
- (void)sayHello;
@end
#import "JSPerson.h"
@implementation JSPerson
- (void)sayHello{
NSLog(@"JSPerson say : Hello!!!");
}
+ (void)load{}
@end
@interface JSPerson (Test)
- (void)saySomething;
@end
#import "JSPerson+Test.h"
@implementation JSPerson (Test)
- (void)saySomething{
NSLog(@"%s",__func__);
}
+ (void)load{}
@end
非懒加载分类和非懒加载类
我们在JSPerson类和分类中都实现load,我们在realizeClassWithoutSwift添加断点
我们用lldb打印当前ro的方法
readClass JSPerson....
_read_images JSPerson....
(lldb) p ro
(const class_ro_t *) $0 = 0x0000000100004280
(lldb) p *$0
(const class_ro_t) $1 = {
flags = 0
instanceStart = 8
instanceSize = 8
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "JSPerson" {
Value = 0x0000000100003bbc "JSPerson"
}
}
baseMethodList = 0x00000001000042c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000000000000
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x00000001000042c8
(lldb) p *$2
(method_list_t) $3 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
name = "sayHello"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000036b0 (KCObjcBuild`-[JSPerson sayHello])
}
发现此时只有类的方法,并没有分类的方法,说明分类目前还没有加载。
我们在attachCategories添加断点,继续执行代码:
继续用lldb调试程序:
(lldb) p mlist
(method_list_t *) $5 = 0x0000000100004420
(lldb) p *$5
(method_list_t) $6 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $6.get(0).big()
(method_t::big) $7 = {
name = "saySomething"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb)
说明现在分类被加载了,被加载到了rwe中:
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
注意:ATTACH_BUFSIZ=64也就是说分类的方法个数不能大于64。
非懒加载分类和懒加载类
我们删除JSPerson类的load方法,重新运行程序。
继续在我们第一个断点位置使用lldb调试:
_read_images JSPerson....
(lldb) p ro
(const class_ro_t *) $0 = 0x00000001000042a8
(lldb) p *$0
(const class_ro_t) $1 = {
flags = 0
instanceStart = 8
instanceSize = 8
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "JSPerson" {
Value = 0x0000000100003bbc "JSPerson"
}
}
baseMethodList = 0x0000000100004178
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000000000000
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x0000000100004178
(lldb) p *$2
(method_list_t) $3 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 2)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
name = "saySomething"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb) p $3.get(1).big()
(method_t::big) $5 = {
name = "sayHello"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000036b0 (KCObjcBuild`-[JSPerson sayHello])
}
(lldb)
发现分类已经加载了,类也加载了,说明非懒加载类会使懒加载的类在启动时提前加载(如果没有分类是第一次调用时加载),说明加载的时机是编译期。
懒加载分类和非懒加载类
我们删除JSPerson分类的load方法,重新运行程序。
使用lldb调试:
(lldb) p ro
(const class_ro_t *) $0 = 0x00000001000042a8
(lldb) p *$0
(const class_ro_t) $1 = {
flags = 0
instanceStart = 8
instanceSize = 8
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "JSPerson" {
Value = 0x0000000100003bbc "JSPerson"
}
}
baseMethodList = 0x0000000100004158
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000000000000
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x0000000100004158
(lldb) p *$2
(method_list_t) $3 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 2)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
name = "saySomething"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb) p $3.get(1).big()
(method_t::big) $5 = {
name = "sayHello"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000036c0 (KCObjcBuild`-[JSPerson sayHello])
}
(lldb)
发现分类和类都已经加载了,说明加载的时机也是编译期。
懒加载分类和懒加载类
我们把类和分类的load方法都删除,重新运行程序
依然走到断点,注意观察左边的调用栈,发现是从lookUpImpOrForward,说明是在第一次调用方法的时候加载的。
One More Condition
前面四种情况基本能包括了分类,但是还有种情况就是:有多个分类,部分分类实现了load方法主类也实现了load。我们就探索一下这个情况,新建一个JSPerson的分类
@interface JSPerson (Test2)
- (void)saySomething2;
@end
@implementation JSPerson (Test2)
- (void)saySomething2{
NSLog(@"%s",__func__);
}
@end
根据前面我们其实应清楚,就是实现load的分类肯定会在运行时加载,我们关注的点就在于没有实现load方法的分类是什么时候加载的呢也就是attachCategories是否会加载未实现load方法的分类,运行程序
使用lldb调试:
lldb) p cat
(category_t *) $1 = 0x0000000100004448
(lldb) p *$1
(category_t) $2 = {
name = 0x0000000100003b8f "Test2"
cls = 0x00000001000049a0
instanceMethods = {
ptr = 0x0000000100004428
}
classMethods = {
ptr = 0x0000000000000000
}
protocols = 0x0000000000000000
instanceProperties = 0x0000000000000000
_classProperties = 0x0000000000000000
}
operator() -JSPerson....
attachCategories -JSPerson....
(lldb) p cat
(category_t *) $3 = 0x00000001000044c8
(lldb) p *$3
(category_t) $4 = {
name = 0x0000000100003b95 "Test"
cls = 0x00000001000049a0
instanceMethods = {
ptr = 0x0000000100004488
}
classMethods = {
ptr = 0x00000001000044a8
}
protocols = 0x0000000000000000
instanceProperties = 0x0000000000000000
_classProperties = 0x0000000000000000
}
调试我们发现,两个分类都加载了,也就是只要有一个分类实现了load,其他分类都会在启动时加载。
总结
本篇主要是探索了分类的加载,主要分为5种情况
- 非懒加载类和非懒加载分类:此时分类是在
运行时,也就是程序启动的时候加载的。 - 懒加载类和非懒加载分类:此时分类是在
编译时加载 - 非懒加载类和懒加载分类:此时分类也是在
编译时加载 - 懒加载类和懒加载分类:此时分类在第一次调用时加载。
- 非懒加载类,多个分类,部分是非懒加载分类:此时所有分类都是在
程序启动时加载。