iOS开源:Dr.Light - 轻量 iOS crash 保护方案

VerKhull 7年前
   <p>前一段时间看了网易大白健康管理系统的文章,感觉很不错,文中对iOS app常见的几种crash进行了总结和分析,并提出了修复方案。平时开发过程中,笔者也略有积累,对一些潜在的crash也有自己的处理方案。利用业余时间开发完成一个简单轻易的开源库,专门处理几种常见的crash,虽还不完善,秉承回馈开源社区,为大家做下分享。</p>    <p>项目命名为Dr.Light,灵感来自于去年看的漫威电影《惊奇博士》。目前修复了以下几种情况:</p>    <h3>UI 非主线程刷新</h3>    <p>如果UI的刷新没有在主线程发生,导致的后果包括可能会有crash、动画失效、界面无反应或者异常等。</p>    <p>解决方案:利用runtime,hook了view的 setNeedsLayout 、 setNeedsDisplay 、 setNeedsDisplayInRect: 、 setNeedsUpdateConstraints 四个方法,判断当前是否是主队列,如果不是主队列,则跳转到 dispatch_get_main_queue 执行。</p>    <h3>KVO 非对称添加删除</h3>    <p>我个人在响应式编程中偏爱使用KVO,它带来的益处不言自明。如果因为疏忽导致add observer/remove obsever不成对出现,可能会导致crash。</p>    <p>解决方案:hook了NSObject的 addObserver:forKeyPath:options:context: 、 removeObserver:forKeyPath: 。为被观察对象增加一个map属性,存储所有的observer和keypath的对应关系。每次添加或者删除观察者的时候,都会先去检查observer的keypath是否已存在或者消失,在执行相当逻辑。避免重复添加或者删除。</p>    <p>下面展示添加观察者的简单流程图,删除类似。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b34229f0dfb9e27a4897777b36188eba.png"></p>    <p>注意:使用前需要设置被观察者的 kvoSafteyToggle 为true,原因是因为许多系统操作会执行kvo操作,没必要为系统kvo增加保护,浪费内存。 非死book的开源库 kvocontroller 可以解决此问题,还能保证线程安全,相比其本项目更轻,代码量更少,有兴趣的可以看下。</p>    <p>完善:被观察者销毁的时候,是否还有残留观察者,这个项目没有加。如果做的话方案是hook dealloc 加判断。</p>    <h3>navigationcontroller的连续push</h3>    <p>先说一个常见的例子,比如点击某个按钮push到一个新的界面,如果连续快速点击,界面就会发生连续的push,这种情景下不一定会发生crash。我见过网上有一种解决方案,也是利用运行时hook了button的sendaction事件,增加时间戳或者计数器,来阻止短时间点击事件的连续调用。个人觉得这个方案虽然能解决问题,但只是浮于表面,甚至可以说是本末倒置。button并没有做错什么却要“背锅”,应该让真正的”肇事者“navigation去处理。</p>    <pre>  <code class="language-objectivec">button action ----->  push viewcontroller</code></pre>    <p>crash的情景,在viewcontroller的 viewDidLoad 执行pushViewController方法,由于此时界面布局尚未完成,会发生 nested push 崩溃,用umeng或者bugly会收集到 cannot addsubView:self 错误信息。</p>    <p>解决方案:hook UINavigationController 的 pushViewController:animated: 方法,设置一个时间间隔,记录每次push的时间,如果距离上次push时间间隔较短,则禁止push操作。</p>    <h3>unrecognized selector</h3>    <p>未知选择器,具体原因和系统的消息转发机制有关,这里不在专门讲述。</p>    <p>解决方案:hook NSObject的 methodSignatureForSelector: 和 forwardInvocation: 。创建一个单例对象,在 forwardInvocation: 中将消息转交该单例处理。利用runtime为单例中增加一个方法,增加一个目标selector的新指向IMP(具体实现为空),这样单例就能像安全地全盘接收无效selector。</p>    <p>开始的时候打算把消息的拦截处理放在 forwardingTargetForSelector 中,毕竟这里提供转发对象就可以了,而且这个函数的调用时机比上面两个都早,理论上越早拦截无效操作越好。不过开发过程中,我发现许多系统级函数例如 getServerAnswerForQuestion:reply: 居然也会出现在 forwardingTargetForSelector 里,这让我有些意外。无奈之下,只能将拦截操作下沉。</p>    <h3>总结</h3>    <p>还有几种错误,比如数组越界,空指针插入dictionary。这种情景跟业务牵扯较大,即使加了处理,但后续界面展现也可能造成困扰,建议开发者在开发阶段就增加判断。</p>    <pre>  <code class="language-objectivec">NSString *str  = [@"Tom Hanks" substringFromIndex:10];      _label.text = str;</code></pre>    <p> </p>    <p> </p>