1、概述
衆所周知,Activity在不明確指定屏幕方向和configChanges時,當用戶旋轉屏幕會重新啓動。當然了,應對這種情況,Android給出了幾種方案:
a、如果是少量數據,可以通過onSaveInstanceState()和onRestoreInstanceState()進行保存與恢復。
Android會在銷燬你的Activity之前調用onSaveInstanceState()方法,於是,你可以在此方法中存儲關於應用狀態的數據。然後你可以在onCreate()或onRestoreInstanceState()方法中恢復。
b、如果是大量數據,使用Fragment保持需要恢復的對象。
c、自已處理配置變化。
注:getLastNonConfigurationInstance()已經被棄用,被上述方法二替代。
2、難點
假設當前Activity在onCreate中啓動一個異步線程去夾在數據,當然爲了給用戶一個很好的體驗,會有一個ProgressDialog,當數據加載完成,ProgressDialog消失,設置數據。
這裏,如果在異步數據完成加載之後,旋轉屏幕,使用上述a、b兩種方法都不會很難,無非是保存數據和恢復數據。
但是,如果正在線程加載的時候,進行旋轉,會存在以下問題:
a)此時數據沒有完成加載,onCreate重新啓動時,會再次啓動線程;而上個線程可能還在運行,並且可能會更新已經不存在的控件,造成錯誤。
b)關閉ProgressDialog的代碼在線程的onPostExecutez中,但是上個線程如果已經殺死,無法關閉之前ProgressDialog。
c)谷歌的官方不建議使用ProgressDialog,這裏我們會使用官方推薦的DialogFragment來創建我的加載框,如果你不瞭解:請看 Android
官方推薦 : DialogFragment 創建對話框。這樣,其實給我們帶來一個很大的問題,DialogFragment說白了是Fragment,和當前的Activity的生命週期會發生綁定,我們旋轉屏幕會造成Activity的銷燬,當然也會對DialogFragment造成影響。
下面我將使用幾個例子,分別使用上面的3種方式,和如何最好的解決上述的問題。
3、使用onSaveInstanceState()和onRestoreInstanceState()進行數據保存與恢復
代碼:
-
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;
-
-
-
-
-
-
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);
-
-
}
-
-
-
-
-
-
-
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,這個不是關鍵)。
效果圖:
4、使用Fragment來保存對象,用於恢復數據
如果重新啓動你的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
-
{
-
-
private Bitmap data;
-
-
@Override
-
public void onCreate(Bundle savedInstanceState)
-
{
-
super.onCreate(savedInstanceState);
-
-
setRetainInstance(true);
-
}
-
-
public void setData(Bitmap data)
-
{
-
this.data = data;
-
}
-
-
public Bitmap getData()
-
{
-
return data;
-
}
-
}
比較簡單,只需要聲明需要保存的數據對象,然後提供getter和setter,注意,一定要在onCreate調用setRetainInstance(true);
然後是: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");
-
-
-
FragmentManager fm = getFragmentManager();
-
dataFragment = (RetainedFragment) fm.findFragmentByTag("data");
-
-
if (dataFragment == null)
-
{
-
-
dataFragment = new RetainedFragment();
-
fm.beginTransaction().add(dataFragment, "data").commit();
-
}
-
mBitmap = collectMyLoadedData();
-
initData();
-
-
-
}
-
-
-
-
-
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(
-
"https://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);
-
-
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();
-
-
dataFragment.setData(mBitmap);
-
}
-
-
private Bitmap collectMyLoadedData()
-
{
-
return dataFragment.getData();
-
}
-
-
}
這裏在onCreate總使用了Volley去加載 了一張美女照片,然後在onDestroy中對Bitmap進行存儲,在onCreate添加一個或者恢復一個Fragment的引用,然後對Bitmap進行讀取和設置。這種方式適用於比較大的數據的存儲與恢復。
注:這裏也沒有考慮加載時旋轉屏幕,問題與上面的一致。
效果圖:
5、配置configChanges,自己對屏幕旋轉的變化進行處理
在menifest中進行屬性設置:
-
<activity
-
android:name=".ConfigChangesTestActivity"
-
android:configChanges="screenSize|orientation" >
-
</activity>
低版本的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;
-
-
-
-
-
-
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);
-
}
-
-
-
-
-
-
-
private ArrayList<String> generateTimeConsumingDatas()
-
{
-
try
-
{
-
Thread.sleep(2000);
-
} catch (InterruptedException e)
-
{
-
}
-
return new ArrayList<String>(Arrays.asList("通過Fragment保存大量數據",
-
"onSaveInstanceState保存數據",
-
"getLastNonConfigurationInstance已經被棄用", "RabbitMQ", "Hadoop",
-
"Spark"));
-
}
-
-
-
-
-
@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,並且onConfigurationChanged中的代碼可以得到調用。從效果圖可以看到,無論如何旋轉不會重啓Activity.
效果圖:
6、旋轉屏幕的最佳實踐
下面要開始今天的難點了,就是處理文章開始時所說的,當異步任務在執行時,進行旋轉,如果解決上面的問題。
首先說一下探索過程:
起初,我認爲此時旋轉無非是再啓動一次線程,並不會造成異常,我只要即使的在onDestroy裏面關閉上一個異步任務就可以了。事實上,如果我關閉了,上一次的對話框會一直存在;如果我不關閉,但是activity是一定會被銷燬的,對話框的dismiss也會出異常。真心很蛋疼,並且即使對話框關閉了,任務關閉了;用戶旋轉還是會造成重新創建任務,從頭開始加載數據。
下面我們希望有一種解決方案:在加載數據時旋轉屏幕,不會對加載任務進行中斷,且對用戶而言,等待框在加載完成之前都正常顯示:
當然我們還使用Fragment進行數據保存,畢竟這是官方推薦的:
OtherRetainedFragment
-
package com.example.zhy_handle_runtime_change;
-
-
import android.app.Fragment;
-
import android.os.Bundle;
-
-
-
-
-
-
-
-
public class OtherRetainedFragment extends Fragment
-
{
-
-
-
-
private MyAsyncTask data;
-
-
-
@Override
-
public void onCreate(Bundle savedInstanceState)
-
{
-
super.onCreate(savedInstanceState);
-
-
setRetainInstance(true);
-
}
-
-
public void setData(MyAsyncTask data)
-
{
-
this.data = data;
-
}
-
-
public MyAsyncTask getData()
-
{
-
return data;
-
}
-
-
-
}
和上面的差別不大,唯一不同的就是它要保存的對象編程一個異步的任務了,相信看到這,已經知道經常上述問題的一個核心了,保存一個異步任務,在重啓時,繼續這個任務。
異步任務中,管理一個對話框,當開始下載前,進度框顯示,下載結束進度框消失,併爲Activity提供回調。當然了,運行過程中Activity不斷的重啓,我們也提供了setActivity方法,onDestory時,會setActivity(null)防止內存泄漏,同時我們也會關閉與其綁定的加載框;當onCreate傳入新的Activity時,我們會在再次打開一個加載框,當然了因爲屏幕的旋轉並不影響加載的數據,所有後臺的數據一直繼續在加載。是不是很完美~~
主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<String> mDatas;
-
private OtherRetainedFragment dataFragment;
-
private MyAsyncTask mMyTask;
-
-
@Override
-
public void onCreate(Bundle savedInstanceState)
-
{
-
super.onCreate(savedInstanceState);
-
Log.e(TAG, "onCreate");
-
-
-
FragmentManager fm = getFragmentManager();
-
dataFragment = (OtherRetainedFragment) fm.findFragmentByTag("data");
-
-
-
if (dataFragment == null)
-
{
-
-
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();
-
}
-
-
}
-
-
-
@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<String>(FixProblemsActivity.this,
-
android.R.layout.simple_list_item_1, mDatas);
-
setListAdapter(mAdapter);
-
}
-
-
}
在onCreate中,如果沒有開啓任務(第一次進入),開啓任務;如果已經開啓了,調用setActivity(this);
在onSaveInstanceState把當前任務加入Fragment
我設置了等待5秒,足夠旋轉三四個來回了~~~~可以看到雖然在不斷的重啓,但是絲毫不影響加載數據任務的運行和加載框的顯示~~~~
效果圖:
可以看到我在加載的時候就三心病狂的旋轉屏幕~~但是絲毫不影響顯示效果與任務的加載~~
最後,說明一下,其實不僅是屏幕旋轉需要保存數據,當用戶在使用你的app時,忽然接到一個來電,長時間沒有回到你的app界面也會造成Activity的銷燬與重建,所以一個行爲良好的App,是有必要擁有恢復數據的能力的~~。