Android Architecture Component 解析之 ViewModel

概览

在考虑到UI 元素生命周期变化的环境下,架构组件中的 ViewModel 主要用于存储以及管理UI相关的数据。ViewModel 可以在配置信息变化(configuration change) 时 (例如屏幕的旋转) 保持数据状态。

众所周知的是,Android系统框架管理着UI控制器(Activity / Fragment)的生命周期。出于对用户行为以及设备事件的响应,系统可能会销毁或重建UI控制器,而这些行为是完全不受控制的。如果系统销毁 或是 重建UI控制器时,任何UI相关的暂存数据都将会被清除。

另一个问题是,UI控制器通常需要频繁地调用异步接口,耗费一定的时间获取数据。除了维护相关的接口外,在UI控制器被重新创建时往往会发起重复的请求,这无疑会造成资源的浪费。

UI 控制器如Activity / Fragment主要用于展示UI数据,响应用户请求,以及处理系统的事件,如授权访问请求。在UI 控制器中加入请求加载数据库或是网络的数据的逻辑将使得类不断膨胀。将大量无关的职责分配给UI 控制器,而不是将各项职责分配出去则会导致出现 God Object,同时会降低整体项目的可测试性。

ViewModel 的生命周期 ( outlive Fragment or Activity ?)

且看官方给出的ViewModel生命周期图示:


viewmodel-lifecycle

对照Demo中的实际效果:

vm_test

利用ViewModel 在旋转屏幕后,可以看到ViewModel的instance 还是同一个,而且其中的score 数据依然保存了下来。(观察实际的log也可以印证这一点)

如果直接按back 键退出,可以看到几乎在ActivityonDestroy() 方法调用的同时,ViewModel#onCleared()方法被调用,此时ViewModel也随即销毁了。

显而易见的是,除了数据已经保存外,还有一个好处在于省去了需要手动关联/解除ViewModel与界面生命周期的调用。即不必在先前自实现的ViewModel|Presenter中添加类似OnDestroy()方法,并发起显示的调用了。

实现

以上的ViewModel中实现了‘自知’的生命周期,这究竟是如何做到的呢?让我们退一步回到在使用Fragment时,看如何在转屏条件下保存内存中的临时数据。

从Fragment的角度来看状态保存

在转屏时,按默认的情况,FragmentManager会负责销毁相关联的Fragment, Fragment的生命周期 onPause, onStop, onDestroy会被相继调用。然而Fragment有一个特性可以设置Fragment#setRetainIntance(true)保存当前的自身实例(instance)。当该选项设置后,Fragment 当前实例被保存下来了,然后随即被传给新的Activity。留存的Fragment实际利用了Fragment关联的View可以被销毁以及重新创建,而不需要销毁Fragment自身。

在发生configuration 改变的时候,Fragment 内部的View与Activity 包含的View 一样,都会因为这一原因被销毁以及重新创建。即当有一个新的configuration变化时,很有可能需要新的资源,为了使用更好适配的资源,View将会重新创建。

随后FragmentManager会检查每一个Fragment的retainInstance属性,如果值为默认的false,FragmentManager会将Fragment实例销毁,Fragment和它关联的View都将会在新的Activity中重新创建;而如果retainInstance值为true,当前Fragment关联的View将会销毁而保留下Fragment的实例,在新的Activity创建时,新的FragmentManager会找到留存的Fragment,并重建它的View。

留存的Fragment没有被销毁,而只是从快要销毁的Activity上解绑了。表示为retain状态的Fragment仍然存在,只是不被任何Activity持有。整个转屏过程,对应生命周期的变化可见下图:

代码寻踪

按照上述Fragment实现的逻辑,架构组件中的实现又是怎样的呢?让我们从源码中寻找线索, 以Activity中的ViewModel创建为例。

ViewModelProviders.of(activity).get(YourViewModel.class)

ViewModelProviders.of(activity)
    -> new ViewModelProvider(ViewModelStores.of(activity), factory) // # factory 为默认的 AndroidViewModelFactory
            ViewModelStores.of(activity)
                -> ViewModelStore
                HolderFragment#holderFragmentFor(activity).getViewModelStore()
                    HolderFragmentManager#holderFragmentFor(activity)
                        FragmentManager fm = activity.getSupportFragmentManager();
                        HolderFragment holder = fm.findFragmentByTag(HOLDER_TAG)
                        if (holder != null) {
                            return holder;
                        }

                        holder = new HolderFragment();
                            public HolderFragment() {
                                setRetainInstance(true); // retained!
                            }
                            public void onDestroy() 
                                super.onDestroy();
                                mViewModelStore.clear();
                                    for (ViewModel vm : mMap.values()) {
                                        vm.onCleared(); // notifies ViewModels that they are no longer used.
                                    }

                        fm.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss();
                holder.getViewModelStore()

viewModelProvider.get(modelClass) //YourViewModel.class
    get(key, modelClass)
        ViewModel viewModel = mViewModelStore.get(key); // 检查是否已经有缓存
        if (modelClass.isInstance(viewModel)) {
            return (T) viewModel
        }

        viewModel = mFactory.create(modelClass);
            AndroidViewModelFactory#create(modelClass)
                if(AndroidViewModel.class.isAssignableFrom(modelClass)) // 检查是否为AndroidViewModel类别
                    return modelClass.getConstructor(Application.class).newInstance(mApplication);
                super.create(modelClass)
                    NewInstanceFactory#create(modelClass)
                        return modelClass.newInstance(); // 调用默认构造方法构造实例

        mViewModelStore.put(key, viewModel); // 添加ViewModel到缓存中
        return (T) viewModel;

可以发现存储的ViewModel实际也是利用Fragment 设置为retained的特性实现的。它的生命周期与内置的不可见的HolderFragment是紧密联系的。在Fragment最终被销毁时,ViewModelonCleared用于执行清理动作的回调方法同时被触发。

与 onSaveInstanceState() 的异同

ViewModel保持的实际是暂存的UI中的数据,但是它们并没有被持久化(persisted)。一旦相关的UI控制器被销毁或是App进程被暂停(由于系统资源的限制),ViewModel和所包含的数据将被GC处理。

onSaveInstanceState() 该方法在以下2种情况下用来保留少量的UI相关数据。

  • App在后台由于资源限制被暂停(stopped)
  • 配置变化 (configuration change)

系统在UI层面已经利用onSaveInstanceState() 去保存View的状态信息(如 EditText中已输入的文本, ListView中的滑动位置等)。由于该方法在主线程中执行,并且序列化本身会有一定的内存开销,即要求此方法能较快执行,以免出现UI掉帧或是更严重的性能的问题。所以它的设计并不适合大数据的存储。

Fragment#setRetainInstance(true) 方法利用retained Fragment 实例,可以保留较大规模的数据 如 bitmap 或是像网络连接(network connections)这样复杂的对象。

值得注意的是,ViewModel 只在由配置信息变化改变(configuration change)引起的销毁过程中留存,当进程被暂停时,它也被销毁了。

ViewModel 以及 Retained Fragment 测试源码

Github: ArchComponentPlayground

其它ViewMode实现

https://github.com/inloop/AndroidViewModel

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容