预编译部分:
#ifndef NULLSAFE_ENABLED
#define NULLSAFE_ENABLED 1
#endif
功能开关,条件编译判断是否定义了宏NULLSAFE_ENABLED
若未定义则NULLSAFE_ENABLED
定义为 1
如果需要关闭这个类的功能,
将NULLSAFE_ENABLED
置为 0
或者在其他可以预编译的地方将 NULLSAFE_ENABLED
置为 0
#pragma clang diagnostic ignored "-Wgnu-conditional-omitted-operand"
这个是clang编译器前端警告忽略
但是忽略的内容很奇怪
看起来像是GCC编译器时代的东西
后面跟的内容conditional-omitted-operand
条件语句忽略操作数,google之
类似 x ? x : y
等价于 x ? : y
http://docs.w3cub.com/gcc~7/conditionals/
没想明白哪里用得到。
然后进行开关判断 NULLSAFE_ENABLED
预编译/条件编译结束
动态转发部分:
分析问题:
通过Runtime
转发机制实现将[NSNull null]
对象转为nil
一般情况下我们通过对空对象[NSNull null]
发送任何消息时,会导致崩溃
example://模拟原理
NSString * strValue ;
strValue = (id)[NSNull null];
NSLog(@"%ld",strValue.lenght);
例子是向字符串类型的strValue实例发送一个获取字符串长度的消息
实质上是向[NSNull null]发送了字符串类型才会响应的消息(方法)
然后,崩了。
分析实现:
我们明白这个黑科技依然使用的是OC的Runtime优势,
原理和关键点在消息转发机制上。
当我们对Null对象发送消息,NullSafe作为Null的Category,无侵入的对Null对象进行扩展
当向一个对象发送消息后,我们有大概三次机会对发送消息这个动作进行修改。
前两次就不说了,跟NullSafe的原理目的不太相关,NullSafe使用的是动态转发的部分。
首先获取方法签名,也是为了进行到下一步动态转发做准备。
methodSignatureForSelector:
调用父类查看能否获取方法签名,这个情况可能是对Null发送null消息会成功获取。
其他情况,获取不到,那么对常用的Foundation框架类遍历验证,
生成新的方法签名,如果被遍历的类响应消息(select),则重新生成方法签名并进行下一步转发,
如果获取不到就不转发了,直接报错。
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if (!signature)
{
for (Class someClass in @[
[NSMutableArray class],
[NSMutableDictionary class],
[NSMutableString class],
[NSNumber class],
[NSDate class],
[NSData class]
])
{
@try
{
if ([someClass instancesRespondToSelector:selector])
{
signature = [someClass instanceMethodSignatureForSelector:selector];
break;
}
}
@catch (__unused NSException *unused) {}
}
}
return signature;
最后一步对方法进行转发:
执行forwardInvocation:
方法实现动态转发。
invocation.target = nil;
[invocation invoke];
target为接收消息的对象,也就是[Null null],置为nil。
调用invocation的invoke方法,就代表需要执行NSInvocation对象中指定对象的指定方法,并且传递指定的参数。
至此,向[NSNULL null]发送消息的行为已经变成了向nil发送消息,解除了崩溃的隐患。
PS.
1.这个解决方案在我刚刚开始接触的时候感觉是很神奇的,因为直接拖进去就可以用了,其中涉及的思路和想法很值得借鉴。
2.条件编译的开关部分设计,Runtime的转发思想,都或多或少的对Runtime的应用有一定启发。