編輯:關於android開發
運行時變更就是設備在運行時發生變化(例如屏幕旋轉、鍵盤可用性及語言)。發生這些變化,Android會重啟Activity,這時就需要保存activity的狀態及與activity相關的任務,以便恢復activity的狀態。
為此,google提供了三種解決方案:
下面會逐一介紹三種情況,其實保存一些變量對象很簡單,難的是當Activity創建異步線程去加載數據時,旋轉屏幕時,怎麼保存線程的狀態。比如,在線程的加載過程中,旋轉屏幕,就會存在問題:此時數據沒有完成加載,onCreate重新啟動時,會再次啟動線程;而上個線程可能還在運行,並且可能會更新已經不存在的控件,造成錯誤。下面會一一解決這些問題。本文較長,主要是代碼多,可以先下載demo,源碼下載:http://download.csdn.net/detail/jycboy/9720486對比著看。
代碼如下:
/** * 使用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,在這裡這個不是關鍵)。
效果圖:
果重啟 Activity 需要恢復大量數據、重新建立網絡連接或執行其他密集操作,依靠系統通過onSaveInstanceState()
回調為您保存的 Bundle
,可能無法完全恢復 Activity 狀態,因為它並非設計用於攜帶大型對象(例如位圖),而且其中的數據必須先序列化,再進行反序列化,這可能會消耗大量內存並使得配置變更速度緩慢。 在這種情況下,如果 Activity 因配置變更而重啟,則可通過保留 Fragment
來減輕重新初始化 Activity 的負擔。此片段可能包含對您要保留的有狀態對象的引用。
當 Android 系統因配置變更而關閉 Activity 時,不會銷毀您已標記為要保留的 Activity 的片段。 您可以將此類片段添加到 Activity 以保留有狀態的對象。
要在運行時配置變更期間將有狀態的對象保留在片段中,請執行以下操作:
例如,按如下方式定義片段:
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, Void, 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()
實現檢查當前設備方向:
@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(); } }
示例代碼:
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, android.R.layout.simple_list_item_1, mDatas); setListAdapter(mAdapter); } /** * 模擬耗時操作 * * @return */ private ArrayList<String> generateTimeConsumingDatas() { try { Thread.sleep(2000); } catch (InterruptedException e) { } return new ArrayList<String>(Arrays.asList("通過Fragment保存大量數據", "onSaveInstanceState保存數據", "getLastNonConfigurationInstance已經被棄用", "RabbitMQ", "Hadoop", "Spark")); } /** * 當配置發生變化時,不會重新啟動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) { mDatas = generateTimeConsumingDatas(); return null; } @Override protected void onPostExecute(Void result) { mLoadingDialog.dismiss(); initAdapter(); } } @Override protected void onDestroy() { Log.e(TAG, "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, 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保存大量數據", "onSaveInstanceState保存數據", "getLastNonConfigurationInstance已經被棄用", "RabbitMQ", "Hadoop", "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(); } } }
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, android.R.layout.simple_list_item_1, mDatas); setListAdapter(mAdapter); } }
主要在onCreate方法中執行一些邏輯判斷,如果沒有開啟任務(第一次進入),開啟任務;如果已經開啟了,調用setActivity(this);
在onPause中關閉dialog,防止內存洩漏。
設置了等待5秒,足夠旋轉三四個來回了~~~~可以看到雖然在不斷的重啟,但是絲毫不影響任務的運行和加載框的顯示~~~~
效果圖:
寫著寫著就這麼晚了,本以為會寫的很快。。中間Androidstudio的錄制視頻的功能還不好使啦,逼得我在手機上下了個。。。好分析到此結束。
源碼下載:http://download.csdn.net/detail/jycboy/9720486
轉載請注明出處:http://www.cnblogs.com/jycboy/p/save_state_data.html
查閱資料時的一些參考文檔:
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
ListView之點擊展開菜單,listview展開菜單一、概述 ListView點擊item顯示菜單是要實現這樣的效果: 需要實現的邏輯如下: 1)點擊一個普通ite
自定義控件——旋轉動畫,自定義控件旋轉
Android 異步Http框架簡介和實現原理,android框架在前幾篇文章中《Android 采用get方式提交數據到服務器》《Android 采用post方式提交數
Android之ListView&Json加載網絡數據,androidlistview使用到的主要內容: 1、Json 解析網絡數據 2、異步任務加載圖片和數據
如何處理 android 方法總數超過 65536 . the numb