編輯:關於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 ArrayList 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(
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 ArrayList 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(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 List 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)
{
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/
有任何問題,歡迎留言
源碼點擊下載
Android7.0 PackageManagerService (4) Intent匹配Activity的過程
通過前面的分析,我們知道PKMS負責維護終端全部的Package信息,因此可以想到PKMS具有能力對外提供統一的Package信息查詢接口。我們以查詢匹配指定Intent
開發隨筆:界面、推薦邏輯優化(文末小彩蛋)
開發隨筆,小結項目開發中的得與失,項目優化工作,用到了以下幾個知識點,在這裡和大家分享一下:進展-界面、推薦邏輯優化:layout_margin、layout_heigh
安卓Andriod使用入門(九)【懸浮窗菜單】
MainActivity.java代碼:package siso.multilistview;import android.os.Build;import android
Android 七種進度條的樣式
當一個應用在後台執行時,前台界面就不會有什麼信息,這時用戶根本不知道程序是否在執行、執行進度如何、應用程序是否遇到錯誤終止等,這時需要使用進度條來提示用戶後台程序執行的進