
一、概述
运行时变更就是设备在运行时发生变化(例如屏幕旋转、键盘可用性及语言)。发生这些变化,AndroID会重启Activity,这时就需要保存activity的状态及与activity相关的任务,以便恢复activity的状态。
为此,Google提供了三种解决方案:
对于少量数据: 通过onSaveInstanceState(),保存有关应用状态的数据。 然后在 onCreate() 或 onRestoreInstanceState() 期间恢复 Activity 状态。 对于大量数据:用 Fragment 保留需要回复的对象。 自行处理配置变更,不重启Activity。下面会逐一介绍三种情况,其实保存一些变量对象很简单,难的是当Activity创建异步线程去加载数据时,旋转屏幕时,怎么保存线程的状态。比如,在线程的加载过程中,旋转屏幕,就会存在问题:此时数据没有完成加载,onCreate重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。下面会一一解决这些问题。本文较长,主要是代码多,可以先下载demo,源码下载:http://download.csdn.net/detail/jycboy/9720486对比着看。
二、使用onSaveInstanceState,onRestoreInstanceState
代码如下:
/** * 使用onSaveInstanceState,onRestoreInstanceState; * 在这里不考虑没有加载完毕,就旋转屏幕的情况。 * @author 超超boy * */public class SavedInstanceStateActivity extends ListActivity{ private static final String TAG = "MainActivity"; private listadapter mAdapter; private ArrayList<String> mDatas; private DialogFragment mloadingDialog; private LoadDataAsyncTask mloadDataAsyncTask; @OverrIDe public voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.e(TAG,"onCreate"); initData(savedInstanceState); } /** * 初始化数据 */ private voID initData(Bundle savedInstanceState) { if (savedInstanceState != null) mDatas = savedInstanceState.getStringArrayList("mDatas"); if (mDatas == null) { mloadingDialog = new LoadingDialog(); mloadingDialog.show(getFragmentManager(),"LoadingDialog"); mloadDataAsyncTask = new LoadDataAsyncTask(); mloadDataAsyncTask.execute(); //mloadDataAsyncTas } else { initAdapter(); } } /** * 初始化适配器 */ private voID initAdapter() { mAdapter = new ArrayAdapter<String>( SavedInstanceStateActivity.this,androID.R.layout.simple_List_item_1,mDatas); setlistadapter(mAdapter); } @OverrIDe protected voID onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); Log.e(TAG,"onRestoreInstanceState"); } @OverrIDe //在这里保存数据,好用于返回 protected voID onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.e(TAG,"onSaveInstanceState"); outState.putSerializable("mDatas",mDatas); } /** * 模拟耗时 *** 作 * * @return */ private ArrayList<String> generateTimeConsumingDatas() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printstacktrace(); } return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据","onSaveInstanceState保存数据","getLastNonConfigurationInstance已经被弃用","RabbitMQ","Hadoop","Spark")); } private class LoadDataAsyncTask extends AsyncTask<VoID,VoID,VoID> { @OverrIDe protected VoID doInBackground(VoID... params) { mDatas = generateTimeConsumingDatas(); return null; } @OverrIDe protected voID onPostExecute(VoID result) { mloadingDialog.dismiss(); initAdapter(); } } @OverrIDe protected voID onDestroy() { Log.e(TAG,"onDestroy"); super.onDestroy(); }}界面为一个ListVIEw,onCreate中启动一个异步任务去加载数据,这里使用Thread.sleep模拟了一个耗时 *** 作;当用户旋转屏幕发生重新启动时,会onSaveInstanceState中进行数据的存储,在onCreate中对数据进行恢复,免去了不必要的再加载一遍。
运行结果:
12-24 20:13:41.814 1994-1994/? E/MainActivity: onCreate
12-24 20:13:46.124 1994-1994/? E/MainActivity: onSaveInstanceState
12-24 20:13:46.124 1994-1994/? E/MainActivity: onDestroy
12-24 20:13:46.154 1994-1994/? E/MainActivity: onCreate
12-24 20:13:46.164 1994-1994/? E/MainActivity: onRestoreInstanceState
当正常加载数据完成之后,用户不断进行旋转屏幕,log会不断打出:onSaveInstanceState->onDestroy->onCreate->onRestoreInstanceState,验证Activity重新启动,但是我们没有再次去进行数据加载。
如果在加载的时候,进行旋转,则会发生错误,异常退出(退出原因:dialog.dismiss()时发生NullPointException,因为与当前对话框绑定的FragmentManager为null,在这里这个不是关键)。
效果图:
三、使用Fragment保留对象,恢复数据
果重启 Activity 需要恢复大量数据、重新建立网络连接或执行其他密集 *** 作,依靠系统通过onSaveInstanceState() 回调为您保存的 Bundle,可能无法完全恢复 Activity 状态,因为它并非设计用于携带大型对象(例如位图),而且其中的数据必须先序列化,再进行反序列化,这可能会消耗大量内存并使得配置变更速度缓慢。 在这种情况下,如果 Activity 因配置变更而重启,则可通过保留 Fragment 来减轻重新初始化 Activity 的负担。此片段可能包含对您要保留的有状态对象的引用。
当 AndroID 系统因配置变更而关闭 Activity 时,不会销毁您已标记为要保留的 Activity 的片段。 您可以将此类片段添加到 Activity 以保留有状态的对象。
要在运行时配置变更期间将有状态的对象保留在片段中,请执行以下 *** 作:
扩展 Fragment 类并声明对有状态对象的引用。 在创建片段后调用 setRetainInstance(boolean)。 将片段添加到 Activity。 重启 Activity 后,使用 FragmentManager 检索片段。例如,按如下方式定义片段:
public class RetainedFragment extends Fragment { // data object we want to retain private MyDataObject data; // this method is only called once for this fragment @OverrIDe public voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // retain this fragment setRetainInstance(true); } public voID setData(MyDataObject data) { this.data = data; } public MyDataObject getData() { return data; }}注意:尽管您可以存储任何对象,但是切勿传递与 Activity 绑定的对象,例如,Drawable、Adapter、View 或其他任何与 Context 关联的对象。否则,它将使Activity无法被回收造成内存泄漏。(泄漏资源意味着应用将继续持有这些资源,但是无法对其进行垃圾回收,因此可能会丢失大量内存)
下面举一个实际的例子:
1.RetainedFragment
public class RetainedFragment extends Fragment{ // data object we want to retain private Bitmap data; // this method is only called once for this fragment @OverrIDe public voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // retain this fragment setRetainInstance(true); } public voID setData(Bitmap data) { this.data = data; } public Bitmap getData() { return data; }}只是保持Bitmap对象的引用,你可以用Fragment保存多个对象。
2.FragmentRetainDataActivity:
public class FragmentRetainDataActivity extends Activity{ private static final String TAG = "FragmentRetainData"; private RetainedFragment dataFragment; private DialogFragment mloadingDialog; private ImageVIEw mImageVIEw; private Bitmap mBitmap; BitmapWorkerTask bitmapWorkerTask; @OverrIDe public voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentVIEw(R.layout.activity_main); Log.e(TAG,"onCreate"); // find the retained fragment on activity restarts FragmentManager fm = getFragmentManager(); dataFragment = (RetainedFragment) fm.findFragmentByTag("data"); // create the fragment and data the first time if (dataFragment == null) { // add the fragment dataFragment = new RetainedFragment(); fm.beginTransaction().add(dataFragment,"data").commit(); } // the data is available in dataFragment.getData() mBitmap = dataFragment.getData(); initVIEw(); } /** * 初始化控件 */ private voID initVIEw() { mImageVIEw = (ImageVIEw) findVIEwByID(R.ID.ID_imageVIEw); if(mBitmap != null) mImageVIEw.setimageBitmap(mBitmap); //图片为空时,加载图片;有时候即使dataFragment!=null时,图片也不一定就加载完了,比如在加载的过程中,旋转屏幕,此时图片就没有加载完 else{ mloadingDialog = new LoadingDialog(); mloadingDialog.show(getFragmentManager(),"LOADING_DIALOG"); bitmapWorkerTask = new BitmapWorkerTask(this); bitmapWorkerTask.execute("http://images2015.cnblogs.com/blog/747969/201612/747969-20161222164357995-1098775233.jpg"); } } /** * 异步下载图片的任务。 * 设置成静态内部类是为了防止内存泄漏 * @author guolin */ private static class BitmapWorkerTask extends AsyncTask<String,Bitmap> { //图片的URL地址 private String imageUrl; //保存外部activity的弱引用 private WeakReference<Context> weakReference; public BitmapWorkerTask(Context context) { weakReference = new WeakReference<>(context); } @OverrIDe protected Bitmap doInBackground(String... params) { imageUrl = params[0]; //为了演示加载过程,阻塞2秒 try {Thread.sleep(2000); } catch (InterruptedException e) { e.printstacktrace(); } return downloadUrlToStream(imageUrl); } @OverrIDe protected voID onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); if(bitmap !=null){ FragmentRetainDataActivity retainDataActivity= (FragmentRetainDataActivity) weakReference.get(); //调用回调方法 retainDataActivity.onLoaded(bitmap); } } /** * 建立http请求,并获取Bitmap对象。 * 修改了下 * @param urlString * 图片的URL地址 * @return 解析后的Bitmap对象 */ private Bitmap downloadUrlToStream(String urlString) { httpURLConnection urlConnection = null; Bitmap bitmap = null; try { final URL url = new URL(urlString); urlConnection = (httpURLConnection) url.openConnection(); if(urlConnection.getResponseCode()==httpURLConnection.http_OK){ //连接成功 inputStream is = urlConnection.getinputStream(); bitmap = BitmapFactory.decodeStream(is); is.close(); return bitmap; }else{ return null; } } catch (final IOException e) { e.printstacktrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } } return null; } } //加载完毕的回掉 public voID onLoaded(Bitmap bitmap){ mBitmap = bitmap; mloadingDialog.dismiss(); mImageVIEw.setimageBitmap(mBitmap); // load the data from the web dataFragment.setData(mBitmap); Log.e(TAG,"onLoaded"); } public voID onPause(){ super.onPause(); Log.e(TAG,"onPause"); if(getFragmentManager() != null && mloadingDialog != null) mloadingDialog.dismiss(); } @OverrIDe public voID onDestroy() { super.onDestroy(); Log.e(TAG,"onDestroy"); if(bitmapWorkerTask !=null) bitmapWorkerTask.cancel(true); // store the data in the fragment dataFragment.setData(mBitmap); }}这里边用BitmapWorkerTask异步下载图片,downloadUrlToStream封装了下载图片的代码;
BitmapWorkerTask用弱引用保持外部Activity对象防止内存泄漏,下载完毕后用onLoaded回调方法更新UI。
通过检查dataFragment、mBitmap判断是否已经加载过,加载过直接用就可以。
效果图:
在gif里可以看到,如果未加载完毕就旋转屏幕,它会重新启动异步线程去下载,这种效果并不好,我们会在最后解决这个问题。
四、自行处理配置变更
如果应用在特定配置变更期间无需更新资源,并且因性能限制您需要尽量避免重启,则可声明 Activity 将自行处理配置变更,这样可以阻止系统重启 Activity。
要声明由 Activity 处理配置变更,需设置清单文件manifest:
<activity androID:name=".MyActivity" androID:configChanges="orIEntation|keyboardHIDden" androID:label="@string/app_name">
"orIEntation" 和 "keyboardHIDden",分别用于避免因屏幕方向和可用键盘改变而导致重启)。您可以在该属性中声明多个配置值,方法是用管道 | 字符分隔这些配置值。
注意:API 级别 13 或更高版本的应用时,若要避免由于设备方向改变而导致运行时重启,则除了 "orIEntation" 值以外,您还必须添加 "screenSize" 值。 也就是说,您必须声明 androID:configChanges="orIEntation|screenSize"。
当其中一个配置发生变化时,MyActivity 不会重启。相反,MyActivity 会收到对onConfigurationChanged() 的调用。向此方法传递Configuration 对象指定新设备配置。您可以通过读取Configuration 中的字段,确定新配置,然后通过更新界面中使用的资源进行适当的更改。
例如,以下onConfigurationChanged() 实现检查当前设备方向:
@OverrIDepublic 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(); }}示例代码:
public class ConfigChangesTestActivity extends ListActivity{ private static final String TAG = "MainActivity"; private listadapter mAdapter; private ArrayList<String> mDatas; private DialogFragment mloadingDialog; private LoadDataAsyncTask mloadDataAsyncTask; @OverrIDe public voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.e(TAG,"onCreate"); initData(savedInstanceState); } /** * 初始化数据 */ private voID initData(Bundle savedInstanceState) { mloadingDialog = new LoadingDialog(); mloadingDialog.show(getFragmentManager(),"LoadingDialog"); mloadDataAsyncTask = new LoadDataAsyncTask(); mloadDataAsyncTask.execute(); } /** * 初始化适配器 */ private voID initAdapter() { mAdapter = new ArrayAdapter<String>(ConfigChangesTestActivity.this,mDatas); setlistadapter(mAdapter); } /** * 模拟耗时 *** 作 * * @return */ private ArrayList<String> generateTimeConsumingDatas() { try { Thread.sleep(2000); } catch (InterruptedException e) { } return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据","Spark")); } /** * 当配置发生变化时,不会重新启动Activity。但是会回调此方法,用户自行进行对屏幕旋转后进行处理 */ @OverrIDe public voID onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orIEntation == Configuration.ORIENTATION_LANDSCAPE) { Toast.makeText(this,Toast.LENGTH_SHORT).show(); } else if (newConfig.orIEntation == Configuration.ORIENTATION_PORTRAIT) { Toast.makeText(this,Toast.LENGTH_SHORT).show(); } } private class LoadDataAsyncTask extends AsyncTask<VoID,"onDestroy"); super.onDestroy(); }}这种方法使用简单,在回调方法onConfigurationChanged执行你的 *** 作就可以,不会重启Activity。
但是自行处理配置变可能导致备用资源的使用更为困难,因此不到万不得已不用,大部分情况推荐用Fragment。
效果图:
五、旋转屏幕时如果任务线程未执行完,如何保存恢复
解决上边提到的问题,在未加载完毕的情况下,旋转屏幕,保存任务线程,重启activity不重新执行异步线程。那么他的难点就是保存任务线程,不销毁它,用上边三个方法都可以实现,推荐用1,2种方法。下边就主要介绍用第二种方法Fragment,那么你也肯定想到了,用Fragment保存AsyncTask就可以了。
代码如下:
1. OtherRetainedFragment
/** * 保存对象的Fragment * @author 超超boy * */public class OtherRetainedFragment extends Fragment{ // data object we want to retain // 保存一个异步的任务 private MyAsyncTask data; // this method is only called once for this fragment @OverrIDe public voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // retain this fragment setRetainInstance(true); } public voID setData(MyAsyncTask data) { this.data = data; } public MyAsyncTask getData() { return data; }和之前差不多就是保存对象是MyAsyncTask。
2.MyAsyncTask:
public class MyAsyncTask extends AsyncTask<VoID,VoID>{ //保存外部activity的弱引用 private WeakReference<Context> weakReference; public MyAsyncTask(Context context) { weakReference = new WeakReference<>(context); } private FixProblemsActivity activity; /** * 是否完成 */ private boolean isCompleted; /** * 进度框 */ private LoadingDialog mloadingDialog; private List<String> items; /** * 开始时,显示加载框 */ @OverrIDe protected voID onPreExecute() { mloadingDialog = new LoadingDialog(); activity = (FixProblemsActivity) weakReference.get(); if(activity != null) mloadingDialog.show(activity.getFragmentManager(),"LOADING"); } /** * 加载数据 */ @OverrIDe protected VoID doInBackground(VoID... params) { items = loadingData(); return null; } /** * 加载完成回调当前的Activity */ @OverrIDe protected voID onPostExecute(VoID unused) { isCompleted = true; notifyActivityTaskCompleted(); if (mloadingDialog != null) mloadingDialog.dismiss(); } public List<String> getItems() { return items; } private List<String> loadingData() { try { Thread.sleep(5000); } catch (InterruptedException e) { } return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据","Spark")); } /** * 设置Activity,因为Activity会一直变化 * * @param activity */ public voID setActivity(Context activity) { weakReference = new WeakReference<>(activity); // 设置为当前的Activity this.activity = (FixProblemsActivity) activity; // 开启一个与当前Activity绑定的等待框 if (activity != null && !isCompleted) { mloadingDialog = new LoadingDialog(); mloadingDialog.show(this.activity.getFragmentManager(),"LOADING"); } // 如果完成,通知Activity if (isCompleted) { notifyActivityTaskCompleted(); } } /** * 在Activity不可见时,关闭dialog */ public voID dialogdismiss(){ if(mloadingDialog != null){ mloadingDialog.dismiss(); } } private voID notifyActivityTaskCompleted() { if (null != activity) { activity.onTaskCompleted(); } }} 和之前一样保留Activity的虚引用,防止内存泄漏。 setActivity()方法用于任务未完成时,重启activity相应的创建一个新的dialog。 dialogdismiss用于在在Activity不可见时,关闭dialog,防止以前的dialog造成内存泄漏。 当任务完成时,调用activity的回调方法onTaskCompleted更新UI。3.FixProblemsActivity
public class FixProblemsActivity extends ListActivity{ private static final String TAG = "MainActivity"; private listadapter mAdapter; private List<String> mDatas; private OtherRetainedFragment dataFragment; private MyAsyncTask mMyTask; @OverrIDe public voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.e(TAG,"onCreate"); // find the retained fragment on activity restarts FragmentManager fm = getFragmentManager(); dataFragment = (OtherRetainedFragment) fm.findFragmentByTag("data"); // create the fragment and data the first time if (dataFragment == null) { // add the fragment dataFragment = new OtherRetainedFragment(); fm.beginTransaction().add(dataFragment,"data").commit(); } mMyTask = dataFragment.getData(); if (mMyTask != null) { //与新的Activity进行绑定 mMyTask.setActivity(this); } else { //启动一个新的 mMyTask = new MyAsyncTask(this); dataFragment.setData(mMyTask); mMyTask.execute(); } // the data is available in dataFragment.getData() } @OverrIDe protected voID onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); Log.e(TAG,"onRestoreInstanceState"); } @OverrIDe protected voID onDestroy() { Log.e(TAG,"onDestroy"); super.onDestroy(); } @OverrIDe //在这里关闭Dialog,否则容易造成内存泄漏 protected voID onPause() { super.onPause(); mMyTask.dialogdismiss(); } /** * 回调方法,更新UI * 这里如果在加载的过程中按下返回键返回主Activity时,会出现异常,setAdapter on a null object reference。因为activity被销毁, * 要解决这个问题,可以监听返回键事件做相应处理。 */ public voID onTaskCompleted() { mDatas = mMyTask.getItems(); mAdapter = new ArrayAdapter<String>(FixProblemsActivity.this,mDatas); setlistadapter(mAdapter); }}主要在onCreate方法中执行一些逻辑判断,如果没有开启任务(第一次进入),开启任务;如果已经开启了,调用setActivity(this);
在onPause中关闭dialog,防止内存泄漏。
设置了等待5秒,足够旋转三四个来回了~~~~可以看到虽然在不断的重启,但是丝毫不影响任务的运行和加载框的显示~~~~
效果图:
写着写着就这么晚了,本以为会写的很快。。中间AndroIDstudio的录制视频的功能还不好使啦,逼得我在手机上下了个。。。好分析到此结束。
查阅资料时的一些参考文档:
https://developer.android.google.cn/guide/topics/resources/runtime-changes.html#HandlingTheChange
http://blog.csdn.net/lmj623565791/article/details/37936275
https://developer.android.google.cn/guide/components/activities.html
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持编程小技巧!
以上是内存溢出为你收集整理的Android屏幕旋转 处理Activity与AsyncTask的最佳解决方案全部内容,希望文章能够帮你解决Android屏幕旋转 处理Activity与AsyncTask的最佳解决方案所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)