前面我们对MVC、MVP、MVVM进行了详尽的分析,但还有一个问题悬而未决,那就是生命周期。在Android平台上生命周期具有十分重要的意义,因此这也是架构必须考虑的因素之一。生命周期处理不好很容易发生内存泄漏,但对架构而言,真正困扰我们的却不是内存泄漏的问题,反而是因生命周期太短,实例被销毁重建,从而产生一系列不必要的行为。这种情况发生的场景主要在屏幕旋转以及页面被系统回收时。
Activity难免需要依赖网络、数据库等数据来渲染页面,当屏幕旋转时,Activity重建,因而数据需要重新加载,但这完全没有必要。一种策略是对数据进行缓存,这是一种可考虑的方案,但它只解决了一半的问题,如果Activity重建发生在数据返回前,此时根本来不及缓存,下一次请求就迅速地发生了。
在MVP、MVVM架构中,数据由M来提供,但真正和生命周期打交道的是P和VM,我们得从这里着手解决生命周期的问题。再明确地说一遍,我们要解决的问题是不论在数据返回前还是返回后,在屏幕旋转这种场景下都不需要多次加载数据。这个问题由两种状态组成:加载中和加载完成后,对于前者我们要知道当前正在加载数据,对于后者则只需要把数据缓存起来即可。
对数据缓存很简单,但加载中的状态就要好好斟酌一番了,我们可以轻易地给这个状态加标记,但随着重建这个标记也会被回收,由此可以想到两种应对之法,一是让P和VM不被回收,这样就可以进行标记了,二是让当前这个加载不被回收,也就是其生命周期不和P与VM同步。不让P和VM回收,有以下几种方式:
配置android:configChanges="orientation|keyboardHidden|screenSize"
onRetainCustomNonConfigurationInstance()
/getLastCustomNonConfigurationInstance()
继承Fragment
除了配置configChanges,其余两种方式都是不错的解决办法。除此之外还有一种方式可以同时实现我们说的两种应对之法,这就是Loader。关于什么是Loader以及Loader如何保持P和VM不被回收,大家可以自行查阅相关资料,如何保持一个加载任务不被回收,可以参阅architecture-samples(https://github.com/android/architecture-samples),并切换到分支deprecated-todo-mvp-loaders
。
我们不打算大刀阔斧地讲述每个方案的细节和优缺点,因为随着Jetpack诞生,这种复杂又费力的方案系统已经帮我们完成了,我们只需要了解系统是如何处理的即可。从书写代码变成查看代码,可以说大大减少了我们对生命周期的“怨恨”,不得不说Google这波操作很圈粉呢。在这里,我们只关注Lifecycle、ViewModel和LiveData三部分。
生命周期让人困扰的很大一部分原因是只有Activity这样的系统组件才可以感知生命周期的变化,而Lifecycle的出现则把这种感知力放大到了任何类。Lifecycle的原理很简单,当生命周期变化时,Activity通知到Lifecycle,其他类就可以通过Lifecycle感知生命周期的变化了。
Lifecycle的核心就三个类:Lifecycle、LifecycleOwner和LifecycleObserver。从名字就可以轻易看出这是一个观察者模式,Activity作为LifecycleOwner,把生命周期的变化反映到Lifecycle,Lifecycle再通知给所有的LifecycleObserver即可。这个概念太简单了,就不在此赘述源码了(看了一下没有什么亮点~),不过如果你感兴趣,请注意一下ReportFragment这个类,Activity的生命周期就是通过它来通知Lifecycle的(添加一个看不见的Fragment,这个操作似乎似曾相识?)。
Lifecycle只是让P和VM获得了生命周期感知能力,并没有解决如何保持的问题,不过它是我们后面内容的基础,所以还是很有必要了解一番。
这个ViewModel其实就是MVVM中的VM,但经过Google的加工之后具备了很好的生命周期感知能力,这就是我们苦苦追寻的东西呀。现在我们就对它抽丝剥茧,看看系统是如何完成这件事的。
ViewModel的使用非常简单,就是一句话:
class LoginActivity : AppCompatActivity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... loginViewModel = ViewModelProvider(this, LoginViewModelFactory()).get(LoginViewModel::class.java) } }
当重建发生时,LoginActivity、ViewModelProvider都是新的实例,但是LoginViewModel一定得是原来的实例,这说明它在某处被缓存了起来。先看下ViewModelProvider做了什么吧:
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) { this(owner.getViewModelStore(), factory); } public <T extends ViewModel> T get(@NonNull Class<T> modelClass) { String canonicalName = modelClass.getCanonicalName(); // ... return get(DEFAULT_KEY + ":" + canonicalName, modelClass); } public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) { ViewModel viewModel = mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) { // ... return (T) viewModel; } else { // ... } if (mFactory instanceof KeyedFactory) { viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass); } else { viewModel = (mFactory).create(modelClass); } mViewModelStore.put(key, viewModel); return (T) viewModel; }
非常简单,从Activity获取到了一个ViewModelStore,如果里面包含了LoginViewModel就直接取出来,否则新建一个并缓存到ViewModelStore里。那么ViewModelStore是什么,它是如何保持下来的?
ViewModelStore里维护了一个Map,存储ViewModel实例,仅此而已。AppCompatActivity实现了ViewModelStoreOwner接口,里面只有一个方法getViewModelStore,它的实现如下:
public ViewModelStore getViewModelStore() { if (getApplication() == null) { throw new IllegalStateException("Your activity is not yet attached to the " + "Application instance. You can't request ViewModel before onCreate call."); } if (mViewModelStore == null) { NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { // Restore the ViewModelStore from NonConfigurationInstances mViewModelStore = nc.viewModelStore; } if (mViewModelStore == null) { mViewModelStore = new ViewModelStore(); } } return mViewModelStore; }
这里出现了一个getLastNonConfigurationInstance(),我们在前面提过一个getLastCustomNonConfigurationInstance()方法,那么应该也有一个onRetainNonConfigurationInstance()与之对应,它的实现如下:
public final Object onRetainNonConfigurationInstance() { Object custom = onRetainCustomNonConfigurationInstance(); ViewModelStore viewModelStore = mViewModelStore; if (viewModelStore == null) { // No one called getViewModelStore(), so see if there was an existing // ViewModelStore from our last NonConfigurationInstance NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { viewModelStore = nc.viewModelStore; } } if (viewModelStore == null && custom == null) { return null; } NonConfigurationInstances nci = new NonConfigurationInstances(); nci.custom = custom; nci.viewModelStore = viewModelStore; return nci; }
一切都很明了了,系统用的是和我们一样的方法,只是方法名称稍有区别而已。ViewModelStore里缓存了ViewModel实例,那么在Activity真正销毁时二手游戏购买地图肯定需要清空,ViewModel和ViewModelStore都提供了一个clear()方法,ViewModelStore的clear方法实现如下:
public final void clear() { for (ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }
它会调用其中每个ViewModel的clear方法使我们有机会清除一些数据或任务,然后就将Map清空了。在Activity中它是这样被调用的:
getLifecycle().addObserver(new LifecycleEventObserver() { @Override public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { if (event == Lifecycle.Event.ON_DESTROY) { if (!isChangingConfigurations()) { getViewModelStore().clear(); } } } });
isChangingConfigurations()用以标识Activity执行onDestory方法后是否准备重建,只有不重建时才会清空ViewModel,所以只要在clear时清理数据和中断任务就好了。
现在我们解决了ViewModel实例保持的问题,接下来让我们再想想应该怎么解决数据重复加载的问题。数据重复加载主要是因为一个异步任务被多次调用,例如请求一个列表数据时,如果屏幕发生旋转,以下方法会被多次调用:
fun getUsers(){ executor.execute { val users = model.getUsers() handler.post{ view?.getUsers(users) } } }
按照之前的说法,可以给加载任务加上标记,当它正在加载中就等待它加载完成,如果已经加载完就取缓存的数据,但是这太复杂了,稍有不慎就会出问题。如何让事情变得简单一些,出错率低一些呢?
要想避免此问题,最好的方式是只调用一次getUsers()方法,那这个方法就不能由Activity来调用了,需要ViewModel自己调用,等它拿到结果后反过来通知Activity。这不就是MVVM吗?现在我们总算明白为什么被系统实现的这个类叫ViewModel了,因为它就是为MVVM量身定制的。
关于数据反过来通知Activity这件事,也不需要担心,因为系统照样帮我们实现了,这就是LiveData。
可观察的数据并不是只有LiveData,但LiveData有自己独特的本领,它也具备生命周期感知力。LiveData只有在有效的生命周期范围内通知观察者,并在生命周期结束后自动移除观察者,仅这一点就足够让它脱颖而出。我们可以从它的observe方法,了解它处理生命周期的大致流程。
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { assertMainThread("observe"); if (owner.getLifecycle().getCurrentState() == DESTROYED) { // ignore return; } LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); if (existing != null && !existing.isAttachedTo(owner)) { throw new IllegalArgumentException("Cannot add the same observer" + " with different lifecycles"); } if (existing != null) { return; } owner.getLifecycle().addObserver(wrapper); }
这里创建了一个LifecycleBoundObserver来观察Activity的生命周期,我们看看它做了哪些工作吧:
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver { // ... @Override boolean shouldBeActive() { return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); } @Override public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { if (mOwner.getLifecycle().getCurrentState() == DESTROYED) { removeObserver(mObserver); return; } activeStateChanged(shouldBeActive()); } // ... }
它实现了LifecycleEventObserver,并在DESTROYED状态时移除了观察者,其后只是调用了一个activeStateChanged方法,这个方法实现如下:
void activeStateChanged(boolean newActive) { if (newActive == mActive) { return; } // immediately set active state, so we'd never dispatch anything to inactive // owner mActive = newActive; boolean wasInactive = LiveData.this.mActiveCount == ; LiveData.this.mActiveCount += mActive ? 1 : -1; if (wasInactive && mActive) { onActive(); } if (LiveData.this.mActiveCount == && !mActive) { onInactive(); } if (mActive) { dispatchingValue(this); } }
这里通过是否active来分发数据,在dispatchingValue中会通知所有的观察者。
LiveData实际上是一个双层的观察者模式,它通过观察Lifecycle得知是否active,在此充当的是观察者。当它的值发生变化或者监听到Lifecycle变化时再通知到它的观察者,在此又充当被观察者。如此它就具备了我们想要的一切能力。
现在我们的架构“本地化”工作又前进了一大步,它终于在生命周期方面也不存在问题了,使用Lifecycle+ViewModel+LiveData组合,解决了架构最棘手的问题,也把MVVM推向了另一个高度。当然这并不代表着MVP就彻底败下阵来,毕竟生命周期问题只影响了初始化时的数据,大量场景下还是有无数的交互行为,需要根据用户的操作主动加载各种各样的数据,这种情况下,MVP的直观性要远远强于MVVM,这个特点也可以简单理解为MVP适合复杂交互场景,MVVM适合展示型场景。因此我们应该根据具体场景灵活选用MVP和MVVM,甚至在某些情况下可以合二为一。
还是那句话,没有最好的架构,只有最适合当前场景的架构。