哈哈哈哈,看见大家都在写自己的学习总结,本猿在踌躇了三年后终于下定决心也写一些东西。
简单直接,直入主题将是宗旨!
下面开始第一篇文章!
一、知识储备
1、消息触发
通过clang(clang是编译器的前端,具体的还没研究,还没想好怎么看)的简单的命令,编译m文件可以看出,对象的消息发送会被编译成objc_msgSendSuper(发送给父类的)和objc_msgSend(发送给自己),以及其他的发送形式。
比如:
[super viewDidLoad]会被编译成:
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
self.edgesForExtendedLayout = UIRectEdgeNone会被编译成:
((void (*)(id, SEL, UIRectEdge))(void *)objc_msgSend)((id)self, sel_registerName("setEdgesForExtendedLayout:"), (UIRectEdge)UIRectEdgeNone);
通过上面可以看出objc_msgSend和objc_msgSendSuper都是一个函数指针,函数指针的用法,需要的可以简单的了解一下c语言相关的知识。
通过这里可以发现,平时的调用方法里面,其实会隐式传递对象和方法选择器。
我们也可以直接使用这两个方法来调用自己想调用的方法,下面的截图是个实例:
消息发送了,今天先看一下如果没有相应的方法,该怎么处理。
二、三次机会
众所周知,系统提供了三次机会:本类通过类方法动态解析、寻求支援、消息重定向
1、类方法动态解析:+ (BOOL)resolveInstanceMethod:(SEL)sel
一般比较推荐在这里就处理了,因为都知道,方法的调用都有一个缓存方法列表,在这里的调用后的方法是会添加到缓存列表中的,这样的话再次调用的时候就会减少遍历查询的过程,提高一定的效率(虽然一般你也体验不到)。
//int resolveInstanceAddMethod(id obj,SEL sel, NSString * haha, int age, char * num){
// NSLog(@"name:%@,age:%@",haha,@(age));
// return 2;
//}
/**
* 这里方法的参数最好和需要替换的相同(位置和类型),不然容易发生不可知的错误
*/
int resolveInstanceAddMethod(id obj,SEL sel, NSString * haha, int age){
NSLog(@"name:%@,age:%@",haha,@(age));
return 2;
}
/**
* class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
* imp是一个函数指针
* types:返回值类型和形参类型数组,第一个是返回值类型,返回值类型地址:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
* types可以不传
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(testResolveInstanceMthod:age:)) {
//方式一:
Method method = class_getInstanceMethod([self class], @selector(resolveInstanceMethodWithOC:age:));
IMP imp = method_getImplementation(method);
//方式二:
// IMP imp = class_getMethodImplementation([self class], @selector(resolveInstanceMethodWithOC:age:));
//方式三:
// IMP imp = class_getMethodImplementation([ZPZPickMissMessage class], @selector(resolveInstanceMethodWithOC:age:));
//方式四:
IMP imp = (IMP)resolveInstanceAddMethod;
return class_addMethod([self class], sel, imp, nil);
}
return [super resolveInstanceMethod:sel];
}
- (int)resolveInstanceMethodWithOC:(NSString *)name age:(NSInteger)age{
NSLog(@"name:%@,age:%@",name,@(age));
return 10;
}
假想:这么说来,咱也可以在这一步替换对象方法,其实很简单,将IMP改为其他的类class,如:
IMP imp = class_getMethodImplementation([ZPZPickMissMessage class], @selector(resolveInstanceMethodWithOC:age:));
2、寻求支援:- (id)forwardingTargetForSelector:(SEL)aSelector
用其他实例对象来处理,但是切记不能返回自己,如果返回了自己会造成死循环,这一步没有第一步那么有可操作性(动态创建类和实例对象可以)。
/**
* 接盘侠
*/
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(testFindOtherReceiver)) {
//也可以先检测替换的对象是否实现了该方法
ZPZPickMissMessage * msg = [[ZPZPickMissMessage alloc] init];
if ([msg respondsToSelector:aSelector]) {
return msg;
}
}
return [super forwardingTargetForSelector:aSelector];
}
3、消息重定向- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector和- (void)forwardInvocation:(NSInvocation *)anInvocation
必须两个都实现,才会起作用。
/**
* 消息重定向,不能返回自己
* 这里有个问题,为什么还需要methodSignatureForSelector?
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature * signature = [super methodSignatureForSelector:aSelector];
// if (!signature && [pick respondsToSelector:aSelector]) {
// signature = [pick methodSignatureForSelector:aSelector];
// }
//配合下面的做手脚二:
if (!signature && [pick respondsToSelector:@selector(msgInvocationWithMessage:andScore:)]) {
signature = [pick methodSignatureForSelector:@selector(msgInvocationWithMessage:andScore:)];
}
return signature;
}
/**
* 如果没有对anInvocation做处理,则其中的target、selector、arguments等都是调用时候的
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//正常使用
// [anInvocation invokeWithTarget:pick];
// [anInvocation invoke]; //这样有问题
// [anInvocation invokeWithTarget:[anInvocation target]]; //下面的是有问题的,因为[anInvocation target]是ZPZReceiveMessage对象
NSMethodSignature * signature = anInvocation.methodSignature;
NSInteger argumentsCount = [signature numberOfArguments]; //获取参数个数,这里有三个,因为前两个是self和SEL,第三个才是传递的参数
for (NSInteger i = 0; i < argumentsCount; i++) {
const char * type = [signature getArgumentTypeAtIndex:i];
printf("%s\n",type);
}
//因为这里我们知道参数的类型,否则可以通过上面就可以看出参数类型
//做手脚一:修改参数
// NSString * changeWord = @"哈!I changed you!";
// [anInvocation setArgument:&changeWord atIndex:(argumentsCount - 1)];
// [anInvocation invokeWithTarget:pick];
//做手脚二:添加传递参数,参数由原来的一个变成两个
if (argumentsCount > 2) {
anInvocation.selector = @selector(msgInvocationWithMessage:andScore:);
NSString * changeWord = @"哈!I changed you!";
[anInvocation setArgument:&changeWord atIndex:(argumentsCount - 2)];
NSInteger score = 100;
[anInvocation setArgument:&score atIndex:argumentsCount - 1];
[anInvocation invokeWithTarget:pick];
}
//做手脚三:可以修改target,然后就可以直接使用[anInvocation invoke]和[anInvocation invokeWithTarget:[anInvocation target]]调用了
}
需要注意的地方:
1、methodSignatureForSelector不要返回自己,否则会是死循环
2、在显式修改target之前不要直接使用[anInvocation invoke]和[anInvocation invokeWithTarget:[anInvocation target]]调用,因为此时target和selector还是原来的
三、总结
具体的工程在:https://github.com/zhoupengzu/ZPZOCPractice
1、消息传递时,会默认携带两个默认参数
2、对于没有实现的函数有三次补救机会:
1)类方法resolveInstanceMethod
一般推荐在这里做补救,这里可以让自己处理,也可以让别人去处理
2)寻求救援:forwardingTargetForSelector
这里需要注意的是不能返回自己,否则会是死循环
3)重定向
首先这里需要注意的是需要实现两个方法;
其次在找函数签名methodSignatureForSelector返回的时候,不能返回自己,否则会进入死循环;
再然后,不要直接invoke或者用target invoke,因为此时target还是自己
最后,可以在这里做一些特殊处理