1、回收活动
当一个活动进入了停止状态后,是有可能会被系统回收的。
如果遇到以下场景:用户在使用一个APP,在活动A的基础上启动了活动B,活动A这时就进入了停止状态,这个时候由于系统内存不足,系统将活动A回收掉了,然后用户按下Back键需要返回活动A,会出现什么情况呢?
其实还是会正常显示活动A的,只不过这时并不会执行onRestart()方法,而是会执行活动A的onCreate()方法,因为活动A在这种情况下会被重新创建一次。
这样看上去好像一切正常,可是别忽略了一个重要问题,活动A中是可能存在临时数据和状态的。
打个比方,MainActivity中有一个文本输入框,输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字全部都没了,因为MainActivity被重新创建了。
如果出现了这种情况,会严重影响用户体验,所以Activity中提供了一个onSaveInstanceState()回调方法,这个方法会保证一定在活动被回收之前调用,因此我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。
我们在MainActivity中重写父类的onSaveInstanceState()方法,该方法有一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。
每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。
在MainActivity中添加如下代码就可以将临时数据进行保存:
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.d(TAG, "onSaveInstanceState"); String tempData = "你想保存住的信息"; outState.putString("data_key", tempData); }
使用Bundle的一系列的putXXX()方法可以保存数据,那么应该在哪里进行恢复呢?
我们发现,之前一直使用的onCreate()方法其实也有一个Bundle类型的参数。
这个参数在一般情况下都是null,但是当活动被系统回收之前有通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。
修改MainActivity的onCreate()方法,如下所示:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate"); setContentView(R.layout.activity_main); if (savedInstanceState != null) { String tempData = savedInstanceState.getString("data_key"); Log.d(TAG, "data_key = " + tempData); } else { Log.d(TAG, "savedInstanceState is null"); } …… }
取出值之后再做相应的恢复操作就可以了,比如说将文本内容重新赋值到文本输入框上,这里我们只是简单地通过Log打印一下。
我们怎么来测试呢?我们可以通过使用模拟器的切换屏幕方向功能来测试。
因为切换屏幕方向会导致activity的摧毁和重建。
从上面的Log日志输出可以看出,MainActivity在第一次创建时,onCreate()方法的参数savedInstanceState是null;当我们切换屏幕方向,触发了销毁MainActivity的情况,但是在执行onStop()和onDestroy()方法前,系统自动调用了onSaveInstanceState()方法,我们在这个方法中保存了数据;在重新创建MainActivity时,onCreate()方法中的savedInstanceState不再为null,这里面保存了之前的数据,我们在这里将其取出输出到Log查看。
2、活动的启动模式
在实际项目中,我们有时需要根据特定的需求为每个活动指定恰当的启动模式。启动模式一共有4种,分别是standard(标准)、singleTop(栈顶复用)、singleTask(栈内复用)和singleInstance(单实例),可以在AndroidManifest.xml中通过给<activity>标签指定android:launchMode属性来选择启动模式。
(1)standard(标准)
standard是活动默认的启动模式,在不进行显式指定的情况下,所有活动都会自动使用这种启动模式。Android是使用返回栈来管理活动的,在standard模式(即默认情况)下,每当启动一个新的活动,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。
我们在之前的ActivityTest项目基础上来实战一下。首先关闭ActivityLifeCycleTest项目,打开ActivityTest项目。修改FirstActivity中onCreate()方法的代码,并且重写onRestart()和onDestroy()两个方法,如下所示:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("FirstActivity", "onCreate: " + this.toString()); setContentView(R.layout.activity_first); Button btn1 = (Button) findViewById(R.id.btn1); btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(FirstActivity.this, FirstActivity.class); startActivity(intent); } }); } @Override protected void onRestart() { super.onRestart(); Log.d("FirstActivity", "onRestart: " + this.toString()); } @Override protected void onDestroy() { super.onDestroy(); Log.d("FirstActivity", "onDestroy: " + this.toString()); }
代码看起来比较奇怪,在FirstActivity的基础上启动FirstActivity。
从逻辑上来讲这没什么意义,不过我们的重点在于研究standard模式,先不必在意这段代码有什么实际用途。
我们在onCreate()、onRestart()和onDestroy()方法中都添加了一行Log打印信息,用于打印当前活动的实例。
重新运行程序,观察LogCat中的信息,在FirstActivity界面连续点击两次按钮,可以看到每点击一次按钮就会创建出一个新的FirstActivity实例。
此时返回栈中也会存在三个FirstActivity的实例,因此你需要连按三次Back键才能退出程序,每次按Back键,都会重新启动之前的活动,结束当前的活动,如图所示。
standard模式的原理示意图,如图所示。
(2)singleTop(栈顶复用)
可能在有些情况下,你会觉得standard模式不太合理。活动明明已经在栈顶了,为什么再次启动的时候还要创建一个新的活动实例呢?别着急,这只是系统默认的一种启动模式而已,你完全可以根据自己的需要进行修改,比如说使用singleTop模式。当活动的启动模式指定为singleTop,在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。
我们还是通过实践来体会一下,修改AndroidManifest.xml中FirstActivity的启动模式,如下所示:
<activity android:name=".FirstActivity" android:launchMode="singleTop" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
然后重新运行程序,查看LogCat会看到已经创建了一个FirstActivity的实例,但是之后不管你点击多少次按钮都不会再有新的打印信息出现,因为目前FirstActivity已经处于返回栈的栈顶,每当想要再启动一个FirstActivity时都会直接使用栈顶的活动,因此FirstActivity也只会有一个实例,仅按一次Back键就可以退出程序,如图所示。
不过当FirstActivity并未处于栈顶位置时,这时再启动FirstActivity,还是会创建新的实例的。
下面我们来实验一下,修改FirstActivity中onCreate()方法的代码,如下所示:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("FirstActivity", "onCreate: " + this.toString()); setContentView(R.layout.activity_first); Button btn1 = (Button) findViewById(R.id.btn1); btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(FirstActivity.this, SecondActivity.class); startActivity(intent); } }); }
这次我们点击按钮后启动的是SecondActivity。
然后修改SecondActivity中onCreate()方法的代码,并且重写onRestart()和onDestroy()两个方法,如下所示:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("SecondActivity", "onCreate: " + this.toString()); setContentView(R.layout.activity_second); Button btn2 = (Button) findViewById(R.id.btn2); btn2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(SecondActivity.this, FirstActivity.class); startActivity(intent); } }); } @Override protected void onRestart() { super.onRestart(); Log.d("SecondActivity", "onRestart: " + this.toString()); } @Override protected void onDestroy() { super.onDestroy(); Log.d("SecondActivity", "onDestroy: " + this.toString()); }
我们在SecondActivity中的按钮点击事件里又加入了启动FirstActivity的代码。现在重新运行程序,在FirstActivity界面点击按钮进入到SecondActivity,然后在SecondActivity界面点击按钮,又会重新进入到FirstActivity。设置Log过滤器,如图所示。
查看LogCat中的打印信息,可以看到系统创建了两个不同的FirstActivity实例,这是由于在SecondActivity中再次启动FirstActivity时,栈顶活动已经变成了SecondActivity,因此会创建一个新的FirstActivity实例。现在按下Back键会返回到SecondActivity,再次按下Back键又会回到FirstActivity,再按一次Back键才会退出程序,如图所示。
singleTop模式的原理示意图,如图所示。
(3)singleTask(栈内复用)
使用singleTop模式可以很好地解决重复创建栈顶活动的问题,但是如果该活动并没有处于栈顶的位置,还是可能会创建多个活动实例的。
那么有没有什么办法可以让某个活动在整个应用程序的上下文中只存在一个实例呢?这就要借助singleTask模式来实现了。
当活动的启动模式指定为singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。
只修改AndroidManifest.xml中FirstActivity的启动模式:
<activity android:name=".FirstActivity" android:launchMode="singleTask"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
现在重新运行程序,在FirstActivity界面点击按钮进入到SecondActivity,然后在SecondActivity界面点击按钮,又会重新进入到FirstActivity。
查看LogCat中的打印信息,我们发现在SecondActivity中启动FirstActivity时,会发现返回栈中已经存在一个FirstActivity的实例,并且是在SecondActivity的下面,于是SecondActivity会从返回栈中出栈,而FirstActivity重新成为了栈顶活动,因此FirstActivity的onRestart()方法和SecondActivity的onDestroy()方法会得到执行。现在返回栈中应该只剩下一个FirstActivity的实例了,按一下Back键就可以退出程序,FirstAcitivity的onDestroy()方法得到执行。如图所示。
singleTask模式的原理示意图,如图所示。
(4)singleInstance(单实例)
singleInstance模式是四种启动模式中最特殊、最复杂的一个,不同于以上三种启动模式,指定为singleInstance模式的活动会启用一个新的返回栈来管理这个活动。
那么这样做有什么意义呢?想象以下场景,假设我们的程序中有一个活动是允许其他程序调用的,如果我们想实现其他程序和我们的程序可以共享这个活动的实例,应该如何实现呢?使用前面三种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个活动在不同的返回栈中入栈时必然是创建了新的实例。
而使用singleInstance模式就可以解决这个问题,在这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都共用的同一个返回栈,也就解决了共享活动实例的问题。
修改AndroidManifest.xml中SecondActivity的启动模式:
<activity android:name=".SecondActivity" android:launchMode="singleInstance" > …… </activity>
我们先将SecondActivity的启动模式指定为singleInstance,然后修改FirstActivity中onCreate()方法的代码:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("FirstActivity", "onCreate: FirstActivity task id is " + getTaskId()); Log.d("FirstActivity", "onCreate: " + this.toString()); setContentView(R.layout.activity_first); Button btn1 = (Button) findViewById(R.id.btn1); btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(FirstActivity.this, SecondActivity.class); startActivity(intent); } }); }
在onCreate()方法中添加了打印当前返回栈的id。
然后修改SecondActivity中onCreate()方法:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("SecondActivity", "onCreate: SecondActivity task id is " + getTaskId()); Log.d("SecondActivity", "onCreate: " + this.toString()); setContentView(R.layout.activity_second); Button btn2 = (Button) findViewById(R.id.btn2); btn2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(SecondActivity.this, ThirdActivity.class); startActivity(intent); } }); }
同样在onCreate()方法中也添加了打印当前返回栈的id,然后又修改了按钮点击事件的代码,用于启动ThirdActivity。
最后修改ThirdActivity中onCreate()方法,并且重写了onDestroy()方法:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("ThirdActivity", "onCreate: ThirdActivity task id is " + getTaskId()); Log.d("ThirdActivity", "onCreate: " + this.toString()); setContentView(R.layout.activity_third); } @Override protected void onDestroy() { super.onDestroy(); Log.d("ThirdActivity", "onDestroy: " + this.toString()); }
仍然是在onCreate()方法中打印了当前返回栈的id和this对象,在onDestroy()中打印this对象。现在重新运行程序,在FirstActivity界面点击按钮进入到SecondActivity,然后在SecondActivity界面点击按钮进入到ThirdActivity。
查看LogCat中的打印信息,如图所示。
可以看到,FirstActivity和ThirdActivity的Task id相同,SecondActivity的Task id不同于FirstActivity和ThirdActivity的,这说明SecondActivity确实是存放在一个单独的返回栈里的,而且这个栈中只有SecondActivity这一个活动。
然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?
其实原理很简单,由于FirstActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶活动显示在界面上,因此也就出现了从ThirdActivity直接返回到FirstActivity的情况。
然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶活动,即SecondActivity。
最后再次按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。
singleInstance模式的原理示意图,如图所示。