问题来源
最近发现了一个在项目中常用的异常处理工具NullSafe,分析了它的实现原理,不小心发现了一个小Bug,现将其分享出来,关于这篇文章的Demo已经上传至GitHub,看完如有收获,欢迎Star,如有疑问欢迎issue,大家一起学习。在IOS开发中我们可能会遇到下面的情景:服务器给我们返回得某个字段是null,比如someValue:null
,这个时候我们利用第三方工具转化之后会得到someValue = <null>
,这个时候如果我们判断这个someValue
的类型,会看到其为:NSNull。那么问题来了,如果这个someValue是要给控件赋值,比如:someLabel.text = someVlaue
,这个时候相当于someLabel.text = nil
,显然,是不会有问题的。但是有时候我们可能会给这个貌似是NSString的对象发送消息(因为我们在Model里定义了NSString * someValue),比如:[someValue length]
。这个时候由于null
这个对象没有这个方法,也就是是说:null 这个对象不能处理这个消息
所有就会Crash,让程序闪退。那么我们怎样处理来避免这种Crash呢?我们怎样处理这个消息呢?
OC的消息转发流程
首先我们来看一下NSObject.h
中我们不常用到的几个方法,以及它们的含义:
1 | // 判断是否发现了这个method,如果发了,将其添加给该对象,并且返回YES,如果没有返回NO。 |
这个对象通过创建一个NSInvocation对象作为参数来调用这个forwardInvocation方法,然后改对象会调用这个方法来将消息转发给其它对象。
这个NSInvocation对象其实是有上一个方法methodSignatureForSelector中返回的NSMethodSignature来得到的,所以在重写这个方法之前我们必须重写methodSignatureForSelector方法。
1 | // 这个类的实例是否具有相应这个selector的能力,也就是说这个类有没有这样一个方法 |
那么这几个方法,在系统中是怎样的调用顺序呢?我们来看下图:
从中可以看出在给一个对象发送消息的时候,如果对象没有对应的IML,那么会调用对象所属类的
1 | + (BOOL)resolveInstanceMethod:(SEL)sel |
方法,然后看对于这个SEL对象是否可以执行,如果不可以执行则会调用
1 | - (id)forwardingTargetForSelector:(SEL)aSelector |
来找到一个对象处理这个方法(我们可以返回一个对象,这个对象可以处理这个方法),如果这个方法返回的是nil
,那么会调用这个对象的
1 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector |
这个方法,如果这方法调用之后,仍然没有找到对应的NSMethodSignature
,那么会调用:
1 | - (void)doesNotRecognizeSelector:(SEL)aSelector |
这个方法,并且抛出异常,如果这个时候返回了一个有效的NSMethodSignature
,那么会调用
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation |
到这里消息的处理结束。
那么对于NSNull这个对象,我们如何让它处理一个其自身不能处理的消息呢?这时候,我们肯定会想到,将这个消息传递给其它可以处理的对象。那么问题来了:我们如何能找到这样一个对象呢?这时候我们想到了RunTime,利用RunTime
的objc_getClassList
方法,我们可以获取整个项目中注册得所有类(只要在项目中添加了这个类文件,无论这个类是否被使用),这个时候我们可以过滤掉用不到的父类,以节约循环得次数,因为子类已经继承了父类的方法,所以具有处理这个消息得能力,之后首先利用上文提到的instancesRespondToSelector
来判断这个类是否可以响应这个消息,如果可以响应,那么可以利用上文提到的instanceMethodSignatureForSelector
来得到这个NSMethodSignature
,并且返回。通过上述分析,系统会调用这个forwardInvocation
,这个是时候我们调用NSInvocation的invokeWithTarget
这个方法来将这个消息发送给nil,在OC中向一个nil发送任何消息都不会引起程序Crash,至此一个由于服务器返回数据异常而导致的Crash被解决了。
这显然增加了系统的容错能力,在项目调试阶段,可能由于数据不完善,所以可以利用这个方法来规避Crash,但是在数据基本完善之后,我们可以去掉这种方法以便我们在程序Crash的时候,及时提醒后台人员来完善数据。
NullSafe的实现详解
1 | @implementation NSNull (NullSafe) |
在原文中作者是这样写的: [excluded addObject:NSStringFromClass(superclass)];
这样ClassList中存放的是Class,而excluded中存放的确是String,这样就不能过滤掉不必要的类。所以,我将其改为了: [excluded addObject:superclass];不知道作者是不是考虑了其他问题,也可能是由于其大意。