Android的Activity在不明确的指定屏幕方向和configChanges,当用户进行屏幕旋转时,Activity就会重新启动,这个时候需要考虑的就是数据的保存与恢复,Android给出了一下几种方案:
1、通过Android Activity的onSaveInstanceState()和onRestoreInstanceState()进行数据的保存与恢复,旋转屏幕时Android需要摧毁Activity,这个时候Android系统会调用onSaveInstanceState方法,我们可以在这个方法中进行相关数据的保存,然后Activity重启后,可以在onCreate()和onRestoreInstanceState()这两个方法中进行数据恢复。Activity的onSaveInstanceState是使用Bundle进行存储的,这种方案比较适合小数据量的存储,而且Bundle存储的是可序列化的数据。
2、通过Fragment进行存储数据,即没有布局的Fragment。这种方式可以存储大数据量的存储。创建一个Fragment对象,在对象创建的过程中,调用setRetainInstance(true),然后将Fragment对象添加到Activity中,当Activity重启后通过FragmentManager将Fragment读取出来进行数据恢复。
3、自己手动处理屏幕旋转配置变化,Activity中重写onConfigurationChanged(),在方法里面加入屏幕旋转后的处理逻辑,这样Activity就不会重启。
但是根据上述有一个问题,就是当Activity启动了一个异步的线程去加载数据,然后启动一个ProcessDialog,当数据加载完毕后,ProcessDialog消失。如果在异步线程加载数据的过程中发生屏幕旋转,这个时候就会出现以下问题:
a、异步线程加载数据还没有完成,Activity重启后又会新启动一个异步线程去加载数据,而上一个异步线程有可能还在运行,运行完后有可能会更新已经不存在的控件,这样就会出现问题;
b、关闭ProcessDialog的操作在onPostExecutez中完成,如果上一个异步线程被杀死的话,那就会出现之前的ProcessDialog无法关闭的问题。
下面将会逐一的说明一下上述三种在系统转屏后进行数据存储与恢复的方案已经最后针对异步线程加载过程中出现转屏时的解决办法。
一、使用onSaveInstanceState和onRestoreInstanceState进行数据保存与恢复
先看一下代码:
package com.test.jiangcaicao.fragmentaysnc; import android.app.DialogFragment; import android.database.Cursor; import android.os.AsyncTask; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.ArrayAdapter; import android.widget.ListView; import java.util.ArrayList; public class SaveInstanceStateActivity extends AppCompatActivity { private static final String TAG = "SaveInstanceState"; private DataBaseHelper mDataBaseHelper; private ArrayListmDatas; private ListView mListView; private LoadDataAsync mLoadDataAsyncTask; private DialogFragment mDialogFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_save_instance_state); mListView = findViewById(R.id.list_view); mDataBaseHelper = ((MyApplication)getApplication()).getDataBaseHelper(); initData(savedInstanceState); if (savedInstanceState == null) { Log.d(TAG, "onCreate: saveInstanceState is "+savedInstanceState); } } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); Log.d("111", "onRestoreInstanceState: ***************"); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.d("111", "onSaveInstanceState: ****************"); outState.putSerializable("mDatas", mDatas); } private void initData(Bundle saveInstanceState) { if (saveInstanceState != null) { mDatas = saveInstanceState.getStringArrayList("mDatas"); } if (mDatas != null) { initAdapter(); } else { mDialogFragment = new DialogFragment(); mDialogFragment.show(getFragmentManager(), "LoadingDialog"); mLoadDataAsyncTask = new LoadDataAsync(); mLoadDataAsyncTask.execute(); } } private ArrayList getDBData() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } ArrayList data = new ArrayList<>(); if (mDataBaseHelper != null) { Cursor cursor = mDataBaseHelper.getAllInitData(); if (cursor != null && cursor.moveToFirst()) { do { String fruit_name = cursor.getString(cursor.getColumnIndex("name")); data.add(fruit_name); } while (cursor.moveToNext()); } cursor.close(); } return data; } private void initAdapter() { ArrayAdapter adapter = new ArrayAdapter (this, android.R.layout.simple_list_item_1, mDatas); mListView.setAdapter(adapter); } private class LoadDataAsync extends AsyncTask , Void, Void> { @Override protected Void doInBackground(Void... voids) { mDatas = getDBData(); return null; } @Override protected void onPostExecute(Void aVoid) { mDialogFragment.dismiss(); initAdapter(); super.onPostExecute(aVoid); } } }
界面中添加了一个ListView,然后去数据库中读取数据,将取到的数据显示到ListView上。在获取数据的过程中使用Thread.sleep模拟耗时事件。首先第一次进入的时候出现等待5S后显示出数据,数据显示出来后进行转屏,等待窗口没有出现,说明是在转屏时,在onSaveInstanceState方法中进行数据存储(可以通过log查看),然后在onCreate方法中直接读取之前在onSaveInstanceState保存的数据,然后显示出来了。如果在第一次数据加载过程中进行转屏操作,程序就会crash。原因是:AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'android.app.FragmentTransaction android.app.FragmentManager.beginTransaction()' on a null object reference。出现了空指针错误,这个问题后面会给出解决方案。
由于我是用真机运行所以就不向这上面截图了,大家可以自己写一遍看一下效果,毕竟自己写完后感触还是不一样的。以上的代码不是全部的,最后会给出源码的链接地址。
二、使用Fragment进行数据的保存与恢复,即没有布局的Fragment的作用
如果重新启动Activity需要恢复大量的数据,比如一些密集型操作,这个时候转屏时可能会给用户一个很不好的用户体验,按照上面的方法,在onSaveInstanceState方法中使用Bundle存储数据显示有一些捉襟见肘了,因为Bundle只能存储序列化的数据,这样序列化数据又很耗费内存,更加的使转屏操作速度变得缓慢。在这种情况下,可以使用Fragment来进行数据保存与恢复,Fragment可以包含你想要保持的有状态的对象的引用。
当Android系统因为配置变化关闭Activity的时候,Activity中被标识保持的fragments不会被销毁。所以可以在Activity中添加这样的fragements来保存有状态的对象。
在运行时配置发生变化时,在Fragment中保存有状态的对象步骤如下:
1. 新建一个继承Fragment的类,并在类里面声明需要保存的数据的数据类型作为成员变量
2. 当这个继承Fragment类的被创建时,类似onCreate方法中调用setRetainInstance(true);
3. 将继承Fragment类的实例添加到Fragment中
4. 当Activity重新启动后,使用FragmentManager对Fragment对象进行恢复
贴一下部分代码:
首先是Fragment的类:
这个类只保存了一个图片对象,比较简单,只需要声明需要保存的数据对象,然后提供getter和setter,注意,一定要在onCreate调用setRetainInstance(true);
public class RemainDataFragment extends Fragment { private Bitmap mBitmap; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } public void setBitmap(Bitmap mBitmap) { this.mBitmap = mBitmap; } public Bitmap getBitmap() { return mBitmap; }
然后是SaveFragmentStateActivity:
package com.test.jiangcaicao.fragmentaysnc; import android.app.DialogFragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Handler; import android.os.Message; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.ImageView; import java.io.IOException; import okhttp3.Call; import okhttp3.Response; public class SaveFragmentStateActivity extends AppCompatActivity { private static final int MSG_UPDATE_BITMAP = 1; private static final String TAG = "SaveFragmentState"; private ImageView mImageView; private RemainDataFragment mRemainDataFragment; private Bitmap mBitmap; private DialogFragment mDialogFragment; private H handler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_save_fragment_state); FragmentManager fragmentManager = getFragmentManager(); mRemainDataFragment = (RemainDataFragment) fragmentManager.findFragmentByTag("data"); if (mRemainDataFragment == null) { mRemainDataFragment = new RemainDataFragment(); FragmentTransaction transaction = fragmentManager.beginTransaction(); transaction.add(mRemainDataFragment, "data"); transaction.commit(); } handler = new H(); mImageView = findViewById(R.id.image); mBitmap = collectLoadedData(); initData(mBitmap); } private void initData(Bitmap bitmap) { if (bitmap == null) { mDialogFragment = new DialogFragment(); mDialogFragment.show(getFragmentManager(), "Loading_Dialog"); String address = "https://img-my.csdn.net/uploads/201407/18/1405652589_5125.jpg"; HttpUtil.sendOkHttpRequest(address, callback); } else { mImageView.setImageBitmap(bitmap); } } private Bitmap collectLoadedData() { return mRemainDataFragment.getBitmap(); } @Override protected void onDestroy() { mRemainDataFragment.setBitmap(mBitmap); super.onDestroy(); } private okhttp3.Callback callback = new okhttp3.Callback(){ @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { Message message = new Message(); message.what = MSG_UPDATE_BITMAP; message.obj = response.body().bytes(); handler.sendMessageDelayed(message, 5000); mDialogFragment.dismiss(); } }; private class H extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_BITMAP: byte[] bytes = (byte[]) msg.obj; mBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); mImageView.setImageBitmap(mBitmap); break; default: break; } super.handleMessage(msg); } } }
在Activity启动时去网络上加载上一张图片,网络请求框架使用okhttp3。当请求下载完后显示到控件上,当Activity要关闭的时候(配置发生变化如屏幕旋转),将下载的图片对象保存到Fragment对象中,当再次启动时,将保存到Fragment对象中的图片取出显示到空间上,不必再次去网络请求下载。
三、手动处理屏幕旋转配置变化
如果屏幕旋转、语言变化等配置变化时,不想系统让重启你的Activity,就需要处理配置变化信息,步骤如下:
1. 在Activity中重写onConfigurationChanged方法,里面是当配置发生变化时的处理逻辑,需要注意的是方法里的super.onConfigurationChanged()这条语句不可以删除,否则会出问题。
2. 在AndroidManifest.xml相应中activity中添加android:configChanges属性,属性值为"orientation|screenSize"(如果target api level小于13的话属性值中删除screenSize即Android3.2)
代码如下,首先是AndroidManifest.xml:
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" package="com.test.jiangcaicao.fragmentaysnc"> android:name="android.permission.INTERNET" /> android:name=".MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> android:name=".MainActivity"> android:name="android.intent.action.MAIN" /> android:name="android.intent.category.LAUNCHER" /> android:name=".SaveInstanceStateActivity" /> android:name=".SaveFragmentStateActivity" /> android:name=".RewriteActivity" android:configChanges="orientation|screenSize" />
RewriteActivity.java代码,这个与SaveInstanceStateActivity代码很相似,只不过删除了保存与恢复的代码,添加重写onConfigurationChange:
package com.test.jiangcaicao.fragmentaysnc; import android.app.DialogFragment; import android.content.res.Configuration; import android.database.Cursor; import android.os.AsyncTask; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.ArrayAdapter; import android.widget.ListView; import java.util.ArrayList; public class RewriteActivity extends AppCompatActivity { private static final String TAG = "RewriteActivity"; private DataBaseHelper mDataBaseHelper; private ArrayListmDatas; private ListView mListView; private LoadDataAsync mLoadDataAsyncTask; private DialogFragment mDialogFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_rewrite); mListView = findViewById(R.id.list_view1); mDataBaseHelper = ((MyApplication)getApplication()).getDataBaseHelper(); initData(); } @Override public void onConfigurationChanged(Configuration newConfig) { Log.d(TAG, "onConfigurationChanged: newConfig oriengation is "+newConfig.orientation); super.onConfigurationChanged(newConfig); } private void initData() { mDialogFragment = new DialogFragment(); mDialogFragment.show(getFragmentManager(), "LoadingDialog"); mLoadDataAsyncTask = new LoadDataAsync(); mLoadDataAsyncTask.execute(); } private ArrayList getDBData() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } ArrayList data = new ArrayList<>(); if (mDataBaseHelper != null) { Cursor cursor = mDataBaseHelper.getAllInitData(); if (cursor != null && cursor.moveToFirst()) { do { String fruit_name = cursor.getString(cursor.getColumnIndex("name")); data.add(fruit_name); } while (cursor.moveToNext()); } cursor.close(); } return data; } private void initAdapter() { ArrayAdapter adapter = new ArrayAdapter (this, android.R.layout.simple_list_item_1, mDatas); mListView.setAdapter(adapter); } private class LoadDataAsync extends AsyncTask , Void, Void> { @Override protected Void doInBackground(Void... voids) { mDatas = getDBData(); return null; } @Override protected void onPostExecute(Void aVoid) { mDialogFragment.dismiss(); initAdapter(); super.onPostExecute(aVoid); } } }
运行后,效果与使用onSaveInstanceState和onRestoreInstanceState进行数据保存与恢复一致。
四、加载数据时进行屏幕旋转(配置信息变化)的解决方案
上述的解决方案基本都有一个问题,那就是在数据加载过程中出现配置信息变化怎么办?上述的解决方案中如果在数据加载过程中转屏会出现程序Crash问题,这样也会给用户一个很不好的体验,接下来就是针对这个问题的解决方案。
解决方案的思路:使用Fragment保存数据,但是保存的是一个异步任务,因为在配置信息发生变化的时候,Fragment保存的数据系统是不会删除的,这样异步任务实际还是在后台运行。这样给用户的体验就是无论怎么转屏数据没有中断加载,这样给用户的体验就很好了。
下面是代码:
SaveAsyncStateActivity:
package com.test.jiangcaicao.fragmentaysnc; import android.content.res.Configuration; import android.os.PersistableBundle; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.ArrayAdapter; import android.widget.ListView; public class SaveAsyncStaskActivity extends AppCompatActivity { private ListView mListView; private MyAsyncTask mMyAsyncTask; private OtherRetainedFragment mOtherRetainedFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_save_async_stask); mListView = findViewById(R.id.listview); startLoadData(); } private void startLoadData() { mOtherRetainedFragment = (OtherRetainedFragment) getSupportFragmentManager().findFragmentByTag("loaddata"); if (mOtherRetainedFragment == null) { mOtherRetainedFragment = new OtherRetainedFragment(); getSupportFragmentManager().beginTransaction().add(mOtherRetainedFragment, "loaddata").commit(); } mMyAsyncTask = mOtherRetainedFragment.getMyAsyncTask(); if (mMyAsyncTask == null) { mMyAsyncTask = new MyAsyncTask(this); mOtherRetainedFragment.setMyAsyncTask(mMyAsyncTask); mMyAsyncTask.execute(); } else { mMyAsyncTask.setActivity(this); } } private void initAdapter() { ArrayAdapteradapter = new ArrayAdapter (this, android.R.layout.simple_list_item_1, mMyAsyncTask.getDatas()); mListView.setAdapter(adapter); } public void notifyDataLoadCompeleted() { initAdapter(); } @Override public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { Log.d("222", "onSaveInstanceState: "); mMyAsyncTask.setActivity(null); super.onSaveInstanceState(outState, outPersistentState); } @Override protected void onDestroy() { super.onDestroy(); } }
MyAsyncTask:
public class MyAsyncTask extends AsyncTask, Void, Void> { private boolean isCompeleted; private SaveAsyncStaskActivity mActivity; private List mItem; private DataBaseHelper mDataBaseHelper; private ProgressDialog mProcessDialog; public MyAsyncTask(SaveAsyncStaskActivity activity) { mActivity = activity; } public void setActivity(SaveAsyncStaskActivity activity) { if (activity == null) { mProcessDialog.dismiss(); } mActivity = activity; if (!isCompeleted && activity != null) { mProcessDialog = new ProgressDialog(mActivity); mProcessDialog.show(); } if (isCompeleted && mActivity != null) { notifyActivity(); } } public List getDatas() { return mItem; } private List loadData() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } ArrayList data = new ArrayList<>(); if (mDataBaseHelper != null) { Cursor cursor = mDataBaseHelper.getAllInitData(); if (cursor != null && cursor.moveToFirst()) { do { String fruit_name = cursor.getString(cursor.getColumnIndex("name")); data.add(fruit_name); }while (cursor.moveToNext()); } cursor.close(); } return data; } @Override protected void onPreExecute() { isCompeleted = false; mDataBaseHelper = ((MyApplication)(mActivity.getApplication())).getDataBaseHelper(); mProcessDialog = new ProgressDialog(mActivity); mProcessDialog.show(); super.onPreExecute(); } @Override protected Void doInBackground(Void... voids) { mItem = loadData(); return null; } @Override protected void onPostExecute(Void aVoid) { if (mProcessDialog != null) { mProcessDialog.dismiss(); } isCompeleted = true; notifyActivity(); } private void notifyActivity() { if (mActivity != null) { mActivity.notifyDataLoadCompeleted(); } } }
OtherRetainedFragment:
public class OtherRetainedFragment extends Fragment { private MyAsyncTask myAsyncTask; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } public void setMyAsyncTask(MyAsyncTask task) { myAsyncTask = task; } public MyAsyncTask getMyAsyncTask() { return myAsyncTask; } }
上述代码中之前的ProcessDialog使用的是DialogFragment,但是在加载过程中进行屏幕旋转,数据加载完后DialogFragment没有dismiss掉,如果在加载过程中不进行旋转就可以正常dismiss掉,具体原因还不清楚,等后续再调查一下吧,暂时使用了ProgressDialog替代了。
代码下载