本文我们研究分类的加载流程。
分类的本质 在研究对象
、类
的本质的时候,我们都用clang
命令将main.m
转换成main.cpp
文件查看其本质,分类
也不例外,我们使用相同的方法分析。
首先我们定义一个JSPerson
的分类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @interface JSPerson (TestLoad) - (void)sayCategory; @end @implementation JSPerson (TestLoad) - (void)sayCategory{ NSLog(@"JSPersonCategpry say : %s",__func__); } + (void)load{ NSLog(@"JSPersonCategpry load"); } @end
然后在main.m
文件中使用:
1 2 3 4 5 6 7 int main(int argc, const char * argv[]) { @autoreleasepool { JSPerson *p = [JSPerson alloc]; [p sayCategory]; NSLog(@"Hello, World!"); } }
我们执行:
1 clang -rewrite-objc main.m -o main.cpp
我们打开main.cpp
文件,找到分类相关的代码。
1 2 3 4 5 6 7 8 9 10 11 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 ; };
我们从源码里也能搜索到它的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 ; 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的赋值 1 auto rwe = cls->data()->extAllocIfNeeded();
extAllocIfNeeded
方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 ); } 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void methodizeClass (Class cls, Class previously) { if (previously) { if (isMeta) { objc::unattachedCategories.attachToClass(cls, previously, ATTACH_METACLASS); } else { objc::unattachedCategories.attachToClass(cls, previously, ATTACH_CLASS_AND_METACLASS); } } objc::unattachedCategories.attachToClass(cls, cls, isMeta ? ATTACH_METACLASS : ATTACH_CLASS); }
这里有一个previously
执行条件,我们依次网上查找调用链,发现previously==nil
,previously
参数只是方便动态化调试,所以实际调用的只有下面这一处代码:
1 2 objc::unattachedCategories.attachToClass(cls, cls, isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
load_categories_nolock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 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) { if (PrintConnecting) { _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with " "missing weak-linked target class" , cat->name, cat); } continue ; } if (cls->isStubClass()) { if (cat->instanceMethods || cat->protocols || cat->instanceProperties || cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) { objc::unattachedCategories.addForClass(lc, cls); } } else { 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void attachLists (List* const * addedLists, uint32_t addedCount) { if (addedCount == 0 ) return ; if (hasArray()) { 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 ) { list = addedLists[0 ]; validate(); } else { 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(); }
这个函数一共三部分我们分别看:
1 2 3 4 5 else if (!list && addedCount == 1 ) { list = addedLists[0 ]; validate(); }
把addedLists[0]
赋值给list
,list
是一维数组。
1 2 3 4 5 6 7 8 9 10 11 12 else { 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
的前面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (hasArray()) { 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
及它的分类
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @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
的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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.b aseMethods() (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
调试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 (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
中:
1 rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
注意:ATTACH_BUFSIZ=64
也就是说分类
的方法个数不能大于64。
非懒加载分类和懒加载类 我们删除JSPerson
类的load
方法,重新运行程序。
继续在我们第一个断点位置使用lldb
调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 _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.b aseMethods() (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
调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 (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.b aseMethods() (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
的分类
1 2 3 4 5 6 7 8 9 10 11 12 @interface JSPerson (Test2) - (void)saySomething2; @end @implementation JSPerson (Test2) - (void)saySomething2{ NSLog(@"%s",__func__); } @end
根据前面我们其实应清楚,就是实现load
的分类肯定会在运行时加载,我们关注的点就在于没有实现load
方法的分类是什么时候加载的呢也就是attachCategories
是否会加载未实现load
方法的分类,运行程序
使用lldb
调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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种情况
非懒加载类和非懒加载分类:此时分类是在运行时
,也就是程序启动
的时候加载的。
懒加载类和非懒加载分类:此时分类是在编译时
加载
非懒加载类和懒加载分类:此时分类也是在编译时
加载
懒加载类和懒加载分类:此时分类在第一次调用时加载。
非懒加载类,多个分类,部分是非懒加载分类:此时所有分类都是在程序启动
时加载。