Model-View-Intent 构建的响应式应用(五)轻松调试

2018-02-27 11:12:59来源:https://saplf.github.io/2018/01/05/mosby3-mvi-5/作者:不明,不名人点击

分享

翻译自 REACTIVE APPS WITH MODEL-VIEW-INTENT - PART5 - DEBUGGING WITH EASE 。


前几篇文章中,我们已经讨论了 MVI 模式的诸多特性,还记得第一篇 里的两个重点内容吗? 单向数据流 与业务逻辑驱动的应用 状态 。这篇文章就来看一看这两点是如何简化码农的调试工作的。


大家有没有遇到这样一种情况:接到了一条 bug 报告,但却无法复现这条小虫?很熟悉吧?我也遇到过!当时我花费了数个小时去追踪异常栈里的信息,梳理源码,然后……关闭了这个 issue,留下诸如“无法复现”、“这是哪个奇怪的设备导致的问题吧”一类信息。


我们正在写的这个购物应用中有一个这样的实例:在 home screen,有用户下拉刷新以获取最新的数据,产生了一个空指针异常。


所以嘛,作为开发者,要解决这个问题,当然是先要打开应用,在 home screen 下拉刷新看一看啦,然而这一次,应用并没有闪退。所以,你又仔细看了一遍代码,还是看不出会导致空指针的地方。你连上 debugger,一步步地执行相关组件的代码,但结果还是:它完全没毛病。这见鬼的应用怎么会在下拉刷新的时候崩呢?!


现在的问题就在于,你无法重现崩溃时的应用状态。要是用户能把应用崩溃前的状态连同异常栈都发给你多好!有了单向数据流和 MVI ,这会相当简单。我们只需要简单地记录下用户触发的所有 intents 与渲染在屏幕上的 model (model 反应了应用的状态 ,或者称之为 view state)就行了。下面,我们就在 HomePresenter 中为 home screen 添加日志吧(这个类的更多细节请看第三篇,那里我们讨论了状态转换机的优势)!下面的代码段里,我们使用了 Crashlytics ,其他日志收集库同理。



class HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {
private final HomeViewState initialState; // 展示加载框
public HomePresenter(HomeViewState initialState){
this.initialState = initialState;
}
@Override protected void bindIntents() {
Observable<PartialState> loadFirstPage = intent(HomeView::loadFirstPageIntent)
.doOnNext(intent -> Crashlytics.log("Intent: load first page"))
.flatmap(...); // 加载数据的逻辑
Observable<PartialState> pullToRefresh = intent(HomeView::pullToRefreshIntent)
.doOnNext(intent -> Crashlytics.log("Intent: pull-to-refresh"))
.flatmap(...); // 加载数据的逻辑
Observable<PartialState> nextPage = intent(HomeView::loadNextPageIntent)
.doOnNext(intent -> Crashlytics.log("Intent: load next page"))
.flatmap(...); // 加载数据的逻辑
Observable<PartialState> allIntents = Observable.merge(loadFirstPage, pullToRefresh, nextPage);
Observable<HomeViewState> stateObservable = allIntents
.scan(initialState, this::viewStateReducer) // 状态转换
.doOnNext(newViewState -> Crashlytics.log( "State: " + gson.toJson(newViewState) ));
subscribeViewState(stateObservable, HomeView::render); // 显示新状态
}
private HomeViewState viewStateReducer(HomeViewState previousState, PartialState changes){
...
}
}


通过 RxJava 的donOnNext()操作符,我们很容易为每一个 intent 的结果与 view state 加上日志。在这里,view state 序列化为 json 了,我马上就会说到这点。


现在,我们的崩溃日志就是这样的:



看一下这些日志:这里不仅有崩溃发生前的最新状态,还记录了用户到达该状态的历史路径。为了更好的阅读效果,我用下划线标记出了改变的状态,并将 data 字段(在 recycler view 中展示的数据)用 [...] 代替了。所以呢,这个用户操作步骤就是:


打开应用( load first page ),显示加载器( "loadingFirstPage": true
展示数据( data[...] )
滚动到底部,触发加载下一页的 intent ( load next page"loadingNextPage": true
下一页加载完成( "loadingNextPage": falsedata[...]
继续加载下一页
然后下拉刷新,状态变更( "loadingPullToRefresh": true
应用崩溃……

那么,我们怎么用这些信息来修复 bug 呢?很显然,我们了解用户触发的 intent,所以可以复现 bug。其次,我们还以 json 的形式存下了应用状态的快照(每一个状态)。我们可以很简单地只取最后一个状态,将 json 串反序列化,把这个状态作为我们的初始状态去修复这个 bug :



String json ="{/"data/":[...],/"loadingFirstPage/":false,/"loadingNextPage/":false,/"loadingPullToRefresh/":false} ";
HomeViewState stateBeforeCrash = gson.fromJson(json, HomeViewState.class);
HomePresenter homePresenter = new HomePresenter(stateBeforeCrash);


然后,我们开启 debugger,触发下拉刷新的操作就行了。最后的原因是,用户获取了两页数据之后,数据没了,而我们的应用没有考虑这种情况,未作出正确的处理,导致接下来的下拉刷新出错了。


结论

“快照”应用的状态让我们的开发更简单。这可不仅仅能够让我们轻易地复现 bug,我们还可以利用序列化而来的状态数据进行回归测试,这基本上是零成本的。需要明确的是,要实现这点,应用的状态必须是业务逻辑驱动的单向数据流,不可变,还有纯函数。 MVI 引导我们走向这个方向,让我们发现这个架构产生的副产物——“状态快照”,是如此的 nice 。


那么这种“快照”特性的缺点呢?那就是序列化状态的过程了,这需要一些额外的计算时间。在我的应用里,平均来说,状态的第一次序列化花费了 30 毫秒,在这期间, Gson 需要通过反射来扫描类中需要序列化的属性。在 Nexus 4 上,连续的状态序列化平均花费 6 毫秒。在我看来,这个时间用户是不会察觉的。然而,这种状态快照的一个问题就是,用户需要从设备上传到服务器的崩溃数据将会非常大。如果用户连接的是 wifi 还没什么大问题,但如果是流量用户那就……还有,最重要的一点,把状态序列化的过程中可能会泄露用户的敏感信息,所以,要么选择去掉这部分信息,要么选择加密,前者的不完整状态可能不足以复现 bug,后者需要更多的 cpu 时间,看情况而定。


总结一下就是:我个人从这种快照中看见了诸多优势,但你们需要自己权衡。或者,你们可以在各自应用的内部测试版本中试一下效果。


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台