iOS消息机制(发送、动态解析、转发)
iOS开发经常碰到以下两个问题:
一: unrecognized selector sent to instance,这是因为调用了不存在的方法导致的(比如字典当做数组来用,使用了下标。 数组当做字典来用,调用了键值对取值,调用外部传递的sel等),在所有的崩溃中占有相当大的比例。
二: 还有一种占比比较高的闪退就是NULL,本来和后端说好的数据类型不会错,但是呢,总是不如意啊。尤其是php当没有数据的时候字典就变成数组了,这样的话客户端解析一不小心就JJ了。
想要彻底解决以上两个问题需要了OC的方法调用机制。
一. 先看整个消息机制流程图
1. objc_msgSend代码执行流程图
objc_msgSend执行流程 – 源码解析2. 消息发送(方法查找)
消息发送3. 动态消息解析
动态方法解析4.消息转发流程
消息转发流程消息解析、转发流程图:
消息转发流程从上边一系列流程图中可以看出调用一个方法大致分为两个大的步骤:
1.消息发送
2.动态消息处理
使用动态消息处理就可以做到当调用了一个不存在的方法的时候程序不会崩溃
下面先来解决第一种情况
1,首先在ViewController类中创建对象以及调用person的sendMessage方法
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person*p = [[Person alloc] init];
[p performSelector:NSSelectorFromString(@"sendMessage") withObject:nil];
}
Person
@interface Person : NSObject
@end
@implementation Person
//在.m中实现这两个方法:
-(void)noObjMethod{
NSLog(@"未实现这个实例方法");
}
+(void)noClassMethod{
NSLog(@"未实现这个类方法");
}
// 返回值是什么没有切实的意义,通过源码可以看到只是作为log打印了出来。但是如果实现了添加方法最好返回YES,否则返回NO。
// 实例方法是存在于当前对象对应的类的方法列表中
+(BOOL)resolveInstanceMethod:(SEL)sel{
SEL aSel = NSSelectorFromString(@"noObjMethod");
Method aMethod = class_getInstanceMethod(self, aSel);
class_addMethod([self class], sel, method_getImplementation(aMethod), method_getTypeEncoding(method));
return YES;
}
// 如果是动态添加类方法class_addMethod第一个参数应该使用object_getClass(self)
+(BOOL)resolveClassMethod:(SEL)sel{
SEL aSel = NSSelectorFromString(@"noClassMethod");
Method aMethod = class_getInstanceMethod(self, aSel);
class_addMethod(object_getClass(self), sel, method_getImplementation(aMethod), method_getTypeEncoding(method));
return YES;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector ==@selector(sendMessage:)) {
return [Student new];
}else{
return [super forwardingTargetForSelector:aSelector];
}
}
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
if(aSelector ==@selector(sendMessage:)) {
return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
}
return nil;
}
- (void)forwardInvocation:(NSInvocation*)anInvocation {
SEL sel = [anInvocation selector];
Student *stu = [Student new];
if([stu respondsToSelector:sel]) {
[anInvocation invokeWithTarget:stu];
}else{
[super forwardInvocation:anInvocation];
}
}
@end
Student类:
@interface Student :NSObject
@end
@implementation Student
- (void)sendMessage:(NSString *)str {
NSLog(@"Student--%@", str);
}
+ (void)sendMessage:(NSString *)str {
NSLog(@"Student++%@", str);
}
@end
上述示例调用了sendMessage
方法,这个方法不存在,所以通过一系列的方法查找还是没有找到指定方法的时候会进入动态消息解析
阶段。
二. 动态方法解析
在Person.m中实现一下动态解析相关方法就可以做到不闪退了.
但是这样做不通用,我们可以搞一个NSObject的分类,将这些动态解析方法写在分类里边,但是这样做太危险了,最好不要这样做。
resolveInstanceMethod
v@:中, v表示返回值void, @表示对象self, :表示SEL
Person.m实现以下方法
//在.m中实现这两个方法:
-(void)noObjMethod{
NSLog(@"未实现这个实例方法");
}
+(void)noClassMethod{
NSLog(@"未实现这个类方法");
}
// 返回值是什么没有切实的意义,通过源码可以看到只是作为log打印了出来。但是如果实现了添加方法最好返回YES,否则返回NO。
// 实例方法是存在于当前对象对应的类的方法列表中
+(BOOL)resolveInstanceMethod:(SEL)sel{
SEL aSel = NSSelectorFromString(@"noObjMethod");
Method aMethod = class_getInstanceMethod(self, aSel);
class_addMethod([self class], sel, method_getImplementation(aMethod), method_getTypeEncoding(method));
return YES;
}
// 如果是动态添加类方法class_addMethod第一个参数应该使用object_getClass(self)
+(BOOL)resolveClassMethod:(SEL)sel{
SEL aSel = NSSelectorFromString(@"noClassMethod");
Method aMethod = class_getInstanceMethod(self, aSel);
class_addMethod(object_getClass(self), sel, method_getImplementation(aMethod), method_getTypeEncoding(method));
return YES;
}
三. 消息转发
1. 如果resolveInstanceMethod
没有实现class_addMethod
,会执行- (id)forwardingTargetForSelector:(SEL)aSelector,此方法会将消息转发给Student类实现
- (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector ==@selector(sendMessage:)) {
return [Student new];
}else{
return [super forwardingTargetForSelector:aSelector];
}
}
如果上述代码做修改,不是返回实例Student对象
,而是返回Student类对象
也是可以的,如下:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sendMessage:)) {
return [Student class];
} else {
return [super forwardingTargetForSelector:aSelector];
}
}
Student.m需要实现类方法
+ (void)sendMessage:(NSString *)str {
NSLog(@"Student++%@", str);
}
完成以上操作就可以成功将消息转发到Student的类方法
了
2. 如果- (id)forwardingTargetForSelector:(SEL)aSelector返回值为nil,则会调用以下方法
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
if(aSelector ==@selector(sendMessage:)) {
return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
}
return nil;
}
- (void)forwardInvocation:(NSInvocation*)anInvocation {
SEL sel = [anInvocation selector];
Student *stu = [Student new];
if([stu respondsToSelector:sel]) {
[anInvocation invokeWithTarget:stu];
}else{
[super forwardInvocation:anInvocation];
}
}
a) methodSignatureForSelector
实现以后,forwardInvocation
里边可以实现任意内容。
b) - (void)forwardInvocation:(NSInvocation*)anInvocation
可以将消息转发给多个对象实现。
c) - (void)forwardInvocation:(NSInvocation*)anInvocation
里边可以无任何实现或者只是打印log都不会有任何问题。
d) forwardingTargetForSelector
、methodSignatureForSelector
、forwardInvocation
这些方法都有对应的类方法,只是敲代码的时候没有提示。
如果以上三种重载都没执行消息,此时会调用- (void)doesNotRecognizeSelector:(SEL)aSelector方法,此时程序会崩溃
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"doesNotRecognizeSelector");
}
从流程图可以看出,越是往后开销越大,所以在早期做出预防处理是最好的选择 所以直接在父类中重写resolveInstanceMethod方法,就可以做到程序不会崩溃了
接下来解决第二种情况,就是NULL的情况
这时候可以搞一个分类,如下:
#import <Foundation/Foundation.h>
@interface NSNull (Exception)
@end
#import "NSNull+Exception.h"
#import <objc/runtime.h>
@implementation NSNull (Exception)
#define pLog
#define JsonObjects @[@"",@0,@{},@[]]
//在.m中实现这两个方法:
-(void)noObjMethod{
NSLog(@"未实现这个实例方法");
}
+(void)noClassMethod{
NSLog(@"未实现这个类方法");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
for(id jsonObj in JsonObjects) {
if([jsonObj respondsToSelector:sel]) {
#ifdef pLog
NSLog(@"NULL出现啦!这个对象应该是是_%@",[jsonObj class]);
#endif
}
}
SEL aSel = NSSelectorFromString(@"noObjMethod");
Method aMethod = class_getInstanceMethod(self, aSel);
class_addMethod([self class], sel, method_getImplementation(aMethod), "v@:");
return YES;
}
如果此时在 ViewController.m中调用一下错误的字典 则不会引起崩溃
NSDictionary* dict = [[NSNull alloc] init];
[dict objectForKey:@"123"];
控制台会打印出以下信息:
**2019-03-02 09:44:30.905674+0800 Test[28763:7642018] NULL****出现啦!这个对象应该是是****___NSDictionary0**
**2019-03-02 09:44:30.905790+0800 Test[28763:7642018] ****未实现这个实例方法**
现在就可以随意折腾了
image好了,至此咱们的APP就可以减少大部分闪退问题了
四. 附录
在源码里边找不到__forwarding__
和__forwarding__clean.c
,这里整理了一份别人写的伪代码供参考。
- forwarding.c
// 伪代码
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 僵尸对象
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
SEL *registeredSel = sel_getUid(selName);
// selector 是否已经在 Runtime 注册过
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(), 9);
}
- __forwarding__clean.c
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
}
}
if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
// The point of no return.
kill(getpid(), 9);
}