赞
踩
安卓基础知识系列旨在简明扼要地提供面试或工作中常用的基础知识,让对安卓还不太熟悉的小伙伴更快地入门。同时自己在工作中,也没法完全记住所有的基础细节,写这样的系列文章,可以让自己形成一个更完备的知识体系,同时给自己日后留个知识参考。
安卓四大组件中,最常用的组件莫过于我们的Activity组件。安卓程序员每天都在直接或间接地接触着Activity,所以Activity基础知识的重要性不言而喻。
正常情况下,新建一个Activity A会顺序经历如下几个生命周期:
如果此时用户拉起另一个Activity B, Activity A会顺序经历如下几个生命周期:
onPause: 表示 A 正在停止,准备从前台返回至后台,此时可以做一些停止动画,数据存储等工作。值得注意的是,在onPause生命周期进行的工作不能太耗时,不然会影响 B 的显示。(Activity A的onPause执行完后,Activity B的onResume才会执行)。
onStop: 在 A 完全不可见时调用,紧随着onPause执行,表 A 即将停止,此时 A 已经不在前台,可以做一些稍微重量级的回收工作,但同样不能太耗时,(如果此时新打开的Activity B是对话框式的Activity,背景存在一定区域是透明的,则Activity A的onStop不会调用)。
onDestroy:表示 A 即将被销毁,在这里可以进行资源的回收、释放工作。一般是经过用户按下back键或者系统资源紧张时,将Activity A释放掉以获得更多的内存时调用。
Activity B经历了onResume生命周期后已经显示在前台,如果此时按下back返回键,从 B 页面返回,而 A 还停留在onStop,没有经过onDestroy生命周期的话,A 会经历如下几个生命周期后重新显示:
可以看到,排除Activity退到后台的情况,Activity从创建到销毁,总共会经过6个生命周期,分别是onCreate,onStart,onResume,onPause,onStop,onDestroy。从这几个生命周期发生时的特性来看,会发现onCreate与onDestroy、onStart与onStop、onResume与onPause是一种相反的状态。
如果该Activity有事件或服务需要注册(register),一般会在onCreate中进行,而对应的解注册操作(unregister),最好在与onCreate对应的onDestroy中完成,释放资源,这是一种良好的编程习惯。
将上述几个生命周期总结成一张图:
通过上面的文字描述,看这个图应该已经很清楚了,不过,未提到的是,上图中onPause()还有个箭头指向了onResume(),这是一种极端情况。即考虑当Activity A 跳转到Activity B 的情况,此时 A 还在执行onPause() , B 还未显示出来。快速地从B回到A,此时会直接执行 A 的onResume()而不会走onRestart()。不过一般很难复现这种操作,大家留个心眼就行。
在将上文的细节提炼一下:
onStart、onResume、onPause、onStop看起来回调调用的时机差不多,它们俩区别在哪呢?
onStart和onStop是从Activity是否可见的角度来回调的,而onResume和onPause则是从Activity是否位于前台、是否可以与用户交互的角度来回调的,除了这方面的差别,在时机使用过程中,它们没有其他明显区别。
从Activity A 跳转到Activity B,是先执行 A 的onPause(),还是先执行 B 的onResume()呢?
这部分设计Activity跳转的源码,源码逻辑太深、太复杂就不先在基础篇讨论了,大家目前 先记住结论就好:A 的onPause()会先执行,然后才执行 B 的onResume(),这个细节也是面试中可能会问到的点。
在onPause中不能进行耗时的操作,否则会影响新Activity的显示,稍微重一点的操作可以放在onStop中,但依然不能太耗时。
如何 Activity A 启动一个透明的 Activity B,会经历哪些生命周期呢?
这是我面试遇到的一个问题,因为 B 页面透明, 所以跳转到 B 页面后,A 页面依然可见,因此就不会调用 Activity A 的 onStop 方法。
异常情况就是除开用户自己主动退出Activity的情况。
考虑一种异常情况,Activity C 打开了Actvity D后,C进入了停止状态(调用了onStop()),此时系统内存不足,需要回收 C(调用C的onDestroy()) ,当用户从 D 返回到 C,C 会被重新创建(调用onCreate())。如果原来 C 里边有临时状态存储着,比如TextView中的文字。那么从 D 返回 C 时,C因为重新创建,如果TextView未指定ID,那它原来的文字就会消失,这一定程度影响了用户的体验。
因此为了优化用户体验,Activity提供了一个onSaveInstanceState()回调方法,这个方法可以保证异常情况下,在Activity被回收之前一定会被调用。
onSaveInstanceState()方法会携带一个bundle参数,我们可以通过bundle对象,存储一些简单的状态信息。
Activity重新创建后,系统会调用onRestoreInstanceState()
,并把Activity销毁时onSaveInstanceState()方法所保存的Bundle对象作为参数同时传递给onRestoreInstanceState()和onCreate()。
你可以选择这两个方法中任意一个来恢复数据,二者的区别是:onRestoreInstanceState一旦被调用,其bundle对象一定是有值的,而onCreate在正常启动Activity的情况下bundle对象是无值的。
override fun onCreate(savedInstanceState: Bundle?) {}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {}
调用时机
onSaveInstanceState()
在onStop()
之前调用,onRestoreInstanceState()
会在onStart()
之后调用。
异常情况下,Activity数据的存储和恢复的生命过程都是一样的,常见的异常情况主要有以下两种:
资源相关的系统配置发生改变导致Activity被杀死并重新创建
首先说说什么是系统配置信息。
不同手机设备的分辨率不同,要将图片适配不同大小的手机屏幕,我们通常会在drawable-xhdpi,drawable-xxhdpi,drawable-xxxhdpi等目录中存放对应大小的图片Resource文件。
当App启动时,系统就会根据当前设备的屏幕情况去加载合适的Resource资源。同一台设备的横屏和竖屏时的屏幕大小也是不一样的,如果当前Activity处于竖屏状态,突然旋转至横屏,那么此时系统的屏幕配置发生了改变。
默认情况下,Activity会被销毁并重建。因为这种销毁是一种非用户主导的、异常的情况,Activity会调用onSaveInstanceState()方法后销毁,重建时会再调用onRestoreInstanceState()方法,即走一遍异常情况的生命周期。
如何避免这种因为系统配置更改而导致Activity重建的异常情况?
如果app在应用配置变更期间无需更新资源,我们可以在AndroidManifest.xml
文件中相应的Activity声明,自行处理相关配置的变更,从而阻止系统重建Activity。
只需指定相关的configChanges属性。比如下面的例子,就阻止了当屏幕发生旋转时Activity的系统自动重建。
<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize" />
当configChanges中指定的配置发生变化时,系统会调用Activity的onConfigurationChanged()
方法,如果有需要处理配置变更的话,可以在这个方法手动处理。一般我们在屏幕旋转时,希望Activity能保持原样,不重建就好了,所以空实现该方法即可。
当然,需要自行处理时,比如检查当前设备的方向,你可以这么写:
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Checks the orientation of the screen
if (newConfig.orientation === Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show()
} else if (newConfig.orientation === Configuration.ORIENTATION_PORTRAIT) {
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show()
}
}
configChanges属性可以指定很多属性,如果你还想指定更多配置,不同配置间用"|"分隔,比如上面那样。
部分常用的configChanges配置项目如下:
项目 | 描述 |
---|---|
keyboard | 键盘类型发生变更 — 例如,用户插入外置键盘。 |
keyboardHidden | 键盘无障碍功能发生变更 — 例如,用户显示硬键盘。 |
locale | 语言区域发生变更 — 用户已为文本选择新的显示语言。 |
orientation | 屏幕方向发生变更 — 用户旋转设备。 请注意:如果应用面向 Android 3.2(API 级别 13)或更高版本的系统,则还应声明 "screenSize" 配置,因为当设备在横向与纵向之间切换时,该配置也会发生变更 |
screenLayout | 屏幕布局发生变更 — 不同的显示现可能处于活跃状态。 |
screenSize | 当前可用屏幕尺寸发生变更。 该值表示当前可用尺寸相对于当前纵横比的变更,当用户在横向与纵向之间切换时,它便会发生变更。(API 13 中新增) |
这个表格我直接照搬安卓开发者官网的,官网有关于系统配置更完整的资料,大家有需要可以自己点开看看,科学上网。
系统内存不足导致低优先级的Activity被杀死
这种情况就是我们分析异常情况下的生命周期时举的例子。Activity C 跳转至Activity D,C 处于后台,当系统内存资源不足时,C的优先级较低,会被系统销毁以获得更多的内存,然后再从 D 回到 C ,C 会被重建,走一遍异常时的数据存储和恢复的生命过程。
Activity按照优先级从高到低,可以分为如下三中情况:
当系统内存不足时,系统会按照上述优先级的顺序去杀死Activity所在的进程。并在后续通过onSaveInstanceState()和onRestoreInstanceState()去存储和恢复数据。
如果一个进程中没有四大组件在执行,那么这个进程将会很快被系统杀死,因此一些后台工作不适合脱离四大组件而单独运行在后台中。比较好的方法是将后台的工作放到Service服务中,从而保证进程有一定的优先级,就不会容易被系统杀死了。
关于Activity生命周期的经典知识,看到上面,已经足够日常工作中的使用了。
下面介绍点偏冷门的知识和数据存储和恢复更优美的技术,内容可能有些繁杂,如果你看的吃力,可以跳过这部分,对你理解Activity的生命周期不会有任何影响!
在这两个方法中,系统自动为我们做了一定的恢复工作。当Activity在异常情况下需要重建时,系统为默认为我们保存当前Activity的视图结构,并且在重启后为我们恢复这些数据,比如文本框中用户输入的数据、ListView滚动的位置等等。
// TextView 的onSaveInstanceState()方法 @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); // Save state if we are forced to final boolean freezesText = getFreezesText(); boolean hasSelection = false; int start = -1; int end = -1; if (mText != null) { start = getSelectionStart(); end = getSelectionEnd(); if (start >= 0 || end >= 0) { // Or save state if there is a selection hasSelection = true; } } if (freezesText || hasSelection) { SavedState ss = new SavedState(superState); ... ss.error = getError(); if (mEditor != null) { // 存储editor状态 ss.editorState = mEditor.saveInstanceState(); } return ss; } return superState; } // TextView 的onRestoreInstanceState()方法 @Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); // XXX restore buffer type too, as well as lots of other stuff if (ss.text != null) { // 调用setText,恢复数据 setText(ss.text); } ... }
上面是EditText竖屏和横屏切换时,text数据存储和恢复演示的效果。
View的onSaveInstanceState()调用过程是这样的:首先Activity被重建时,会调用onSaveInstanceState()去保存数据,然后Activity会委托Window去保存数据,接着Window再委托与它关联的顶层容器DecorView去保存数据,最后DecorView再遍历它一个个子View去保存数据。
View的onRestoreInstanceState()也是类似的思想。
值得注意的是,如果你希望View在异常销毁时能顺利调用onSaveInstanceState()
方法,你必须得为该View指定一个id,否则View不会走到onSaveInstanceState()流程。
isSaveEnable也会影响onSaveInstanceState()的调用,这个值是个标志位,表示View是否会保存其状态。默认为true,即,默认会保存状态。
如果你不想View保存状态,可以将其设为false
<com.jamgu.hwstatistics.MyEditText
android:id="@+id/edit_text"
...
android:saveEnabled="false"/>
View异常情况下的数据存储与恢复介绍完了,说这个知识点冷门,其实只是我的看法。我之前看到过这个知识点,但我工作了一年多的时间,完全没有涉及过View的数据存储与恢复的内容,所以就淡忘了,因此说其冷门。
今天写这篇内容的时候,才又想起来View也有异常情况的数据恢复过程!不过其实忘掉也没什么关系。在工作中,View销毁了后,我们一般会让它重新走一遍数据加载的过程,不需要执行其默认的数据存储与恢复。
再说它系统默认恢复的状态也是有限的,一般工作中的数据都是从网络拉回来的数据,所以我们更偏向于直接从网络再拉一遍数据回来。
上面的知识可能有点晦涩,View的流程如果你跟着源码看,应该会有更深刻的认识。如果你是个新手,对安卓还不是很熟悉,看上面这一部分内容可能有些吃力,看不太懂。
不过完全没关系,View的数据与恢复,你只需要知道有这个过程就行了,
接下来我将为大家介绍Jetpack库的组件ViewModel,它在设备竖屏与横屏切换时的状态存储和恢复,比传统的用onSaveInstanceState()、onRestoreInstanceState()方法更优。
JetPack库是一个由多个库组成的套件,可帮助开发者遵循最佳做法、减少样板代码,降低代码间的耦合度,并让代码在不同Android版本的设备中保持一致的体验。fragment、dataBinding、ViewModel,MVVM等都是JetPack的一部分。
大家现在基本都androidx库,在这之前,你可能还用过android-support-v7,v28库等等,为什么会有support库,就是因为随着安卓版本的迭代,一些旧的控件在新的安卓版本上难以兼容,所以为了保持体验的一致性,安卓每发一个新的版本,就会有一个support库跟着发出来。
最后发布的是android.support.v28系列库,这样的命名格式,随着安卓版本的升级,support库越来越多,谷歌也意识到这不是一个办法。因此发布了一个androidx库来统一,以后发布新的版本,只需要对androidx库进行升级迭代就可以了,就不需要再发布新的库。JetPack库是androidx库的一部分。
所以回到正题,ViewModel为啥更优?主要有以下几个优点:
可以存储更复杂的临时状态。
前文提到,Activity的onSaveInstanceState()主要是通过将状态存储到Bundle对象中,Bundle对象中的数据是一个个键值对,一个key对应着一个value,存储基本数据类型,如Int、Float、Boolean等很方便,但如果我们要在bundle中存储一个对象,就会很麻烦。
首先这个对象需要继承Parcelable接口,并实现几个方法,使对象能够序列化,这需要开发者编写一定的代码。
其次要从Bundle取出对象,需要经过反序列化的过程,如果你需要反序列化的对象有很多个,那么用户可能需要等待一段时间才能看到之前的状态,不仅消耗了性能,对应用的用户体验也会有一定的影响。
而ViewModel,它是一个对象,其状态是存储在内存的,存取复杂状态不需要经过序列化和反序列化的过程,性能会比从Bundle高很多。
将数据,也就是状态,交给ViewModel管理,可以分担Activity的工作,易于解耦,方便后期的维护。
像Activity和Fragment(后续的文章会提到)之类的组件,主要用于显示界面数据,对用户操作做出响应,进而给予用户UI上的反馈。比如用户按下了一个按钮,按下时按钮颜色变深。
如果要求Activity也负责从数据库或网络加载数据,这不仅需要在Activity维护加载数据的代码,也需要维护数据加载回来后的一个个异步的数据加载回调,甚至你还得考虑当Activity退出时,异步回调对象的释放工作,这会使Activity类越发膨胀,臃肿。
给Activity分配过多的责任可能会导致单个类尝试自己处理应用的所有工作,会增加测试和维护的难度。将数据加载和回调的工作交给ViewModel,将数据加载的逻辑工作与Activity的UI显示工作分离,不仅能让逻辑更清晰,同时也可以避免一个类代码量太多,太臃肿,难以维护。
ViewModel的生命周期
ViewModel对象存在的时间范围是获取ViewModel时传递给 ViewModelProvider的Lifecycle 。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失。
比如你在Activity中通过ViewModelProvider获取了一个ViewModel,那么这个lifecycle指的就是Activity的生命周期。
我们一般会在Activity执行onCreate()方法时拿到ViewModel对象,系统可能会在activity的整个生命周期内多次调用onCreate(),比如在旋转设备屏幕时。
**ViewModel存在的时间范围是在首次请求ViewModel对象直到Activity完成并销毁。**在一个生命周期内,多次请求ViewModel对象,获取到的都是同一个ViewModel对象。
ViewModel的简单使用
首先要用ViewModel,需要引入它的库。
// ViewModel and LiveData
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
简单演示下加速器的demo,布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".viwemodel.ViewModelActivity"> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/tv_number" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="24sp" android:textColor="#000" android:gravity="center" android:layout_centerInParent="true"/> <androidx.appcompat.widget.AppCompatButton android:id="@+id/btn_add" android:layout_width="match_parent" android:layout_height="46dp" android:text="ADD NUMBER" android:layout_below="@+id/tv_number" android:layout_marginTop="20dp"/> </RelativeLayout>
布局文件比较简单,就不解释了,接下来看看逻辑代码:
class ViewModelActivity : AppCompatActivity() { private lateinit var vNumber: AppCompatTextView private lateinit var mViewModel: UserModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_view_model) val viewModelProvider = ViewModelProvider(this) mViewModel = viewModelProvider.get(UserModel::class.java) vNumber = findViewById(R.id.tv_number) findViewById<AppCompatButton>(R.id.btn_add).setOnClickListener { mViewModel.count++ setCounter() } setCounter() } private fun setCounter() { vNumber.text = mViewModel.count.toString() } }
运行,看看效果:
可以看到,activity没有指定configchanges属性,在竖屏和横屏切换时,TextView的状态也是没有丢失的。
ViewModel的介绍就到这里啦,ViewModel还有很多用途,例如在同一个Activity间的两个Fragment之间共享数据。如果配合JetPack库的另一个LiveData使用,它将更加强大。
课外知识提到了几个本文没有涉及的概念,如Fragment,Parcelable序列化等,如果你对安卓还不是很熟悉,对上面提到的一些名词感觉很陌生?不用担心!完全没有关系!这些不影响你理解Activity的生命周期,课外知识只是做个科普,如果你觉得这部分太复杂,你甚至可以跳过这部分内容,没有任何关系。这些知识在工作中用的很少,包括ViewModel这些比较新的组件,或许考虑到代码重构的成本,大厂里一些的老项目里也很少用到,你只需要记住Activity生命周期的经典知识就可以了!
下一篇内容,Activity启动模式~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。