iOS Objective-C 分类的加载
前言
在我的另一篇文章iOS 应用的加载objc
篇中分析了在objc
中类、协议、sel等的加载,但是在文章中并没有详细的分析分类是如何加载的,那么分类是如何加载的呢?在本文我们会进行详细的分析。
1. 通过clang查看分类在底层的实现
我们打开一个新的objc4-779.1
工程,新建一个LGTeacher
的类,并创建一个LGTeacher+Test
的分类。
LGTeacher+Test.m
源码:
#import "LGTeacher+Test.h"
@implementation LGTeacher (Test)
+ (void)load{
NSLog(@"分类 load");
}
- (void)setCate_p1:(NSString *)cate_p1{
NSLog(@"%s",__func__);
}
- (NSString *)cate_p1{
return @"cate_p1";
}
- (void)cate_instanceMethod2{
NSLog(@"%s",__func__);
}
+ (void)cate_classMethod2{
NSLog(@"%s",__func__);
}
@end
通过clang
编译我们的LGTeacher+Test.m
。
clang -rewrite-objc LGTeacher+Test.m -o category.cpp
我们查看category.cpp
这个文件,可以找到分类被编译后的结果:
static struct _category_t _OBJC_$_CATEGORY_LGTeacher_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"LGTeacher",
0, // &OBJC_CLASS_$_LGTeacher,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_Test,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LGTeacher_$_Test,
};
我们可以看到LGTeacher_+_Test
这个分类在底层实现实际是一个category_t
的结构体。我们还可以看出我们的分类是存储在Mach-O
中__Data
段的__objc_const
节中的。
根据结构体内部的信息我们可以看出三行重要的代码:
_OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_Test
_OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_Test
_OBJC_$_PROP_LIST_LGTeacher_$_Test
以上三行分别对应着我的对象方法,类方法和*属性。
1.1 分类的定义
上面我们提到category_t
,那么它到底是什么样的结构中呢,我们来到libobjc
源码中进行查看其真正的定义。(PS:C++去掉下划线)
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *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
的定义和刚才我们clang
的结果我们可以得到如下结论:
-
name
:是分类所关联类的名字 -
cls
:clang
后这个值是0,但是我们根据其注释可知这是我们的类对象,只是在编译期这个值不存在。 -
instanceMethods
:分类存储的对象方法 -
classMethods
:分类存储的类方法 -
protocols
:分类实现的协议 -
instanceProperties
:分类定义的实例属性,不过我们一般在分类中添加属性都是通过关联对象来实现的 -
_classProperties
:分类所定义的类属性,根据注释我们知道这个内容不一定一直在磁盘上保存。可能是私有属性,不一定一直都存在。 - 下面还有是否是元类的一些处理
2. 分类的加载
我们在分析类的加载的时候会涉及到懒加载类和非懒加载类的区别。那么分类的加载又是怎么样的呢?是否也有懒加载和非懒加载的区别呢?而且分类是依附于类的,类存在才会存在分类,那么类的懒加载和非懒加载会不会对分类有什么影响呢?
其实一开始就是写了个分类,并没有考虑是否是懒加载,然后在_read_images
函数中的Discover categories.
段中添加下图红色框框中的代码,但是没到断点处,所以才有了上面那段的想法。
2.1 没有实现load的分类
2.1.1 类为懒加载的时候
此处就是都没实现+load
方法,这在我们开发中也是比较常见的,只写业务逻辑,不需要通过+load
方法进行提前加载。
那么此时分类在何处进行加载呢?根据我们上面刚开始时得到的结果,该分类的加载肯定不会走_read_images
函数,在类的加载的时候我们关于懒加载类的加载是在第一次发送消息的时候,那么分类呢?首先想到的是我们在类加载的时候methodizeClass
函数中的Attach categories
。
我们发现上图中的第二个断点是不会来到的,这个我们在应用的加载objc
篇这篇文章中详细的分析了关于Attach categories
中 if
分支 不会调用的原因。
此时我们查看lldb
发现分类中的方法已经存在于ro
中,如下图所示:
虽然我们看到分类中的方法已经在ro
中了,但是我们还是来看看if
分支下面那行代码吧:
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);
}
}
attachToClass
函数中主要还是通过attachCategories
函数将获取到的分类列表进行附加操作,但是区分了元类和类。
那么attachCategories
函数都做了什么工作呢?
attachCategories 源码:
// 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)
{
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 rw = cls->data();
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);
rw->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) {
rw->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) {
rw->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
rw->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}
rw->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rw->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
根据注释我们就可以知道该函数是将方法列表、属性和协议从分类附加到类上,如果所有分类都附加完毕则后来居上,后面附加的排在第一个。
我们分别在这两个函数开始处添加断点,发现来到methodizeClass
的断点后并不会走attachCategories
函数,到了attachToClass
函数中也会因为if (it != map.end())
的不成立直接跳过。
结论:
所以说对于懒加载的分类和懒加载的类同时使用时,分类也是在编译期间直接存储在了Mach-O
中,然后在类加载的时候直接与类一起加载了。
2.1.2 类为非懒加载的时候
如果类是非懒加载,那么类就是走_read_images
里面的流程,首先还是来点methodizeClass
函数,沿用上面的断点和添加的信息。
此时运行我们的程序结果跟上面一模一样,这里就不上图了,因为都是一样的,不管是通过发送消息来到methodizeClass
函数还是通过_read_images
来到methodizeClass
最终都会来到methodizeClass
。最终也会调用attachToClass
函数,但是依然不会调用attachCategories
函数。
结论:
所以说,类是非懒加载的还是懒加载的都不会影响分类在编译期写入到Mach-O
中,通过类加载的时候直接读入ro
。
2.2 实现了load的分类
虽然在我们的开发中一般很少在分类中去写+load
方法。但是我们还是来看看在分类中实现+load
方法的时候,分类是怎么加载的。
2.2.1 类为懒加载的时候
还是上面的断点,我们运行一下我们的程序,这时候断点首先来到了_read_images
函数中的Discover categories.
处
此时我们通过控制台查看我们的ro
和rw
内部并没有什么数据。
这个时候对于懒加载类,并不是由发送消息触发的类的加载,所以说懒加载类的加载也会由它的非懒加载类触发。
我们添加如下图所示的断点,看看会不会在这里附加我们的分类,然后并不会,因为在这里类还没有被实现,所以类不存在分类还没有被附加,元类也一样。所以说对于非懒加载分类和懒加载类组合的时候,分类不是在这里加载的。
我们过掉这些断点,再次来到了methodizeClass
函数中。
此时我们的ro
中依旧没有数据,那么我们首先看看调用堆栈。
调用堆栈中我们可以看到prepare_load_methods
函数,那么这个函数做了些什么事情呢?
prepare_load_methods 源码:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
由此函数我们可以知道在methodizeClass
的断点是由prepare_load_methods
中的realizeClassWithoutSwift(cls, nil);
这行代码调用过去的,我们也知道realizeClassWithoutSwift
函数是加载我们的OC类的。 在prepare_load_methods
函数中:
- 首先定义变量和加锁就没什么可说的了
- 然后通过
_getObjc2NonlazyClassList
函数获取非懒加载的类 - 遍历这些非懒加载的类,进行重新映射
- 接下来就是通过
_getObjc2NonlazyCategoryList
获取非懒加载的分类列表 - 遍历这些非懒加载的分类,因为分类依附于类所以在这里要提前实现类,如果类实现了也会在
realizeClassWithoutSwift
函数中返回。 - 调用
add_category_to_loadable_list
add_category_to_loadable_list 源码:
/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
if (!method) return;
if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
译 : 分类们的父类(依附的类)存在,并且该分类已附加到其类中。 在其依附的类建立连接并调用其自己的
+ load
方法之后,将该类别安排为+ load
。(PS: 没太明白啥意思)
小结:
- 对于存在的非懒加载分类的情况会通过
prepare_load_methods
函数调用realizeClassWithoutSwift
进行类的加载 - 类的加载以及分类的处理会通过
realizeClassWithoutSwift
函数进一步调用methodizeClass
进一步处理 -
methodizeClass
的处理我们在上面以及看过
接下来所以我们可以大胆测猜想,这次肯定会调用到attachCategories
函数,我们可以直接来到attachCategories
函数中进行查看。
当attachCategories
执行完毕后我们的ro
中虽然没有数据,但是在rw
中已经有了我们分类的方法,因为ro
是readonly
,在类加载的时候从Mach-O
中读取出来的,既然此时分类的数据不在编译期写在了Mach-O
中,也就不会存在于ro
中。
总结:
所以说非懒加载分类和懒加载类组合时,通过调用load_images
->prepare_load_methods
->realizeClassWithoutSwift
->methodizeClass
->objc::unattachedCategories.attachToClass
->attachCategories
将协议、方法、属性附加到类上。
2.2.2 类为非懒加载的时候
此时就是在分类和类中都实现+load
方法,对于非懒加载类我们已经很熟悉了,在_read_images
里面进行加载,我们沿用上面的断点:
首先还是会来到_read_images
中的Discover categories
处,但是处理还是与懒加载类的时候一样,所以说对于非懒加载的分类,才会通过_getObjc2CategoryList
或者_getObjc2CategoryList2
取出分类列表,但是由于类还未实现都不会进行进一步的处理。在objc4-779.1
中Discover categories
的代码在Realize non-lazy classes
前,所以Discover categories
这段代码貌似没什么用处,在objc4-756.2
中这两段代码顺序相反,就会有作用。
我们跳过这些断点来到methodizeClass
中:
在断点处我们分类中的信息同样不在ro
和rw
中。 同样我们来到attachCategories
函数中:
在attachCategories
函数执行完毕后,我们发现我们的分类方法已经在rw
的methods
中。至此分类已经加载到了内存。
所以说非懒加载分类和非懒加载类组合时,我们的分类在类加载的时候就顺便加载了,流程为map_images
->_read_images
->realizeClassWithoutSwift
->methodizeClass
->objc::unattachedCategories.attachToClass
->attachCategories
3. 总结
关于分类的加载非常的绕,因为我们要考虑类的加载方式,现在总结如下:
- 懒加载分类 + 懒加载类
直接通过编译打包生成
Mach-O
时将分类信息写入,与类一起加载
- 懒加载分类 + 非懒加载类
直接通过编译打包生成
Mach-O
时将分类信息写入,与类一起加载
- 非懒加载分类 + 懒加载类
通过
load_images
->prepare_load_methods
进一步加载。
- 非懒加载分类 + 非懒加载类
通过
map_images
->_read_images
在类加载的时候加载
注: 分类的加载并不像我们想象的那样在_read_images
的Discover categories
进行加载,而是根据不同的组合有更多种的加载方式。至此关于Objective-C 分类的加载就分析完毕了,流程比较繁琐,文章排版一般,因为分析的时候太繁琐了,就一步一步的罗列了,有的地方也没上图说明,如有问题欢迎指正。