Objective-C的+load方法调用原理分析
Objective-C的+initialize方法调用原理分析
Category的使用场景
我个人粗浅理解,就是将一个类的实现,拆解成小的模块,便于管理和维护。因为实际项目中,有些类的功能可能会非常复杂,导致一个类的代码过多,这对后期修改和维护是比较不利的,所以category方便了程序员,可以根据功能,业务等形式的划分,将类的一大堆方法分组放置以及调用。
有趣的思考
先来看一个最简单的category结构,一下代码定义了一个CLPerson
类 和它的一个category CLPerson+Test
// ******************** CLPerson
#import <Foundation/Foundation.h>
@interface CLPerson : NSObject
-(void)run;
@end
#import "CLPerson.h"
@implementation CLPerson
-(void)run
{
NSLog(@"CLPerson Run");
}
@end
// ******************** CLPerson+Test
#import "CLPerson.h"
@interface CLPerson (Test)
-(void)test;
@end
#import "CLPerson+Test.h"
@implementation CLPerson (Test)
-(void)test{
NSLog(@"Test");
}
@end
// ******************** CLPerson+Eat
#import "CLPerson.h"
@interface CLPerson (Eat)
-(void)eat;
@end
#import "CLPerson+Eat.h"
@implementation CLPerson (Eat)
-(void)eat{
NSLog(@"Eat");
}
@end
请问❓❓❓:以下的两个方法调用,底层到底发生了什么,它们本质是否相同?
CLPerson *person = [[CLPerson alloc]init];
[person run]; //类的实例方法调用
[person test];//分类的实例方法调用
[person eat];//分类的实例方法调用
我们都知道,[实例对象 方法]
这种写法,经过底层转换之后,实际上就是,objc_msgSend(类对象, @selector(实例方法))
,也就我们oc的一个基本概念,消息发送机制。因此,我们可以推定,[person run]
这句代码,在消息发送机制下,首先会根据 person
的isa
指针找到CLPerson
的类对象,然后在类对象的方法列表(method_list_t * methods
)里面找到该方法的实现,然后进行调用。
接下来,你肯定会想
- 那么
[person test]
和[person eat]
呢?它的消息是发送给谁呢? - 是发送给
person
的类对象吗? - 还是说,对于
CLPerson+Test.h
和CLPerson+Eat.h
来说,也有其独立对应的分类对象呢?
带着这些思考和问题,我们接下来一步一步地进行拆解。
Category的实现原理
底层结构——所有一切始于编译
要想知道原理,不要猜,也不要轻易相信别人说的东西,自己验证一下才是最靠谱的。在命令行下,进入CLPerson+Test.m
文件所在路径执行以下命令-->
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc CLPerson+Test.m
得到编译后的c++文件CLPerson+Test.cpp
,将其拖入xcode项目中进行查看,但是不要加入编译列表,否则程序跑不起来。直接查看文件底部,就可以找到category相关的底层信息,请看下图剖析
上图比较粗糙,请谅解,但比文字描述来的更加直观,上面基本上分析清楚了在编译结束之后,category是以何种形式存在的,现在用文字来总结一下:
category经过编译过程之后,系统为其定义了如下的一个结构体
//注意,编译后的cpp文件一般比较长,会有好几万行,
//一般我们关注类结构相关的信息,都在最后,
//所以可以直接把文件拖到底,便可以找到这些信息
struct _category_t {
const char *name; //用来存放类名
struct _class_t *cls;
const struct _method_list_t *instance_methods;//用来存放category里面的实例方法列表
const struct _method_list_t *class_methods;//用来存放category里面的类方法列表
const struct _protocol_list_t *protocols;//用来存放category里面的协议列表
const struct _prop_list_t *properties;//用来存放category里面的属性列表
};
这个struct _category_t
结构体,就是在程序在编译之后,被用来存放category
的相关信息(instance methods
, class methods
,protocol
,property
)的。
反过来描述,编译的时候,系统会给每一个category
生成一个对应的结构体变量,而且他们都是struct _category_t
类型的,然后把category
里面的信息存到这个变量里面。
在我的示例里面,这个变量的名称叫_OBJC_$_CATEGORY_CLPerson_$_Test
,这个名字很清晰的表明,它存储的是Objective-c
下的CLPerson
类的Test
分类的信息。
struct _category_t
中定义了六个成员变量,除去其中的第二个,我个人还没搞明白有什么用,其他的五个作用则非常清晰了
const char *name;
上图中的a部分,其值表示category
所对应的类的名字。
const struct _method_list_t *instance_methods;
上图中的b部分,其值就是实例方法列表,可以看到里面正好放了我们定义的实例方法-test
const struct _method_list_t *class_methods;
上图中的c部分,其值就是类方法列表,可以看到里面放了我们定义的类方法-classTest
const struct _protocol_list_t *protocols;
上图中的d部分,其值就是协议列表,可以看到里面存放了NSCoping
协议
const struct _prop_list_t *properties;
上图中的e部分,其值就是属性,可以看到里面有我们定义的age
属性
源码分析
上面的篇章,我们通过查看编译后的cpp文件,了解了category
在编译阶段完成后的存在形式,以CLPerson+Test
为例,它所对应的struct _category_t
变量中,第一个成员变量name
的值为"CLPerson"
(CLPerson+Eat
对应的name
也是"CLPerson"
,可以自行验证),而且根据我在对象的本质(上)——OC对象的底层实现中所讨论所得出的结果可以知道,一个OC类XXX
在底层都存在一个对应的C++结构体实现struct XXX_IMPL
,但我们在CLPerson+Test.cpp
文件中,并没有发现 struct CLPerson+Test_IMPL
/struct CLPerson+Eat_IMPL
,因此,我猜想CLPerson
的category
中的信息,应该还是存储在CLPerson
所对应的class
对象和meta-class
对象中,category
自己并没有独立的class
对象和meta-class
对象。CLPerson
旗下的所有category
里面的信息,应该是在某个阶段被合并到了类的CLPerson
的class
对象和meta-class
对象中。从编译的结果看,我们并没有发现有合并的操作,仅仅是给每个category
生成了对应的struct _category_t
类型的变量,存放其信息。所以我合理怀疑,合并操作应该是发生在Runtime阶段。
为了证明以上猜想,我们还是要挖掘Runtime的源码。我们先去苹果官网下载一份objc4的最新源码。然后我们直接寻找objc-os.mm
文件,这个文件可以看作是Runtime进行初始化的地方。然后找到_objc_init()
方法,这个方法是Runtime被加载后执行的第一个方法,可以理解成Runtime的入口方法。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
_objc_init()
中前面的一堆方法,跟本文的主题不相关,不入坑,且看最后一个方法_dyld_objc_notify_register(&map_images, load_images, unmap_image)
。这个函数里面的三个参数分别是另外三个函数:
-
map_images
-- Process the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件) -
load_images
-- Process +load in the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件中的+load
方法) -
unmap_image
-- Process the given image which is about to be unmapped by dyld.(处理那些将要被dyld进行去映射操作的镜像文件)
我们查看一下map_images
方法,点进去
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
这里面看不出啥,返回了map_images_nolock(count, paths, mhdrs)
,感觉像是一层转换,继续点进该方法看一下。好家伙,这个方法就比较丰富了,为了节约纸张,这里就不贴完整代码了,有兴趣自己上源码看。经过牛人指点,找到里面一个关键方法_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
。从方法名字可以看出,意思是要读取镜像,也就处理系统动态库以及我们写过的代码中的各种自定义类文件。这个方法也比较长,就截取关键的一段
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
一句// Discover categories
真的是对读者非常友好,这立马使我明白,接下来的代码是处理category相关内容的。这个read_images
方法从上倒下,分好几大块,每大块头部都有类似的注释,说明该部分所做的事情,将作者的思路描述的非常清晰,不愧是苹果的源码。下面通过图解来说明一下category处理部分的大致思路
这里注意我一个细节,上图的第一部分我已经画出来了,一开始的那个catlist
是一个二维数组,里面的成员也是一个一个的数组,也就是代码里面的cat所指向的数组,它的类型是category_t *
,说明cat
数组里面装的就是category_t
,(有点绕,慢慢来:-)一个cat
里面装的就是某个class
所对应的所有category。
那么什么决定了这些category_t在cat数组中的顺序呢?
答案是category文件的编译顺序决定的。先参与编译的,就放在数组的前面,后参与编译的,就放在数组后面。我们可以在xcode-->target-->Build Phases-->Compile Sources列表查看和调整category文件的编译顺序
在上面的category先编译,下面的category后编译。可以鼠标拖拽进行调整。
然后我们继续往下看,进入remethodizeClass
方法看一看
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
然后在这里面找到一个方法attachCategories
,肯名字就知道,附着分类,也就是把分类的内容添加/合并到class里面,貌似快接近真相了,小鸡动🐔
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
//分配一块空间来放所有分类的方法数组,这里是一个二维数组,数组的每个成员,对应着某个分类的方法数组
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
//分配一开空间来放所有分类的属性数组,理解同上
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
//分配一块空间来放所有分类的协议数组,理解同上
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {//这里i--说明,是从
//取出某个分类变量
entry = cats->list[i];
//提取分类中的对象方法/类方法
/* mlists最终会是以下形式
[
[method_t, method_t],
[method_t, method_t]
]
*/
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
//提取分类中的属性
/* proplists最终会是以下形式
[
[property_t, property_t],
[property_t, property_t]
]
*/
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
//提取分类中的协议
/* protolists最终会是以下形式
[
[protocol_t, protocol_t],
[protocol_t, protocol_t]
]
*/
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
//得到类对象里面的数据
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
//将所有分类的对象方法附加到类对象的方法列表中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
//将所有分类的属性附加到类对象的属性列表中
rw->properties.attachLists(proplists, propcount);
free(proplists);
//将所有分类的协议附加到类对象的协议列表中
rw->protocols.attachLists(protolists, protocount);
free(protolists);
//搞定,结束
}
⚠️以上这一部分代码中的注解引用自MJ大神在腾讯平台的相关分享⚠️
这里注意一个地方,这里面用了while (i--) {entry = cats->list[i]; ......}
,entry
可以简单理解成 category_t
,(里面还有一些其他内容,不影响我们的理解),那么list
里面就装了一堆的category_t
,他们都对应着同一个class
,这些category_t
在数组中的顺序,和前面我们讨论的category
文件的编译顺序是相同的,也就是先编译的category
在前,后编译的category
在后。 在while
循环里面进行处理的时候,是从下标 cats->count-1
(也就是i--
)开始的,也就是从数组的尾部向前一个一个的处理。处理过程主要就是把category
的方法列表添加到mlists
里面,mlists[mcount++] = mlist;
,而mcount
是从0开始的,所以结果就是最终,放到mlists
里面的方法列表顺序是倒过来的,最前面的方法列表,对应着最后编译的cetegory(协议和属性的处理过程和这里一样)
上述方法里面的最后一个操作rw->methods.attachLists
我们再进一步分析一下,看一看,最终分类中的方法和class中的方法,最终是以怎么样的顺序合并存放到最后的方法列表里面的,进入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]));
}
}
这个函数的两个参数分别代表
-
addedLists
--将要被添加的category
中的方法列表组成的的数组, -
addedCount
--addedLists
数组的元素数量。
这个方法是今天讨论的问题里面最有趣的地方,将会解释我们在使用category
中所碰到的各种现象。请看下图分解
如此看来,最终类的方法列表里面,如果class
有自己对应的category
,那么category
中的方法列表会被合并放置在class
的方法列表的前部,类本身的方法则会被往列表尾部挪,当我们通过[obj method]
的方式调用方法的时候,系统会到在类的方法列表里面,从前往后遍历查找。
因此,如果category
里面如果重写了class
里面的方法,那么,最终会调用category
的方法实现,就是因为它被放在了列表前面,先被找到,就被调用了,其实class
里面的同名方法还是在的,并没有被覆盖,只不过看起来像是覆盖了。
另外,我们在上面分析attachCategories
方法的时候得知,该方法实际上将category的方法列表按照编译顺序倒过来存到了一个数组里,供后续方法使用。
那么过程走到这里,便可以知道,最最最终,在class的方法列表里面,最后参加编译的
category
的方法会出现在方法列表的最前面,先参加编译的category
的方法会出现在方法列表的后面,列表的最后存着class
自己的方法[对于meta-class也是一样的],好,分析结束。
回答开篇的几个问题
✈️✈️✈️✈️
-
[person test]
和[person eat]
的消息是发送给谁呢?
发送给
CLPerson
的类对象
- 还是说,对于
CLPerson+Test.h
来说,也有其独立对应的分类对象呢?
不存在所谓的 分类的类对象,一个类以及它的所有分类,都只对应一个类对象,它们所有的实例方法(
-方法
),属性(@property
),协议(@protocol
)都被合并到了这一个类对象里面,它们所有的类方法(+方法
),都被合并到了这个类的元类对象里面。上面所说的合并,都是发生在程序运行阶段,运用了Objc的Runtime机制完成。
✈️✈️✈️✈️
*****************砍瓜切菜*****************
(1)category里面的方法存放在哪里?
- 一个类所对应的分类下的对象方法,存放在该类的类对象的方法列表里面。
- 一个类所对应的分类下的类方法,会存放在该类的元类对象的方法列表里面
(2)category里面的方法,是什么时候被放到类的类对象/元类对象的方法列表里面的?(编译阶段 or 运行阶段)
- 结论:是程序运行的时候进行的。通过runtime动态地将分类的方法,合并到类对象、元类对象中。
所有的category结构是一样的,只不过里面存储的具体数据不同,每一个category都有自己对应的一个变量,类型为 struct _category_t
,在编译过程中,会完成对struct _category_t
类型变量的赋值。
(3)程序运行过程中,分类中的方法是如何合并到类的方法列表中的?
面试官要问,就直接画图改他看吧,文字描述感觉弱爆了:)
(4)分类方法会覆盖类里面的方法吗?
不会
(5)如果有多个分类有同名的方法A,那么实际哪一个方法A会被调用?
最后参加编译的category里面的A方法会被调用
(6)如何控制分类的编译顺序?
在Build Phase->Compile Sources里面调整,直接拖拽
(7)category和extension的区别是什么?
- extension的内容是在编译完成后,就存在于类对象里面,extension只不过是将原本.h文件里面的内容挪到了.m文件里面,不让外界看见,实质上它就是class.h的一部分,
- category的内容,是在编译的时候,保存到了struct _category_t 结构体变量中,然后在程序运行阶段(runtime机制)才动态合并到类对象当中的。