处理服务端API返回JSON的总结
当服务端API返回给客户端的数据中,有空字段时一般是使用null填充,这时若iOS客户端不对字段先作类型判断的话,就很容易出现崩溃,因为Cocoa在JSON对象转换时对null转为了NSNull对象,而不是nil,且nil也不能存放于集合对象中,不先作类型判断,直接使用该字段转出的对象就可能出现找不到访问的selector而崩溃了。(Android的空判断本来就是用null作依据,所以这个情况对它没有影响。)
所以开发API时,应该与服务端协商好规范约定,而客户端也应该做好防范措施。
规范约定
当时与iOS客户端同事约定的规则:
- 客户端对服务端返回的JSON数据,必须要作类型及安全值判断(iOS统一动态地将NSNull调用无法识别的方法时返回nil):
- 与服务端约定字段类型,若为不确定类型的需要先作类型判断;
- 字符串的字段,使用string.length > 0(为nil时调用length是安全的);
- 数字的字段,使用number.intValue、number.floatValue、number.doubleValue、number.boolValue等;
- 集合的字段,使用set.count > 0(为nil时调用count是安全的)。
与服务端同事约定的规则:
- 与服务端设计API的约定
- 布尔值的字段统一使用true、false(不再使用”true”、”y”、“yes”、1等等);
- 空字段统一采用设置默认值,不使用null或”null”,数字的使用0,字符串的使用””,数组使用[],对象使用{}。
安全的NSNull
为了实现“统一动态地将NSNull调用无法识别的方法时返回nil”,我参考了一个第三方库NullSafe去对现有的客户端工程打了一下补丁,其实也算不上一个库,只有一个m文件。使用前先分析了一下它的实现原理。
NullSafe
首先NullSafe
是NSNull
的分类Category,其次它只有三个利用运行时实现的方法,一个是初始化缓存数据的,另外两个是实现消息转发的。消息转发的流程如下图
缓存策略
NullSafe做了一个缓存机制,主要缓存两项内容
- 所有的NSObject子类;
- selector对应签名的集合;
因为查找NSObject子类和seletor的响应者时会历遍项目里的所有注册类和NSObject子类,这个过程消耗很大,缓存是十分必要的。
然后NSObject的子类是从所有注册类中筛选出来。通过objc_getClassList获取所有注册了的类,class_getSuperclass获取所有注册类的父类。这里大家可能会有疑问,为什么不直接使用superclass获取父类,原因是objc_getClassList获取到的类不一定都是NSObject的子类,所以就不一定都有superclass这个方法。关于更多的Objective-C Runtime方法说明可以查阅官方文档
签名Selector
1.重写Selector的签名方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
这个方法的作用是告诉系统该selector的参数有多少个、是什么类型(也可以通过调用instanceMethodSignatureForSelector
获取指定类的selector的签名),返回值非空则通过forwardInvocation:转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:
消息,程序崩溃退出。
先检查缓存中是否已经存在selector的签名
- 若存在且签名是一个NSNull类对象的话,则返回nil终止消息转发流程(这个判断的意义在于缓存没有时,到NSObject的所有子类也找不到响应该selector的类或该类没有对应该selector的签名,简单说就是调用了全系统都木有的方法,理应继续崩溃);
- 若不存在且缓存未初始化,则初始化缓存并收集起所有NSObject的子类;
历遍NSObject的子类
- 若响应当前selector,则将其对selector的签名进行缓存,并返回此签名,系统接着发送forwardInvocation消息;
- 若找不到响应selector的类或其签名为空,则将NSNull的null作为该selector的签名进行缓存,并返回nil(存上NSNull的null是为了方便后续对缓存结果的判断,是否一个合理范围的selector)。
有几个实现细节点值得学习
- 线程安全,利用
@synchronized([NSNull class])
对缓存的初始化、读取实施线程锁,同时缓存的读取在锁前和锁后分别做了两步判断,目的是防止等待解锁的过程中缓存更新的部分包含了目标值而重新执行一遍,降低了效率; - 执行顺序,缓存初始化执行前先判断当前是否在主队列,是则直接执行,否则利用GCD的
dispatch_sync
同步方法在主队列执行,这样避免了主队列出现死锁的可能,同时使初始化下面的逻辑和锁外的逻辑按顺序地等初始化完成后再执行。
- 线程安全,利用
2.重写消息转发处理
- (void)forwardInvocation:(NSInvocation *)invocation
将转发的target设为nil,这样调用任何方法就都是安全的,后面也就不会再出现消息转发,最后找不到方法而抛出异常导致崩溃。
使用方法
把唯一的m文件引入项目就完了,它会在运行时自动加载功能,不需要引用任何h文件。
若某些target或者scheme不想使用NullSafe,可以再build settings里配置预编译宏NULLSAFE_ENABLED=0
,又或者在prefix.pch里添加
#ifdef DEBUG
#define NULLSAFE_ENABLED 0
#endif
JSON中的Boolean
最后,还模拟确认了一下Java后端API的布尔值字段返回true和false给我们的效果
可以看出,Cocoa直接把true或false的Boolean转换为数字类1或者0,我们可以通过对该数字字段值调用boolValue这样的方法,方便阅读语义。(注意输出中的json object,NSDictionary的直接输出并不能直接当作JSON的格式,只能用作参考转换原生对象后的效果,像字符串已经不带双引号)
总结
与服务端必须约定好交互API的数据格式和规范,避免掉坑,同时客户端也应该有自己的一套防御机制,不能完成倚靠别人为你做多少。而关于数据格式的,除了上面提到的Boolean,像float、double、date这些都是需要特别注意的类型,这里就不展开探讨,先抛出来。
参考
ios runtime浅析(二):消息转发
API返回结果设计经验与总结
Objective-C的“多继承”——消息转发
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 mingfungliu@gmail.com
文章标题:处理服务端API返回JSON的总结
文章字数:1.7k
本文作者:Mingfung
发布时间:2018-01-09, 14:35:00
最后更新:2018-01-09, 18:26:52
原始链接:http://blog.ifungfay.com/iOS/处理服务端API返回JSON的总结/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。