首先我们要知道这个实际应用场景,
最特别的就是手机横竖屏切换,换了之后,屏幕的宽高比就变了,调用各个资源就需要重新来;还有如语言的切换
参考文章:http://blog.csdn.net/lonelyroamer/article/details/19084173
当这样的变化发生时,Android会重新启动这个正在运行的Activity(onDestroy()方法会被调用,然后调用onCreate()方法)。这是系统自己调用,不是人为
这个行为设计意图是为了帮助应用程序适应新的配置,他会自动的用你提供的适当的资源(比如对应不同屏幕方向和尺寸的layout资源)重新加载应用程序。
为了正确执行一次重启,你的Activity在整个普通的生命周期中重新保存它之前的状态是很重要的,
Android是通过在销毁你的Activity之前调用onSaveInstanceState()方法来保存关于应用之前状态的数据。
然后你就可以在onCreate(Bundle outState)方法或者onRestoreInstanceState()方法中重新恢复应用的状态了。
之前学过,当你的应用程序在后台中,被系统销毁前,你可以通过onSaveInstanceState()方法保存状态,然后在恢复。
这种方式同样适合现在所学习的运行时配置的改变。
在大多数情况下,onSaveInstanceState()方法和onRestoreInstanceState()方法配合使用,能够解决大多数状态保存和恢复的问题。
但是面对如下情景:
保存大量数据并在Activity重启时恢复。在这种情况下,如果还是使用onSaveInstanceState()和onRestoreInstanceState(),
数据的大量保存和恢复非常耗时不说,而且有些数据也不一定容易放到Bundle中,例如bitmap,即使序列化了,估计有时候也不好。
遇到这种情况,你有两个选择:
1、在配置改变期间维持一个对象
当配置发生改变时允许你的Activity重启,但让其携带一个有状态的对象到你的新Activity实例中。【Handless Fragment也叫做无UI Fragment】
2、你自己来处理配置的变化
当某些配置发生变化的时候阻止系统重启你的Activity,并且当配置改变时要接收一个回调,这样你就可以根据需要来手动更新你的Activity。
在配置改变期间维持一个对象
带有AsyncTask的运行时配置变化
如果重启你的Activity,需要恢复大量的数据,重新执行网络连接,或者其他深入的操作,这样由配置改变引起的一次完全启动就会引起不好的用户体验。
而且,仅有Activity生命周期中为你保存的的Bundle对象,你是不可能完全维护你的Activity的状态的—不能传递很大的对象(如bitmap对象),
并且,这些对象里面的数据必须序列化,然后解序列化,这些都需要消耗很多内存从而使配置改变得很慢。慢,用户体验就不好了
在这样的情境下,当你的Activity由于配置发生改变而重启时,你可以通过重新预置一个有状态的对象来减缓你程序的负担。
在运行期间配置改变时维护一个对象:
1. 重写 onRetainNonConfigurationInstance() 方法来返回你想要维护的对象
2. 当你的Activity再次创建时,调用getLastNonConfigurationInstance()方法恢复你的对象
当你的Activity由于配置发生改变要关闭的时候,
Android会在执行onStop()方法与onDestroy()方法之间调用onRetainNonConfigurationInstance()方法。
为了在配置改变后更有效地保存状态,在实现onRetainNonConfigurationInstance() 方法时你应该返回你所需要的一个对象。
这个场景的可贵之处在于当你的应用程序需要从网上下载很多数据的时候,
如果用户更改设备的方向并且Activity重启,你的应用程序必须要重新载入数据,那就会很慢了。
你需要做的就是实现onRetainNonConfigurationInstance() 方法并返回带有你的数据的对象,
然后当你的 Activity通过getLastNonConfigurationInstance()方法重启时就能获取数据。
例如:
- @Override
- public Object onRetainNonConfigurationInstance() {
- final MyDataObject data = collectMyLoadedData();
- return data;
- }
特别提醒:
当你要返回任何对象的时候,你应该不要传递一个跟Activity有关联的对象,例如一个Drawable对象,一个Adapter对象,一个View对象或者任何其他跟Context相关的对象 。如果你这样做,它会泄漏原来Activity实例的所有视图和资源。(泄漏资源意味着您的应用程序保持对他们的持有,他们不能被当做垃圾收集,因此内存就丢失了)
然后当你的Activity重启时获取数据:
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- final MyDataObject data = (MyDataObject) getLastNonConfigurationInstance();
- if (data == null) {
- data = loadMyData();
- }
- ...
- }
这个例子中,getLastNonConfigurationInstance()获取了onRetainNonConfigurationInstance()方法中保存的数据。
如果数据为空,这种情况发生在,当Activity重启是由其他原因而不是配置改变引起的,那么程序将从原来的数据源载入数据对象 。
上面只是onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()的最简单的用法,这个方法有很多的好处:
* 当activity曾经通过某个网络资源得到一些图片或者视频信息,那么当再次恢复后,无需重新通过原始资源地址获取,可以快速的加载整个activity状态信息。
* 当activity包含有许多线程时,在变化后依然可以持有原有线程,无需通过重新创建进程恢复原有状态。
* 当activity包含某些connection实例时,同样可以在整个变化过程中保持连接状态。
下面就是一个关于AsyncTask的例子:
- import android.app.Activity;
- import android.os.AsyncTask;
- import android.os.Bundle;
- import android.os.SystemClock;
- import android.util.Log;
- import android.view.View;
- import android.widget.ProgressBar;
- /**
- * Android实现屏幕旋转异步下载效果
- * @Description: Android实现屏幕旋转异步下载效果
- */
- public class RotationAsyncActivity extends Activity {
- // 进度条
- private ProgressBar progressBar=null;
- // 异步任务类
- private RotationAsyncTask asyncTask=null;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- progressBar=(ProgressBar)findViewById(R.id.progress);
- // 获取对象
- asyncTask=(RotationAsyncTask)getLastNonConfigurationInstance();
- if (asyncTask==null) {
- asyncTask=new RotationAsyncTask(this);
- asyncTask.execute();
- }else {
- asyncTask.attach(this);
- updateProgress(asyncTask.getProgress());
- if (asyncTask.getProgress()>=100) {
- markAsDone();
- }
- }
- }
- /**
- * 保存对象
- */
- @Override
- public Object onRetainNonConfigurationInstance() {
- asyncTask.detach();
- return asyncTask;
- }
- private void updateProgress(int progress) {
- progressBar.setProgress(progress);
- }
- private void markAsDone() {
- findViewById(R.id.completed).setVisibility(View.VISIBLE);
- }
- // 异步任务类
- private static class RotationAsyncTask extends AsyncTask<Void, Void, Void> {
- private RotationAsyncActivity activity=null;
- private int progress=0;
- /**
- * 默认的构造器
- */
- public RotationAsyncTask() {
- // TODO Auto-generated constructor stub
- }
- /**
- * 带参构造器
- * @param activity
- */
- public RotationAsyncTask(RotationAsyncActivity activity) {
- attach(activity);
- }
- @Override
- protected Void doInBackground(Void... unused) {
- for (int i=0;i<20;i++) {
- SystemClock.sleep(500);
- publishProgress();
- }
- return null;
- }
- @Override
- protected void onProgressUpdate(Void... unused) {
- if (activity==null) {
- Log.w("RotationAsyncActivity", "onProgressUpdate()");
- }else {
- progress += 5;
- activity.updateProgress(progress);
- }
- }
- @Override
- protected void onPostExecute(Void unused) {
- if (activity==null) {
- Log.w("RotationAsyncActivity", "onPostExecute()");
- }else {
- activity.markAsDone();
- }
- }
- protected void detach() {
- activity = null;
- }
- protected void attach(RotationAsyncActivity activity) {
- this.activity = activity;
- }
- protected int getProgress() {
- return progress;
- }
- }
- }
注意:onSaveInstanceState()是在未经用户允许的情况下,系统销毁Activity之前调用的,而onRetainNonConfigurationInstance()是在因运行时配置改变引起系统销毁Activity之前调用的。
所以onSaveInstanceState()调用,onRetainNonConfigurationInstance()不一定会调用。onRetainNonConfigurationInstance()调用了,必定调用了onSaveInstanceState()。
自己来处理配置的改变
如果在某个特殊的配置发生改变的期间你的应用程序不需要更新资源,而且你有个操作限制需要你避免Activity的重启,
那么你可以声明使你自己的Activity来处理配置的变化,从而阻止系统重启你的Activity。
特别提醒: 选择自己来处理配置的变化会使得可替代资源的使用变得更困难,因为系统不会为你来自动调用这些资源。这种技术应该被视为避免Activity重启的最后手段,对于大多数应用程序不建议使用。
为了声明你的Activity来处理配置的变化,在manifest文件中编辑正确的<activity>元素,包括赋好值的android:configChanges属性,代表你要处理的配置。android:configChanges属性所有可能的值都要在文档android:configChanges 中列出(最常用的值是:orientation来处理当屏幕的方向变化时,keyboardHidden来处理键盘可用性改变时)。你可以在属性中声明多个配置的值,通过“|”符号将它们分隔开。
例如,以下清单片段声明了Activity中将同时处理屏幕的方向变化和键盘的可用性变化:
- <activity android:name=".MyActivity"
- android:configChanges="orientation|keyboardHidden"
- android:label="@string/app_name">
现在,当这些配置中的一个发生改变时,MyActivity不会重新启动。相反,这个 Activity会接收onConfigurationChanged()方法的调用。
这个方法传递一个Configuration类的对象来标识新的设备配置。
通过读取配置字段,你可以确定新的配置信息并通过更新你界面中使用的资源来正确应用这些改变。
任何时候这个方法被调用,你的Activity的Resources对象会被更新并返回一个基于新配置的Resources对象,
因此你可以在不用系统重启你的Activity的情况下很容易地重置你的UI元素。
特别提醒: 从Android 3.2(API level 13)开始,当设备的横竖屏切换时,屏幕尺寸也会发生改变。所以,如果你想在API level 13或更高(由minSdkVersion和targetSdkVersion声明)上阻止运行时因屏幕方向引发的重启,你必须在声明"orientation" 时额外声明"screenSize" 值。
如下:android:configChanges=“orientation|screenSize”。
然而,如果你的应用程序目标API level 12或者更低,那么你的Activity同样会自己处理这个配置的改变
(这个配置改变不会重启你的Activity,即使运行在Android3.2或者更高的设置上)
例如,接下来的onConfigurationChanged()方法中实现了检查当前设备的方向:
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- 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();
- }
- }
这个Configuration类的对象代表着当前所有的配置信息,不仅仅那些改变的配置信息。
在很多时候,你不会确切地在乎这些配置是怎么改变的,并且可以简单地重新分配所有资源,提供您正在处理的配置的可替代资源。
例如,因为这个Resources对象现在被更新,你可以通过setImageResource(int)方法重置任何ImageView,并重置恰当的资源给当前配置使用。(详见Providing Resources)
请注意,配置字段的值是一些匹配 Configuration类里特定的常量的整数。
对于文档中的每个字段使用那个常量,请在Configuration类中参阅相应的字段。
记住:当你声明你的Activity来处理配置的变化时,你必须负责重置所有你提供可替代资源的元素 。
如果你声明你的Activity来处理屏幕方向的改变并具有在横向和纵向之间切换的图像,
你必须在onConfigurationChanged()方法中给每个元素重新指定一个资源。
如果你不需要根据配置的变化来更新你的程序,你可以不实现onConfigurationChanged()方法。
在这种情况下,所有在配置改变之前使用的资源仍然会被使用,并且你只需要避免你的Activity被重启。
然而,您的应用程序应该始终能够关闭并从其之前的状态完好地重新启动。
这不仅是因为存在有一些配置发生改变时你不能防止它重新启动您的应用程序,而且为了处理一些事件,例如当用户接收了电话然后返回到应用程序。
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- mImageView.setImageResource(0);
- mImageView.setImageResource(R.drawable.temp);
- }
这是因为虽然Resource更新了,但是R.drawable.temp的int值是没有变的,而setImageResource的逻辑判断会忽略掉资源Id没有改变的设置。所以,在更新资源的时候注意是否会有类似的情况发生。
转载请表明出处:http://blog.csdn.net/lmj623565791/article/details/37936275
1、概述
众所周知,Activity在不明确指定屏幕方向和configChanges时,当用户旋转屏幕会重新启动。当然了,应对这种情况,Android给出了几种方案:
a、如果是少量数据,可以通过onSaveInstanceState()和onRestoreInstanceState()进行保存与恢复。
Android会在销毁你的Activity之前调用onSaveInstanceState()方法,于是,你可以在此方法中存储关于应用状态的数据。然后你可以在onCreate()或onRestoreInstanceState()方法中恢复。
b、如果是大量数据,使用Fragment保持需要恢复的对象。
c、自已处理配置变化。
注:getLastNonConfigurationInstance()已经被弃用,被上述方法二替代。
2、难点
假设当前Activity在onCreate中启动一个异步线程去夹在数据,当然为了给用户一个很好的体验,会有一个ProgressDialog,当数据加载完成,ProgressDialog消失,设置数据。
这里,如果在异步数据完成加载之后,旋转屏幕,使用上述a、b两种方法都不会很难,无非是保存数据和恢复数据。
但是,如果正在线程加载的时候,进行旋转,会存在以下问题:
a)此时数据没有完成加载,onCreate重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。
b)关闭ProgressDialog的代码在线程的onPostExecutez中,但是上个线程如果已经杀死,无法关闭之前ProgressDialog。
c)谷歌的官方不建议使用ProgressDialog,这里我们会使用官方推荐的DialogFragment来创建我的加载框,如果你不了解:请看 Android 官方推荐 : DialogFragment 创建对话框。这样,其实给我们带来一个很大的问题,DialogFragment说白了是Fragment,和当前的Activity的生命周期会发生绑定,我们旋转屏幕会造成Activity的销毁,当然也会对DialogFragment造成影响。
下面我将使用几个例子,分别使用上面的3种方式,和如何最好的解决上述的问题。
3、使用onSaveInstanceState()和onRestoreInstanceState()进行数据保存与恢复
代码:
界面为一个ListView,onCreate中启动一个异步任务去加载数据,这里使用Thread.sleep模拟了一个耗时操作;当用户旋转屏幕发生重新启动时,会onSaveInstanceState中进行数据的存储,在onCreate中对数据进行恢复,免去了不必要的再加载一遍。
运行结果:
当正常加载数据完成之后,用户不断进行旋转屏幕,log会不断打出:onSaveInstanceState->onDestroy->onCreate->onRestoreInstanceState,验证我们的确是重新启动了,但是我们没有再次去进行数据加载。
如果在加载的时候,进行旋转,则会发生错误,异常退出(退出原因:dialog.dismiss()时发生NullPointException,因为与当前对话框绑定的FragmentManager为null,又有兴趣的可以去Debug,这个不是关键)。
效果图:
4、使用Fragment来保存对象,用于恢复数据
如果重新启动你的Activity需要恢复大量的数据,重新建立网络连接,或者执行其他的密集型操作,这样因为配置发生变化而完全重新启动可能会是一个慢的用户体验。并且,使用系统提供的onSaveIntanceState()的回调中,使用Bundle来完全恢复你Activity的状态是可能是不现实的(Bundle不是设计用来携带大量数据的(例如bitmap),并且Bundle中的数据必须能够被序列化和反序列化),这样会消耗大量的内存和导致配置变化缓慢。在这样的情况下,当你的Activity因为配置发生改变而重启,你可以通过保持一个Fragment来缓解重新启动带来的负担。这个Fragment可以包含你想要保持的有状态的对象的引用。
当Android系统因为配置变化关闭你的Activity的时候,你的Activity中被标识保持的fragments不会被销毁。你可以在你的Activity中添加这样的fragements来保存有状态的对象。
在运行时配置发生变化时,在Fragment中保存有状态的对象
a) 继承Fragment,声明引用指向你的有状态的对象
b) 当Fragment创建时调用setRetainInstance(boolean)
c) 把Fragment实例添加到Activity中
d) 当Activity重新启动后,使用FragmentManager对Fragment进行恢复
代码:
首先是Fragment:
比较简单,只需要声明需要保存的数据对象,然后提供getter和setter,注意,一定要在onCreate调用setRetainInstance(true);
然后是:FragmentRetainDataActivity
这里在onCreate总使用了Volley去加载 了一张美女照片,然后在onDestroy中对Bitmap进行存储,在onCreate添加一个或者恢复一个Fragment的引用,然后对Bitmap进行读取和设置。这种方式适用于比较大的数据的存储与恢复。
注:这里也没有考虑加载时旋转屏幕,问题与上面的一致。
效果图:
5、配置configChanges,自己对屏幕旋转的变化进行处理
在menifest中进行属性设置:
低版本的API只需要加入orientation,而高版本的则需要加入screenSize。
ConfigChangesTestActivity
对第一种方式的代码进行了修改,去掉了保存与恢复的代码,重写了onConfigurationChanged;此时,无论用户何时旋转屏幕都不会重新启动Activity,并且onConfigurationChanged中的代码可以得到调用。从效果图可以看到,无论如何旋转不会重启Activity.
效果图:
6、旋转屏幕的最佳实践
下面要开始今天的难点了,就是处理文章开始时所说的,当异步任务在执行时,进行旋转,如果解决上面的问题。
首先说一下探索过程:
起初,我认为此时旋转无非是再启动一次线程,并不会造成异常,我只要即使的在onDestroy里面关闭上一个异步任务就可以了。事实上,如果我关闭了,上一次的对话框会一直存在;如果我不关闭,但是activity是一定会被销毁的,对话框的dismiss也会出异常。真心很蛋疼,并且即使对话框关闭了,任务关闭了;用户旋转还是会造成重新创建任务,从头开始加载数据。
下面我们希望有一种解决方案:在加载数据时旋转屏幕,不会对加载任务进行中断,且对用户而言,等待框在加载完成之前都正常显示:
当然我们还使用Fragment进行数据保存,毕竟这是官方推荐的:
OtherRetainedFragment
和上面的差别不大,唯一不同的就是它要保存的对象编程一个异步的任务了,相信看到这,已经知道经常上述问题的一个核心了,保存一个异步任务,在重启时,继续这个任务。
异步任务中,管理一个对话框,当开始下载前,进度框显示,下载结束进度框消失,并为Activity提供回调。当然了,运行过程中Activity不断的重启,我们也提供了setActivity方法,onDestory时,会setActivity(null)防止内存泄漏,同时我们也会关闭与其绑定的加载框;当onCreate传入新的Activity时,我们会在再次打开一个加载框,当然了因为屏幕的旋转并不影响加载的数据,所有后台的数据一直继续在加载。是不是很完美~~
主Activity:
在onCreate中,如果没有开启任务(第一次进入),开启任务;如果已经开启了,调用setActivity(this);
在onSaveInstanceState把当前任务加入Fragment
我设置了等待5秒,足够旋转三四个来回了~~~~可以看到虽然在不断的重启,但是丝毫不影响加载数据任务的运行和加载框的显示~~~~
效果图:
可以看到我在加载的时候就三心病狂的旋转屏幕~~但是丝毫不影响显示效果与任务的加载~~
最后,说明一下,其实不仅是屏幕旋转需要保存数据,当用户在使用你的app时,忽然接到一个来电,长时间没有回到你的app界面也会造成Activity的销毁与重建,所以一个行为良好的App,是有必要拥有恢复数据的能力的~~。
查阅资料时的一些参考文档:
http://developer.android.com/guide/topics/resources/runtime-changes.html
http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/