編輯:關於Android編程
眾所周知,Activity在不明確指定屏幕方向和configChanges時,當用戶旋轉屏幕會重新啟動。當然了,應對這種情況,Android給出了幾種方案:
a、如果是少量數據,可以通過onSaveInstanceState()和onRestoreInstanceState()進行保存與恢復。
Android會在銷毀你的Activity之前調用onSaveInstanceState()方法,於是,你可以在此方法中存儲關於應用狀態的數據。然後你可以在onCreate()或onRestoreInstanceState()方法中恢復。
b、如果是大量數據,使用Fragment保持需要恢復的對象。
c、自已處理配置變化。
注:getLastNonConfigurationInstance()已經被棄用,被上述方法二替代。
假設當前Activity在onCreate中啟動一個異步線程去夾在數據,當然為了給用戶一個很好的體驗,會有一個ProgressDialog,當數據加載完成,ProgressDialog消失,設置數據。
這裡,如果在異步數據完成加載之後,旋轉屏幕,使用上述a、b兩種方法都不會很難,無非是保存數據和恢復數據。
但是,如果正在線程加載的時候,進行旋轉,會存在以下問題:
a)此時數據沒有完成加載,onCreate重新啟動時,會再次啟動線程;而上個線程可能還在運行,並且可能會更新已經不存在的控件,造成錯誤。
b)關閉ProgressDialog的代碼在線程的onPostExecutez中,但是上個線程如果已經殺死,無法關閉之前ProgressDialog。
c)谷歌的官方不建議使用ProgressDialog,這裡我們會使用官方推薦的DialogFragment來創建我的加載框,如果你不了解:請看 Android 官方推薦 : DialogFragment 創建對話框。這樣,其實給我們帶來一個很大的問題,DialogFragment說白了是Fragment,和當前的Activity的生命周期會發生綁定,我們旋轉屏幕會造成Activity的銷毀,當然也會對DialogFragment造成影響。
下面我將使用幾個例子,分別使用上面的3種方式,和如何最好的解決上述的問題。
代碼:
package com.example.zhy_handle_runtime_change; import java.util.ArrayList; import java.util.Arrays; import android.app.DialogFragment; import android.app.ListActivity; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.widget.ArrayAdapter; import android.widget.ListAdapter; /** * 不考慮加載時,進行旋轉的情況,有意的避開這種情況,後面例子會介紹解決方案 * @author zhy * */ public class SavedInstanceStateUsingActivity extends ListActivity { private static final String TAG = MainActivity; private ListAdapter mAdapter; private ArrayListmDatas; 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 ( 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 generateTimeConsumingDatas() { try { Thread.sleep(2000); } catch (InterruptedException e) { } return new ArrayList (Arrays.asList(通過Fragment保存大量數據, onSaveInstanceState保存數據, getLastNonConfigurationInstance已經被棄用, RabbitMQ, Hadoop, Spark)); } private class LoadDataAsyncTask extends AsyncTask { @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,這個不是關鍵)。
效果圖:
如果重新啟動你的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:
package com.example.zhy_handle_runtime_change; import android.app.Fragment; import android.graphics.Bitmap; import android.os.Bundle; 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; } }
然後是:FragmentRetainDataActivity
package com.example.zhy_handle_runtime_change; import android.app.Activity; import android.app.DialogFragment; import android.app.FragmentManager; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.os.Bundle; import android.util.Log; import android.widget.ImageView; import com.android.volley.RequestQueue; import com.android.volley.Response; import com.android.volley.toolbox.ImageRequest; import com.android.volley.toolbox.Volley; public class FragmentRetainDataActivity extends Activity { private static final String TAG = FragmentRetainDataActivity; private RetainedFragment dataFragment; private DialogFragment mLoadingDialog; private ImageView mImageView; private Bitmap mBitmap; @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(); } mBitmap = collectMyLoadedData(); initData(); // the data is available in dataFragment.getData() } /** * 初始化數據 */ private void initData() { mImageView = (ImageView) findViewById(R.id.id_imageView); if (mBitmap == null) { mLoadingDialog = new LoadingDialog(); mLoadingDialog.show(getFragmentManager(), LOADING_DIALOG); RequestQueue newRequestQueue = Volley .newRequestQueue(FragmentRetainDataActivity.this); ImageRequest imageRequest = new ImageRequest( http://img.my.csdn.net/uploads/201407/18/1405652589_5125.jpg, new Response.Listener() { @Override public void onResponse(Bitmap response) { mBitmap = response; mImageView.setImageBitmap(mBitmap); // load the data from the web dataFragment.setData(mBitmap); mLoadingDialog.dismiss(); } }, 0, 0, Config.RGB_565, null); newRequestQueue.add(imageRequest); } else { mImageView.setImageBitmap(mBitmap); } } @Override public void onDestroy() { Log.e(TAG, onDestroy); super.onDestroy(); // store the data in the fragment dataFragment.setData(mBitmap); } private Bitmap collectMyLoadedData() { return dataFragment.getData(); } }
這裡在onCreate總使用了Volley去加載 了一張美女照片,然後在onDestroy中對Bitmap進行存儲,在onCreate添加一個或者恢復一個Fragment的引用,然後對Bitmap進行讀取和設置。這種方式適用於比較大的數據的存儲與恢復。
注:這裡也沒有考慮加載時旋轉屏幕,問題與上面的一致。
效果圖:
在menifest中進行屬性設置:
低版本的API只需要加入orientation,而高版本的則需要加入screenSize。
ConfigChangesTestActivity
package com.example.zhy_handle_runtime_change; import java.util.ArrayList; import java.util.Arrays; import android.app.DialogFragment; import android.app.ListActivity; import android.content.res.Configuration; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.widget.ArrayAdapter; import android.widget.ListAdapter; import android.widget.Toast; /** * @author zhy * */ public class ConfigChangesTestActivity extends ListActivity { private static final String TAG = MainActivity; private ListAdapter mAdapter; private ArrayListmDatas; 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 (ConfigChangesTestActivity.this, android.R.layout.simple_list_item_1, mDatas); setListAdapter(mAdapter); } /** * 模擬耗時操作 * * @return */ private ArrayList generateTimeConsumingDatas() { try { Thread.sleep(2000); } catch (InterruptedException e) { } return new ArrayList (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 { @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,並且onConfigurationChanged中的代碼可以得到調用。從效果圖可以看到,無論如何旋轉不會重啟Activity.
效果圖:
下面要開始今天的難點了,就是處理文章開始時所說的,當異步任務在執行時,進行旋轉,如果解決上面的問題。
首先說一下探索過程:
起初,我認為此時旋轉無非是再啟動一次線程,並不會造成異常,我只要即使的在onDestroy裡面關閉上一個異步任務就可以了。事實上,如果我關閉了,上一次的對話框會一直存在;如果我不關閉,但是activity是一定會被銷毀的,對話框的dismiss也會出異常。真心很蛋疼,並且即使對話框關閉了,任務關閉了;用戶旋轉還是會造成重新創建任務,從頭開始加載數據。
下面我們希望有一種解決方案:在加載數據時旋轉屏幕,不會對加載任務進行中斷,且對用戶而言,等待框在加載完成之前都正常顯示:
當然我們還使用Fragment進行數據保存,畢竟這是官方推薦的:
OtherRetainedFragment
package com.example.zhy_handle_runtime_change; import android.app.Fragment; import android.os.Bundle; /** * 保存對象的Fragment * * @author zhy * */ 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; } }
package com.example.zhy_handle_runtime_change; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import android.os.AsyncTask; public class MyAsyncTask extends AsyncTask{ private FixProblemsActivity activity; /** * 是否完成 */ private boolean isCompleted; /** * 進度框 */ private LoadingDialog mLoadingDialog; private List items; public MyAsyncTask(FixProblemsActivity activity) { this.activity = activity; } /** * 開始時,顯示加載框 */ @Override protected void onPreExecute() { mLoadingDialog = new LoadingDialog(); 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 getItems() { return items; } private List loadingData() { try { Thread.sleep(5000); } catch (InterruptedException e) { } return new ArrayList (Arrays.asList(通過Fragment保存大量數據, onSaveInstanceState保存數據, getLastNonConfigurationInstance已經被棄用, RabbitMQ, Hadoop, Spark)); } /** * 設置Activity,因為Activity會一直變化 * * @param activity */ public void setActivity(FixProblemsActivity activity) { // 如果上一個Activity銷毀,將與上一個Activity綁定的DialogFragment銷毀 if (activity == null) { mLoadingDialog.dismiss(); } // 設置為當前的Activity this.activity = activity; // 開啟一個與當前Activity綁定的等待框 if (activity != null && !isCompleted) { mLoadingDialog = new LoadingDialog(); mLoadingDialog.show(activity.getFragmentManager(), LOADING); } // 如果完成,通知Activity if (isCompleted) { notifyActivityTaskCompleted(); } } private void notifyActivityTaskCompleted() { if (null != activity) { activity.onTaskCompleted(); } } }
主Activity:
package com.example.zhy_handle_runtime_change; import java.util.List; import android.app.FragmentManager; import android.app.ListActivity; import android.os.Bundle; import android.util.Log; import android.widget.ArrayAdapter; import android.widget.ListAdapter; public class FixProblemsActivity extends ListActivity { private static final String TAG = MainActivity; private ListAdapter mAdapter; private ListmDatas; 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) { 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 onSaveInstanceState(Bundle outState) { mMyTask.setActivity(null); super.onSaveInstanceState(outState); Log.e(TAG, onSaveInstanceState); } @Override protected void onDestroy() { Log.e(TAG, onDestroy); super.onDestroy(); } /** * 回調 */ public void onTaskCompleted() { mDatas = mMyTask.getItems(); mAdapter = new ArrayAdapter (FixProblemsActivity.this, android.R.layout.simple_list_item_1, mDatas); setListAdapter(mAdapter); } }
在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/
有任何問題,歡迎留言
源碼點擊下載
通過前面的分析,我們知道PKMS負責維護終端全部的Package信息,因此可以想到PKMS具有能力對外提供統一的Package信息查詢接口。我們以查詢匹配指定Intent
開發隨筆,小結項目開發中的得與失,項目優化工作,用到了以下幾個知識點,在這裡和大家分享一下:進展-界面、推薦邏輯優化:layout_margin、layout_heigh
MainActivity.java代碼:package siso.multilistview;import android.os.Build;import android
當一個應用在後台執行時,前台界面就不會有什麼信息,這時用戶根本不知道程序是否在執行、執行進度如何、應用程序是否遇到錯誤終止等,這時需要使用進度條來提示用戶後台程序執行的進