我在工程里准备了这么一个类LCHero,有一个对象方法throwSkill
, 继承至LCPerson。 LCPerson里面有一个对象方法attack
, 一个类方法defence
,LCPerson 继承至NSObject. 我在NSObject的一个分类里准备了一个测试方法test
代码以下:c++
#import <Foundation/Foundation.h>
#import "LCHero.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCHero *hero = [LCHero new];
[hero throwSkill];
}
return 0;
}
/******************************************/
@interface LCPerson : NSObject
- (void)attack;
+ (void)defence;
- (void)revival;
@end
@implementation LCPerson
- (void)attack {
NSLog(@"%s --> 开始进攻!",__func__);
}
+ (void)defence {
NSLog(@"%s ||-- 开始防护! ",__func__);
}
@end
/******************************************/
@interface LCHero : LCPerson
- (void)throwSkill;
@end
@implementation LCHero
- (void)throwSkill {
NSLog(@"%s --> 释放终极技能!",__func__);
}
@end
/******************************************/
@interface NSObject (test)
- (void)test;
@end
@implementation NSObject (test)
- (void)test {
NSLog(@"%s, 测试一下!",__func__);
}
@end
复制代码
使用的命令以下:程序员
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk main.m
复制代码
咱们看到的对应的cpp文件中对main文件中咱们写的内容的c++编译内容objective-c
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LCHero *hero = ((LCHero *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCHero"), sel_registerName("new"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)hero, sel_registerName("throwSkill"));
}
return 0;
}
复制代码
若是把方法的类型以及类型转换去掉,就是以下的样式。咱们发现底层调用的是一个objc_msgSend
方法,咱们调用的方法被转成了SEL。数组
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LCHero *hero = (LCHero *)objc_msgSend(objc_getClass("LCHero"), sel_registerName("new"));
(void *)objc_msgSend(hero, sel_registerName("throwSkill"));
}
return 0;
}
复制代码
既然咱们知道了底层吊起的是objc_msgSend,那么咱们在方法调用以前打一个调试断点,当断点来了以后咱们按住control
键 + step into 一步一步点击看看它会怎么走。 缓存
来到这一步以后,咱们想既然方法这些都在类里面,而类和对象是经过isa联系起来的,是否是要找isa呢?而后经过isa找到对应的类,答案是确定的。bash
找到class以后开始查找类中的方法缓存。 多线程
慢速查找
经过上面那个景点咱们看到了_class_lookupMethodAndLoadCache3
方法,源码中以下:发现它直接调起一个下层方法lookUpImpOrForward
,_class_lookupMethodAndLoadCache3只是起到中间链接做用app
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
复制代码
这个方法里面的内容有点多同时也很重要
,我就把判断、断言、赋值的代码都删掉了,保留一些关键的方法和注释。你们先过个眼隐。咱们再一一分析下。oop
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
if (!cls->isRealized()) { //先判断类有没有加载到内存,若是没有,先加载类
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {//判断是否实现了initialize,若是有实现,先调用initialize
_class_initialize (_class_getNonMetaClass(cls, inst));
}
retry:
// Try this class's cache. imp = cache_getImp(cls, sel); if (imp) goto done; // Try this class's method lists.
//从类的方法表里查询,若是有就返回imp 顺便存一份到类的cache里
// Try superclass caches and method lists.
// Found the method in a superclass. Cache it in this class.
// No implementation found. Try method resolver once.
// No implementation found, and method resolver didn't help. // Use forwarding. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlock(); return imp; } 复制代码
cls->isRealized()
判断类是否加载了,realizeClass(cls)
这是一个递归操做,全部继承链上的类都会被加载。父类-->根类-->根元类,直到cls为空才退出递归。methodizeClass(cls)
分类方法加载咱们来看看他们作了什么,主线流程我在代码块中介绍了先看注释,我英文不太好可是大概意思明白了,把方法、协议、属性安排好,而后把外面的分类也添加进来 --> 感受看到了美景 ><
/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list. * Attaches any outstanding categories. * Locking: runtimeLock must be held by the caller **********************************************************************/ 咱们把这个方法的主要内容拆开说明一下 1. 把方法、属性、协议从类的ro 拷贝到 rw中来,为啥有个1?-->是由于把数组的首地址传进去组成了一个二维数组 method_list_t *list = ro->baseMethods(); if (list) { prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls)); rw->methods.attachLists(&list, 1); } property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } protocol_list_t *protolist = ro->baseProtocols; if (protolist) { rw->protocols.attachLists(&protolist, 1); } 2. 判断是不是根元类,若是是根元类须要把方法加载一下,确保在分类替换以前就已经加载好了 --> ? 下面的注释也有, 就是说若是根类调用了一个它本身没有的方法,它会往根元类中找。 个人根元类要是有相关方法我要把他添加到个人类的方法表里面,它才能找的获得,并且要早于分类方法添加以前。 为何是在分类替换以前呢?我在这里只能大胆猜测一下,方法表设计的多是一个相似栈的表, 若是有分类在后面添加以后那么我就找imp的时候就先找到分类的imp就返回了,就出现了替换了类的方法的这么个现象 // Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}
3. 添加分类方法
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/); 复制代码
咱们前面已经找过缓存了,为何还要找缓存呢?缘由有2:组件化
Method meth = getMethodNoSuper_nolock(cls, sel);//找类的方法表
log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到就缓存一份
imp = meth->imp;
goto done
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
imp = cache_getImp(curClass, sel); //递归找父类的方法表
log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到就缓存一份
imp = meth->imp;
goto done
复制代码
若是咱们找了类的方法表,同时递归找了父类都没有找到,因为咱们传递的resolver默认是YES
同时triedResolver也没有进行重新赋值仍是NO,咱们会走下到下一站方法决议_class_resolveMethod
,具体源码在下面,
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
复制代码
在经历了类->父类->元类->根元类->...->NSObject ,分类等一系列的查找以后没有找到,那而后怎么办?咱们的旅程还要继续啊!苹果爸爸仍是很心疼咱们的,给你个机会处理一下吧,我不直接让它崩溃。因而咱们看到了下一站的风景_class_resolveMethod(cls, sel, inst)
咱们来看下这个方法里有啥东西,具体代码在下面代码块。步骤在下面分析
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
复制代码
为何要判断?--> 对象方法在类里面,类方法在元类里面
注释中写try [cls resolveInstanceMethod:sel] ?--> 咱们的类里面没有怎么try? 是否是系统帮咱们实现了这个方法,不知道,继续往下看
先判断是否没有实现这个方法,若是没有就直接返回。我本身写的类里面没有这个方法,若是咱们本身没有实现的话是否是系统帮咱们实现?
在objc的源码中找到了这个方法,默认返回的是NO.
如今咱们思考一下,若是我能让这个方法执行下去势必要找到一个imp返回回去。若是咱们重写这个方法而后添加一个imp到类里面是否是就解决这个问题了呢?咱们去文档中查一下这个方法,果真验证了咱们的想法。
无论咱们有没有处理lookUpImpOrNil
都会调起,而后再回到lookUpImpOrForward
,由于已经retry过了这个结果已经保存了,若是找到imp直接到done流程,若是仍是没找到就会来到imp = (IMP)_objc_msgForward_impcache
.
咱们搜一下,结果这个家伙在汇编里面调用的是__objc_msgForward
。
咱们再看看__objc_msgForward
,它里面调用的有两个很像的方法,再搜一下就发如今objc的源码中有实现,这里的打印内容咱们好熟悉哦,咱们来试下在咱们开始准备的main里面调用LCHero没有实现的对象方法- (void)revival
看看错误输出 --> 没错就是没找到方法的报错输出
类方法和对象方法处理有点不太同样调用的是resolveClassMethod
,添加imp是往元类里面添加,只是和对象方法相似的处理流程只不过调用的方法不同,只是调用完类方法决议以后竟然还走了对象方法的决议
。咱们就猜测为何还要走这步。
lookUpImpOrNil
再次查找一下若是仍是没有就会走一次对象方法决议_class_resolveInstanceMethod
。_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
复制代码
可是就这样结束了么?咱们再来看看奔溃时候的堆栈是否是还有咱们没了解过的方法,明显还有。
log_and_fill_cache
方法,除了fill_cache 还有log,不妨一看究竟。发现调用了
logMessageSend
,再往里面看看发现了一个相似log开关控制的参数
这里咱们不妨大胆地玩一下,由于根据咱们在log_and_fill_cache的流程中发现它有个打印log的方法,还会写到一个文件里,这个文件里根据它的注释说会给咱们一些方法有关的线索。咱们把这个开关在咱们准备的工程中拓展一下使用范围在奔溃先后都调用了啥方法?
再运行一下,咱们在/tmp/msgSends 找有没有相似的文档记录log
打开一看,OMG, 啥!! 都打印出来的确有两个咱们没跟出来的流程一个是forwardingTargetForSelector
,另外一个是methodSignatureForSelector
。突然发现咱们还想逛的还要不少,咱们都想看看后面两站都是什么风景!!
表面意思是传递一个对象,什么意思?原来我方法调用时传入的对象不要了么? 咱们来看看源码是否是NSObject也实现了只是跟上一个站点同样没有作处理额?
- (void)revival
@implementation LCEnemy
- (void)revival {
NSLog(@"%s --> 哈哈哈 本魔王又活了!",__func__);
}
@end
复制代码
forwardingTargetForSelector
,返回一个LCEnemy对象。-(id)forwardingTargetForSelector:(SEL)aSelector {
return [NSClassFromString(@"LCEnemy") alloc];
}
复制代码
到这个流程以后也就意味着:
那这个时候我是否是要在网上发给求助帖-->看看哪一个好心人能处理。 可是总得知道你这个方法是什么格式吧,否则别人怎么知道能不能处理?-->方法调用必须签名和SEL 相匹配才会被调用。 咱们来看看官方文档怎么解释的
- (void)forwardInvocation:(NSInvocation *)anInvocation;
那么咱们来试一下:
// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(revival)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息转发 -- 开始祈祷谁来处理一下
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
NSLog(@"%@",anInvocation);
}
复制代码
若是咱们调用没有实现的方法,既没有动态决议、也没有转发给其余对象处理同时也没有写求助帖🙏好心人来处理此时苹果爸爸也帮不了你,只好结束你的方法旅程,给你一张红色的回程票doesNotRecognizeSelector
--> 熟悉的崩溃。
objc_msgSend汇编流程 -->从类缓存中快速查找imp
_class_lookupMethodAndLoadCache3 --> 开始进入c、c++慢速查找
lookUpImpOrForward --> 继承链上的类的方法表里遍历查找,找到了缓存一份而后返回imp
_class_resolveMethod --> 开始查看是否有动态决议,若是有给到imp,从新lookUpImpOrForward
forwardingTargetForSelector --> 本身没有处理,是否有交给别人代理处理。
methodSignatureForSelector + forwardInvocation --> 若是也没有代理者,请按照规范写求助信
doesNotRecognizeSelector --> 若是什么都不作,你太懒了 苹果爸爸表示上帝也救不了你。
复制代码
通过这段方法探索的旅程我领悟了一些东西
void instrumentObjcMessageSends(BOOL flag)
,在合适的时候拓展做用域就能跟踪一些方法调用的线索。感谢你们的阅读,若是你以为写得还能够请动动大家的小手给我点个赞。我会更有动力给你们分享一下好东西。下一次计划更新关于类的加载的文章。有兴趣交流学习的能够加我QQ:578200388