源代码参考:360云盘中---自己的学习资料---Android总结过的项目---FragmentDemo.rar
一、概述
众所周知,Activity 在不明确指定屏幕方向和 configChanges 时,当用户旋转屏幕会重新启动。当然了,应对这种情况,Android 给出了几种方案:
1、如果是少量数据,可以通过 onSaveInstanceState() 和 onRestoreInstanceState() 进行保存与恢复。
Android 会在销毁你的 Activity 之前调用 onSaveInstanceState() 方法,于是,你可以在此方法中存储关于应用状态的数据。然后你可以在 onCreate() 或onRestoreInstanceState() 方法中恢复。
2、如果是大量数据,使用 Fragment 保持需要恢复的对象。
3、自已处理配置变化。
注:getLastNonConfigurationInstance() 已经被弃用,被上述方法二替代。
--------------------------------------------------------------------------------------------
二、难点
假设当前 Activity 在 onCreate 中启动一个异步线程去加在数据,当然为了给用户一个很好的体验,会有一个 ProgressDialog,当数据加载完成,ProgressDialog 消失,设置数据。
这里,如果在异步数据完成加载之后,旋转屏幕,使用上述1、2两种方法都不会很难,无非是保存数据和恢复数据。
但是,如果正在线程加载的时候,进行旋转,会存在以下问题:
1.此时数据没有完成加载,onCreate 重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。
2.关闭 ProgressDialog 的代码在线程的 onPostExecutez 中,但是上个线程如果已经杀死,无法关闭之前 ProgressDialog。
3.谷歌的官方不建议使用 ProgressDialog,这里我们会使用官方推荐的 DialogFragment 来创建我的加载框,如果你不了解:请看 Android 官方推荐 : DialogFragment 创建对话框。这样,其实给我们带来一个很大的问题,DialogFragment 说白了是 Fragment,和当前的 Activity 的生命周期会发生绑定,我们旋转屏幕会造成 Activity 的销毁,当然也会对 DialogFragment 造成影响。
下面我将使用几个例子,分别使用上面的 3 种方式,和如何最好的解决上述的问题。
--------------------------------------------------------------------------------------------
三、使用 onSaveInstanceState() 和 onRestoreInstanceState() 进行数据保存与恢复
/**
* 不考虑加载时,进行旋转的情况,有意的避开这种情况,后面例子会介绍解决方案
*/
public class SavedInstanceStateUsingActivity 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();
} else {
initAdapter();
}
}
/**
* 初始化适配器
*/
private void initAdapter() {
mAdapter = new ArrayAdapter<String>(
SavedInstanceStateUsingActivity.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(2000);
} catch (InterruptedException e) {
}
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 中对数据进行恢复,免去了不必要的再加载一遍。
运行结果:
当正常加载数据完成之后,用户不断进行旋转屏幕,log会不断打出:onSaveInstanceState->onDestroy->onCreate->onRestoreInstanceState,验证我们的确是重新启动了,但是我们没有再次去进行数据加载。
如果在加载的时候,进行旋转,则会发生错误,异常退出(退出原因:dialog.dismiss() 时发生 NullPointException,因为与当前对话框绑定的 FragmentManager 为 null,又有兴趣的可以去 Debug,这个不是关键)。
效果图:
--------------------------------------------------------------------------------------------
四、使用 Fragment 来保存对象,用于恢复数据
如果重新启动你的 Activity 需要恢复大量的数据,重新建立网络连接,或者执行其他的密集型操作,这样因为配置发生变化而完全重新启动可能会是一个慢的用户体验。并且,使用系统提供的 onSaveIntanceState() 的回调中,使用 Bundle 来完全恢复你 Activity 的状态是可能是不现实的(Bundle 不是设计用来携带大量数据的(例如 bitmap),并且Bundle 中的数据必须能够被序列化和反序列化),这样会消耗大量的内存和导致配置变化缓慢。在这样的情况下,当你的 Activity 因为配置发生改变而重启,你可以通过保持一个Fragment 来缓解重新启动带来的负担。这个 Fragment 可以包含你想要保持的有状态的对象的引用。
当 Android 系统因为配置变化关闭你的 Activity 的时候,你的 Activity 中被标识保持的 Fragments 不会被销毁。你可以在你的 Activity 中添加这样的 Fragements 来保存有状态的对象。
在运行时配置发生变化时,在 Fragment 中保存有状态的对象
1.继承 Fragment,声明引用指向你的有状态的对象
2.当 Fragment 创建时调用 setRetainInstance(boolean)
3.把 Fragment 实例添加到 Activity 中
4.当 Activity 重新启动后,使用 FragmentManager 对 Fragment 进行恢复
/**
* 使用本 Fragment 保存大数据
*/
public class RetainedFragment extends Fragment {
// data object we want to retain
private Bitmap mData;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public Bitmap getmData() {
return mData;
}
public void setmData(Bitmap mData) {
this.mData = mData;
}
}
比较简单,只需要声明需要保存的数据对象,然后提供 getter 和 setter,注意,一定要在 onCreate 调用 setRetainInstance(true);
/**
* 使用 Fragment 保存大数据的主 Activity
*/
public class FragmentRetainDataActivity extends Activity {
private static final String TAG = "FragmentRetainDataActivity";
private RetainedFragment mDataFragment;
private DialogFragment mLoadingDialog;
private LoadDataAsyncTask mLoadDataAsyncTask;
private ImageView mImageView;
private Bitmap mBitmap;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_retain);
Log.e(TAG, "onCreate");
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
mDataFragment = (RetainedFragment) fm.findFragmentByTag("data");
// create the fragment and data the first time
if (mDataFragment == null) {
// add the fragment
mDataFragment = new RetainedFragment();
fm.beginTransaction().add(mDataFragment, "data").commit();
}
mBitmap = collectMyLoadedData();
initData();
// the data is available in mDataFragment.getData()
}
@Override
public void onDestroy() {
Log.e(TAG, "onDestroy");
super.onDestroy();
// store the data in the fragment
mDataFragment.setmData(mBitmap);
}
/**
* 初始化数据
*/
private void initData() {
mImageView = (ImageView) findViewById(R.id.ivFragmentRetain);
if (mBitmap == null) {
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(getFragmentManager(), "LOADING_DIALOG");
mLoadDataAsyncTask = new LoadDataAsyncTask();
mLoadDataAsyncTask.execute();
// RequestQueue tRequestQueue = Volley
// .newRequestQueue(FragmentRetainDataActivity.this);
// ImageRequest imageRequest = new ImageRequest(
// "//img-my.csdn.net/uploads/201407/18/1405652589_5125.jpg",
// new Response.Listener<Bitmap>() {
// //
// @Override
// public void onResponse(Bitmap response) {
// mBitmap = response;
// mImageView.setImageBitmap(mBitmap);
// // load the data from the web
// mDataFragment.setmData(mBitmap);
// mLoadingDialog.dismiss();
// }
// }, 0, 0, Config.RGB_565, null);
// tRequestQueue.add(imageRequest);
} else {
mImageView.setImageBitmap(mBitmap);
}
}
private Bitmap collectMyLoadedData() {
return mDataFragment.getmData();
}
private class LoadDataAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
mBitmap = getImg();
return null;
}
@Override
protected void onPostExecute(Void result) {
mLoadingDialog.dismiss();
initImg();
}
}
private Bitmap getImg() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
return FileUtils.getImage();
}
private void initImg() {
mImageView.setImageBitmap(mBitmap);
}
}
--------------------------------------------------------------------------------------------
五、配置 configChanges,自己对屏幕旋转的变化进行处理
在menifest中进行属性设置:
<activity
android:name="com.xjl.fragmentdemo.rotate_screen.config.ConfigChangesTestActivity"
android:configChanges="screenSize|orientation"
android:label="配置 configChanges,自己对屏幕旋转的变化进行处理" >
<intent-filter>
<action android:name="fragment_demo" />
</intent-filter>
</activity>
低版本的 API 只需要加入 orientation,而高版本的则需要加入 screenSize。
/**
* 配置 configChanges,自己对屏幕旋转的变化进行处理
*/
public class ConfigChangesTestActivity extends Activity {
private static final String TAG = "MainActivity";
private DialogFragment mLoadingDialog;
private LoadDataAsyncTask mLoadDataAsyncTask;
private ImageView mImageView;
private Bitmap mBitmap;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate");
initData(savedInstanceState);
}
@Override
protected void onDestroy() {
Log.e(TAG, "onDestroy");
super.onDestroy();
}
/**
* 初始化数据
*/
private void initData(Bundle savedInstanceState) {
setContentView(R.layout.activity_fragment_retain);
mImageView = (ImageView) findViewById(R.id.ivFragmentRetain);
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(getFragmentManager(), "LoadingDialog");
mLoadDataAsyncTask = new LoadDataAsyncTask();
mLoadDataAsyncTask.execute();
}
/**
* 当配置发生变化时,不会重新启动Activity。但是会回调此方法,用户自行进行对屏幕旋转后进行处理
*/
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
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();
}
}
private class LoadDataAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
mBitmap = getImg();
return null;
}
@Override
protected void onPostExecute(Void result) {
mLoadingDialog.dismiss();
initImg();
}
}
/**
* 模拟耗时操作
*
* @return
*/
private Bitmap getImg() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
return FileUtils.getImage();
}
/**
* 加载图片
*/
private void initImg() {
mImageView.setImageBitmap(mBitmap);
}
}
对第一种方式的代码进行了修改,去掉了保存与恢复的代码,重写了 onConfigurationChanged;此时,无论用户何时旋转屏幕都不会重新启动 Activity,并且onConfigurationChanged 中的代码可以得到调用。从效果图可以看到,无论如何旋转不会重启 Activity.
--------------------------------------------------------------------------------------------
六、旋转屏幕的最佳实践
下面要开始今天的难点了,就是处理文章开始时所说的,当异步任务在执行时,进行旋转,如果解决上面的问题。
首先说一下探索过程:
起初,我认为此时旋转无非是再启动一次线程,并不会造成异常,我只要即使的在onDestroy里面关闭上一个异步任务就可以了。事实上,如果我关闭了,上一次的对话框会一直存在;如果我不关闭,但是 activity 是一定会被销毁的,对话框的 dismiss 也会出异常。真心很蛋疼,并且即使对话框关闭了,任务关闭了;用户旋转还是会造成重新创建任务,从头开始加载数据。
下面我们希望有一种解决方案:在加载数据时旋转屏幕,不会对加载任务进行中断,且对用户而言,等待框在加载完成之前都正常显示:
当然我们还使用 Fragment 进行数据保存,毕竟这是官方推荐的:
/**
* 保存对象的 Fragment
*/
public class OtherRetainedFragment extends Fragment {
// data object we want to retain
// 保存一个异步的任务
private MyAsyncTask mData;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public MyAsyncTask getmData() {
return mData;
}
public void setmData(MyAsyncTask mData) {
this.mData = mData;
}
}
和上面的差别不大,唯一不同的就是它要保存的对象编程一个异步的任务了,相信看到这,已经知道经常上述问题的一个核心了,保存一个异步任务,在重启时,继续这个任务。
public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
private FixProblemsActivity mActivity;
/**
* 是否完成
*/
private boolean mBolCompleted;
/**
* 进度框
*/
private LoadingDialog mLoadingDialog;
private List<String> mItems;
public MyAsyncTask(FixProblemsActivity mActivity) {
this.mActivity = mActivity;
}
/**
* 开始时,显示加载框
*/
@Override
protected void onPreExecute() {
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(mActivity.getFragmentManager(), "LOADING");
}
/**
* 加载数据
*/
@Override
protected Void doInBackground(Void... params) {
mItems = loadingData();
return null;
}
/**
* 加载完成回调当前的mActivity
*/
@Override
protected void onPostExecute(Void unused) {
mBolCompleted = true;
notifymActivityTaskCompleted();
if (mLoadingDialog != null) {
mLoadingDialog.dismiss();
}
}
public List<String> getmItems() {
return mItems;
}
private List<String> loadingData() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
"onSaveInstanceState保存数据",
"getLastNonConfigurationInstance已经被弃用", "RabbitMQ", "Hadoop",
"Spark"));
}
/**
* 设置mActivity,因为mActivity会一直变化
*
* @param mActivity
*/
public void setmActivity(FixProblemsActivity mActivity) {
// 如果上一个mActivity销毁,将与上一个mActivity绑定的DialogFragment销毁
if (mActivity == null) {
mLoadingDialog.dismiss();
}
// 设置为当前的mActivity
this.mActivity = mActivity;
// 开启一个与当前mActivity绑定的等待框
if (mActivity != null && !mBolCompleted) {
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(mActivity.getFragmentManager(), "LOADING");
}
// 如果完成,通知mActivity
if (mBolCompleted) {
notifymActivityTaskCompleted();
}
}
private void notifymActivityTaskCompleted() {
if (null != mActivity) {
mActivity.onTaskCompleted();
}
}
}
异步任务中,管理一个对话框,当开始下载前,进度框显示,下载结束进度框消失,并为A ctivity 提供回调。当然了,运行过程中 Activity 不断的重启,我们也提供了setActivity 方法,onDestory 时,会 setActivity(null)防止内存泄漏,同时我们也会关闭与其绑定的加载框;当 onCreate 传入新的 Activity 时,我们会在再次打开一个加载框,当然了因为屏幕的旋转并不影响加载的数据,所有后台的数据一直继续在加载。是不是很完美
/**
* 主 Activity
*/
public class FixProblemsActivity extends ListActivity {
private static final String TAG = "MainActivity";
private OtherRetainedFragment mDataFragment;
private MyAsyncTask mMyTask;
private ListAdapter mAdapter;
private List<String> mDatas;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate");
initControl(); // 加载控件
}
private void initControl() {
// find the retained fragment on activity restarts
FragmentManager tFManager = getFragmentManager();
mDataFragment = (OtherRetainedFragment) tFManager.findFragmentByTag("data");
// create the fragment and data the first time
if (mDataFragment == null) {
// add the fragment
mDataFragment = new OtherRetainedFragment();
tFManager.beginTransaction().add(mDataFragment, "data").commit();
}
mMyTask = mDataFragment.getmData();
if (mMyTask != null) {
mMyTask.setmActivity(this);
} else {
mMyTask = new MyAsyncTask(this);
mDataFragment.setmData(mMyTask);
mMyTask.execute();
}
// the data is available in mDataFragment.getData()
}
@Override
protected void onDestroy() {
Log.e(TAG, "onDestroy");
super.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mMyTask.setmActivity(null);
Log.e(TAG, "onSaveInstanceState");
}
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
Log.e(TAG, "onRestoreInstanceState");
}
/**
* 回调
*/
public void onTaskCompleted() {
mDatas = mMyTask.getmItems();
mAdapter = new ArrayAdapter<String>(FixProblemsActivity.this,
android.R.layout.simple_list_item_1, mDatas);
setListAdapter(mAdapter);
}
}
在 onCreate 中,如果没有开启任务(第一次进入),开启任务;如果已经开启了,调用 setActivity(this);
在 onSaveInstanceState 把当前任务加入 Fragment
我设置了等待5秒,足够旋转三四个来回了,可以看到虽然在不断的重启,但是丝毫不影响加载数据任务的运行和加载框的显示
可以看到我在加载的时候就三心病狂的旋转屏幕~~但是丝毫不影响显示效果与任务的加载~~
最后,说明一下,其实不仅是屏幕旋转需要保存数据,当用户在使用你的 app 时,忽然接到一个来电,长时间没有回到你的 app 界面也会造成 Activity 的销毁与重建,所以一个行为良好的 App,是有必要拥有恢复数据的能力的