Android(安卓)屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案
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()进行数据保存与恢复
代码:
[java] view plain copy
- packagecom.example.zhy_handle_runtime_change;
- importjava.util.ArrayList;
- importjava.util.Arrays;
- importandroid.app.DialogFragment;
- importandroid.app.ListActivity;
- importandroid.os.AsyncTask;
- importandroid.os.Bundle;
- importandroid.util.Log;
- importandroid.widget.ArrayAdapter;
- importandroid.widget.ListAdapter;
- /**
- *不考虑加载时,进行旋转的情况,有意的避开这种情况,后面例子会介绍解决方案
- *@authorzhy
- *
- */
- publicclassSavedInstanceStateUsingActivityextendsListActivity
- {
- privatestaticfinalStringTAG="MainActivity";
- privateListAdaptermAdapter;
- privateArrayList<String>mDatas;
- privateDialogFragmentmLoadingDialog;
- privateLoadDataAsyncTaskmLoadDataAsyncTask;
- @Override
- publicvoidonCreate(BundlesavedInstanceState)
- {
- super.onCreate(savedInstanceState);
- Log.e(TAG,"onCreate");
- initData(savedInstanceState);
- }
- /**
- *初始化数据
- */
- privatevoidinitData(BundlesavedInstanceState)
- {
- if(savedInstanceState!=null)
- mDatas=savedInstanceState.getStringArrayList("mDatas");
- if(mDatas==null)
- {
- mLoadingDialog=newLoadingDialog();
- mLoadingDialog.show(getFragmentManager(),"LoadingDialog");
- mLoadDataAsyncTask=newLoadDataAsyncTask();
- mLoadDataAsyncTask.execute();
- }else
- {
- initAdapter();
- }
- }
- /**
- *初始化适配器
- */
- privatevoidinitAdapter()
- {
- mAdapter=newArrayAdapter<String>(
- SavedInstanceStateUsingActivity.this,
- android.R.layout.simple_list_item_1,mDatas);
- setListAdapter(mAdapter);
- }
- @Override
- protectedvoidonRestoreInstanceState(Bundlestate)
- {
- super.onRestoreInstanceState(state);
- Log.e(TAG,"onRestoreInstanceState");
- }
- @Override
- protectedvoidonSaveInstanceState(BundleoutState)
- {
- super.onSaveInstanceState(outState);
- Log.e(TAG,"onSaveInstanceState");
- outState.putSerializable("mDatas",mDatas);
- }
- /**
- *模拟耗时操作
- *
- *@return
- */
- privateArrayList<String>generateTimeConsumingDatas()
- {
- try
- {
- Thread.sleep(2000);
- }catch(InterruptedExceptione)
- {
- }
- returnnewArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
- "onSaveInstanceState保存数据",
- "getLastNonConfigurationInstance已经被弃用","RabbitMQ","Hadoop",
- "Spark"));
- }
- privateclassLoadDataAsyncTaskextendsAsyncTask<Void,Void,Void>
- {
- @Override
- protectedVoiddoInBackground(Void...params)
- {
- mDatas=generateTimeConsumingDatas();
- returnnull;
- }
- @Override
- protectedvoidonPostExecute(Voidresult)
- {
- mLoadingDialog.dismiss();
- initAdapter();
- }
- }
- @Override
- protectedvoidonDestroy()
- {
- Log.e(TAG,"onDestroy");
- super.onDestroy();
- }
- }
界面为一个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:
[java] view plain copy
- packagecom.example.zhy_handle_runtime_change;
- importandroid.app.Fragment;
- importandroid.graphics.Bitmap;
- importandroid.os.Bundle;
- publicclassRetainedFragmentextendsFragment
- {
- //dataobjectwewanttoretain
- privateBitmapdata;
- //thismethodisonlycalledonceforthisfragment
- @Override
- publicvoidonCreate(BundlesavedInstanceState)
- {
- super.onCreate(savedInstanceState);
- //retainthisfragment
- setRetainInstance(true);
- }
- publicvoidsetData(Bitmapdata)
- {
- this.data=data;
- }
- publicBitmapgetData()
- {
- returndata;
- }
- }
比较简单,只需要声明需要保存的数据对象,然后提供getter和setter,注意,一定要在onCreate调用setRetainInstance(true);
然后是:FragmentRetainDataActivity
[java] view plain copy
- packagecom.example.zhy_handle_runtime_change;
- importandroid.app.Activity;
- importandroid.app.DialogFragment;
- importandroid.app.FragmentManager;
- importandroid.graphics.Bitmap;
- importandroid.graphics.Bitmap.Config;
- importandroid.os.Bundle;
- importandroid.util.Log;
- importandroid.widget.ImageView;
- importcom.android.volley.RequestQueue;
- importcom.android.volley.Response;
- importcom.android.volley.toolbox.ImageRequest;
- importcom.android.volley.toolbox.Volley;
- publicclassFragmentRetainDataActivityextendsActivity
- {
- privatestaticfinalStringTAG="FragmentRetainDataActivity";
- privateRetainedFragmentdataFragment;
- privateDialogFragmentmLoadingDialog;
- privateImageViewmImageView;
- privateBitmapmBitmap;
- @Override
- publicvoidonCreate(BundlesavedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- Log.e(TAG,"onCreate");
- //findtheretainedfragmentonactivityrestarts
- FragmentManagerfm=getFragmentManager();
- dataFragment=(RetainedFragment)fm.findFragmentByTag("data");
- //createthefragmentanddatathefirsttime
- if(dataFragment==null)
- {
- //addthefragment
- dataFragment=newRetainedFragment();
- fm.beginTransaction().add(dataFragment,"data").commit();
- }
- mBitmap=collectMyLoadedData();
- initData();
- //thedataisavailableindataFragment.getData()
- }
- /**
- *初始化数据
- */
- privatevoidinitData()
- {
- mImageView=(ImageView)findViewById(R.id.id_imageView);
- if(mBitmap==null)
- {
- mLoadingDialog=newLoadingDialog();
- mLoadingDialog.show(getFragmentManager(),"LOADING_DIALOG");
- RequestQueuenewRequestQueue=Volley
- .newRequestQueue(FragmentRetainDataActivity.this);
- ImageRequestimageRequest=newImageRequest(
- "http://img.my.csdn.net/uploads/201407/18/1405652589_5125.jpg",
- newResponse.Listener<Bitmap>()
- {
- @Override
- publicvoidonResponse(Bitmapresponse)
- {
- mBitmap=response;
- mImageView.setImageBitmap(mBitmap);
- //loadthedatafromtheweb
- dataFragment.setData(mBitmap);
- mLoadingDialog.dismiss();
- }
- },0,0,Config.RGB_565,null);
- newRequestQueue.add(imageRequest);
- }else
- {
- mImageView.setImageBitmap(mBitmap);
- }
- }
- @Override
- publicvoidonDestroy()
- {
- Log.e(TAG,"onDestroy");
- super.onDestroy();
- //storethedatainthefragment
- dataFragment.setData(mBitmap);
- }
- privateBitmapcollectMyLoadedData()
- {
- returndataFragment.getData();
- }
- }
这里在onCreate总使用了Volley去加载 了一张美女照片,然后在onDestroy中对Bitmap进行存储,在onCreate添加一个或者恢复一个Fragment的引用,然后对Bitmap进行读取和设置。这种方式适用于比较大的数据的存储与恢复。
注:这里也没有考虑加载时旋转屏幕,问题与上面的一致。
效果图:
5、配置configChanges,自己对屏幕旋转的变化进行处理
在menifest中进行属性设置:
[html] view plain copy
- <activity
- android:name=".ConfigChangesTestActivity"
- android:configChanges="screenSize|orientation">
- </activity>
ConfigChangesTestActivity
[java] view plain copy
- packagecom.example.zhy_handle_runtime_change;
- importjava.util.ArrayList;
- importjava.util.Arrays;
- importandroid.app.DialogFragment;
- importandroid.app.ListActivity;
- importandroid.content.res.Configuration;
- importandroid.os.AsyncTask;
- importandroid.os.Bundle;
- importandroid.util.Log;
- importandroid.widget.ArrayAdapter;
- importandroid.widget.ListAdapter;
- importandroid.widget.Toast;
- /**
- *@authorzhy
- *
- */
- publicclassConfigChangesTestActivityextendsListActivity
- {
- privatestaticfinalStringTAG="MainActivity";
- privateListAdaptermAdapter;
- privateArrayList<String>mDatas;
- privateDialogFragmentmLoadingDialog;
- privateLoadDataAsyncTaskmLoadDataAsyncTask;
- @Override
- publicvoidonCreate(BundlesavedInstanceState)
- {
- super.onCreate(savedInstanceState);
- Log.e(TAG,"onCreate");
- initData(savedInstanceState);
- }
- /**
- *初始化数据
- */
- privatevoidinitData(BundlesavedInstanceState)
- {
- mLoadingDialog=newLoadingDialog();
- mLoadingDialog.show(getFragmentManager(),"LoadingDialog");
- mLoadDataAsyncTask=newLoadDataAsyncTask();
- mLoadDataAsyncTask.execute();
- }
- /**
- *初始化适配器
- */
- privatevoidinitAdapter()
- {
- mAdapter=newArrayAdapter<String>(ConfigChangesTestActivity.this,
- android.R.layout.simple_list_item_1,mDatas);
- setListAdapter(mAdapter);
- }
- /**
- *模拟耗时操作
- *
- *@return
- */
- privateArrayList<String>generateTimeConsumingDatas()
- {
- try
- {
- Thread.sleep(2000);
- }catch(InterruptedExceptione)
- {
- }
- returnnewArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
- "onSaveInstanceState保存数据",
- "getLastNonConfigurationInstance已经被弃用","RabbitMQ","Hadoop",
- "Spark"));
- }
- /**
- *当配置发生变化时,不会重新启动Activity。但是会回调此方法,用户自行进行对屏幕旋转后进行处理
- */
- @Override
- publicvoidonConfigurationChanged(ConfigurationnewConfig)
- {
- super.onConfigurationChanged(newConfig);
- if(newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE)
- {
- Toast.makeText(this,"landscape",Toast.LENGTH_SHORT).show();
- }elseif(newConfig.orientation==Configuration.ORIENTATION_PORTRAIT)
- {
- Toast.makeText(this,"portrait",Toast.LENGTH_SHORT).show();
- }
- }
- privateclassLoadDataAsyncTaskextendsAsyncTask<Void,Void,Void>
- {
- @Override
- protectedVoiddoInBackground(Void...params)
- {
- mDatas=generateTimeConsumingDatas();
- returnnull;
- }
- @Override
- protectedvoidonPostExecute(Voidresult)
- {
- mLoadingDialog.dismiss();
- initAdapter();
- }
- }
- @Override
- protectedvoidonDestroy()
- {
- Log.e(TAG,"onDestroy");
- super.onDestroy();
- }
- }
对第一种方式的代码进行了修改,去掉了保存与恢复的代码,重写了onConfigurationChanged;此时,无论用户何时旋转屏幕都不会重新启动Activity,并且onConfigurationChanged中的代码可以得到调用。从效果图可以看到,无论如何旋转不会重启Activity.
效果图:
6、旋转屏幕的最佳实践
下面要开始今天的难点了,就是处理文章开始时所说的,当异步任务在执行时,进行旋转,如果解决上面的问题。
首先说一下探索过程:
起初,我认为此时旋转无非是再启动一次线程,并不会造成异常,我只要即使的在onDestroy里面关闭上一个异步任务就可以了。事实上,如果我关闭了,上一次的对话框会一直存在;如果我不关闭,但是activity是一定会被销毁的,对话框的dismiss也会出异常。真心很蛋疼,并且即使对话框关闭了,任务关闭了;用户旋转还是会造成重新创建任务,从头开始加载数据。
下面我们希望有一种解决方案:在加载数据时旋转屏幕,不会对加载任务进行中断,且对用户而言,等待框在加载完成之前都正常显示:
当然我们还使用Fragment进行数据保存,毕竟这是官方推荐的:
OtherRetainedFragment
[java] view plain copy
- packagecom.example.zhy_handle_runtime_change;
- importandroid.app.Fragment;
- importandroid.os.Bundle;
- /**
- *保存对象的Fragment
- *
- *@authorzhy
- *
- */
- publicclassOtherRetainedFragmentextendsFragment
- {
- //dataobjectwewanttoretain
- //保存一个异步的任务
- privateMyAsyncTaskdata;
- //thismethodisonlycalledonceforthisfragment
- @Override
- publicvoidonCreate(BundlesavedInstanceState)
- {
- super.onCreate(savedInstanceState);
- //retainthisfragment
- setRetainInstance(true);
- }
- publicvoidsetData(MyAsyncTaskdata)
- {
- this.data=data;
- }
- publicMyAsyncTaskgetData()
- {
- returndata;
- }
- }
和上面的差别不大,唯一不同的就是它要保存的对象编程一个异步的任务了,相信看到这,已经知道经常上述问题的一个核心了,保存一个异步任务,在重启时,继续这个任务。
[java] view plain copy
- packagecom.example.zhy_handle_runtime_change;
- importjava.util.ArrayList;
- importjava.util.Arrays;
- importjava.util.List;
- importandroid.os.AsyncTask;
- publicclassMyAsyncTaskextendsAsyncTask<Void,Void,Void>
- {
- privateFixProblemsActivityactivity;
- /**
- *是否完成
- */
- privatebooleanisCompleted;
- /**
- *进度框
- */
- privateLoadingDialogmLoadingDialog;
- privateList<String>items;
- publicMyAsyncTask(FixProblemsActivityactivity)
- {
- this.activity=activity;
- }
- /**
- *开始时,显示加载框
- */
- @Override
- protectedvoidonPreExecute()
- {
- mLoadingDialog=newLoadingDialog();
- mLoadingDialog.show(activity.getFragmentManager(),"LOADING");
- }
- /**
- *加载数据
- */
- @Override
- protectedVoiddoInBackground(Void...params)
- {
- items=loadingData();
- returnnull;
- }
- /**
- *加载完成回调当前的Activity
- */
- @Override
- protectedvoidonPostExecute(Voidunused)
- {
- isCompleted=true;
- notifyActivityTaskCompleted();
- if(mLoadingDialog!=null)
- mLoadingDialog.dismiss();
- }
- publicList<String>getItems()
- {
- returnitems;
- }
- privateList<String>loadingData()
- {
- try
- {
- Thread.sleep(5000);
- }catch(InterruptedExceptione)
- {
- }
- returnnewArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
- "onSaveInstanceState保存数据",
- "getLastNonConfigurationInstance已经被弃用","RabbitMQ","Hadoop",
- "Spark"));
- }
- /**
- *设置Activity,因为Activity会一直变化
- *
- *@paramactivity
- */
- publicvoidsetActivity(FixProblemsActivityactivity)
- {
- //如果上一个Activity销毁,将与上一个Activity绑定的DialogFragment销毁
- if(activity==null)
- {
- mLoadingDialog.dismiss();
- }
- //设置为当前的Activity
- this.activity=activity;
- //开启一个与当前Activity绑定的等待框
- if(activity!=null&&!isCompleted)
- {
- mLoadingDialog=newLoadingDialog();
- mLoadingDialog.show(activity.getFragmentManager(),"LOADING");
- }
- //如果完成,通知Activity
- if(isCompleted)
- {
- notifyActivityTaskCompleted();
- }
- }
- privatevoidnotifyActivityTaskCompleted()
- {
- if(null!=activity)
- {
- activity.onTaskCompleted();
- }
- }
- }
异步任务中,管理一个对话框,当开始下载前,进度框显示,下载结束进度框消失,并为Activity提供回调。当然了,运行过程中Activity不断的重启,我们也提供了setActivity方法,onDestory时,会setActivity(null)防止内存泄漏,同时我们也会关闭与其绑定的加载框;当onCreate传入新的Activity时,我们会在再次打开一个加载框,当然了因为屏幕的旋转并不影响加载的数据,所有后台的数据一直继续在加载。是不是很完美~~
主Activity:
[java] view plain copy
- packagecom.example.zhy_handle_runtime_change;
- importjava.util.List;
- importandroid.app.FragmentManager;
- importandroid.app.ListActivity;
- importandroid.os.Bundle;
- importandroid.util.Log;
- importandroid.widget.ArrayAdapter;
- importandroid.widget.ListAdapter;
- publicclassFixProblemsActivityextendsListActivity
- {
- privatestaticfinalStringTAG="MainActivity";
- privateListAdaptermAdapter;
- privateList<String>mDatas;
- privateOtherRetainedFragmentdataFragment;
- privateMyAsyncTaskmMyTask;
- @Override
- publicvoidonCreate(BundlesavedInstanceState)
- {
- super.onCreate(savedInstanceState);
- Log.e(TAG,"onCreate");
- //findtheretainedfragmentonactivityrestarts
- FragmentManagerfm=getFragmentManager();
- dataFragment=(OtherRetainedFragment)fm.findFragmentByTag("data");
- //createthefragmentanddatathefirsttime
- if(dataFragment==null)
- {
- //addthefragment
- dataFragment=newOtherRetainedFragment();
- fm.beginTransaction().add(dataFragment,"data").commit();
- }
- mMyTask=dataFragment.getData();
- if(mMyTask!=null)
- {
- mMyTask.setActivity(this);
- }else
- {
- mMyTask=newMyAsyncTask(this);
- dataFragment.setData(mMyTask);
- mMyTask.execute();
- }
- //thedataisavailableindataFragment.getData()
- }
- @Override
- protectedvoidonRestoreInstanceState(Bundlestate)
- {
- super.onRestoreInstanceState(state);
- Log.e(TAG,"onRestoreInstanceState");
- }
- @Override
- protectedvoidonSaveInstanceState(BundleoutState)
- {
- mMyTask.setActivity(null);
- super.onSaveInstanceState(outState);
- Log.e(TAG,"onSaveInstanceState");
- }
- @Override
- protectedvoidonDestroy()
- {
- Log.e(TAG,"onDestroy");
- super.onDestroy();
- }
- /**
- *回调
- */
- publicvoidonTaskCompleted()
- {
- mDatas=mMyTask.getItems();
- mAdapter=newArrayAdapter<String>(FixProblemsActivity.this,
- android.R.layout.simple_list_item_1,mDatas);
- setListAdapter(mAdapter);
- }
- }
在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/
转载自:http://blog.csdn.net/lmj623565791/article/details/37936275
更多相关文章
- mybatisplus的坑 insert标签insert into select无参数问题的解决
- python起点网月票榜字体反爬案例
- android中使用sqlite、复制assets下的数据库到SD卡、支持大于1M
- Android青翼蝠王之ContentProvider
- Android(安卓)异步加载解决方案
- 关于Android连接远程数据库(mysql、oracle)
- Android(安卓)Contact分析(一):Data, RawContact, Contact之间的关
- Android(安卓)camera 默认显示黑白的问题
- 在Android中查看和管理sqlite数据库