• Android 旋转屏幕--处理Activity与AsyncTask的最佳解决方案


    一、概述

    运行时变更就是设备在运行时发生变化(例如屏幕旋转、键盘可用性及语言)。发生这些变化,Android会重启Activity,这时就需要保存activity的状态及与activity相关的任务,以便恢复activity的状态。

    为此,google提供了三种解决方案:

    1. 对于少量数据: 通过onSaveInstanceState(),保存有关应用状态的数据。 然后在 onCreate() 或 onRestoreInstanceState() 期间恢复 Activity 状态。
    2. 对于大量数据:用 Fragment 保留需要回复的对象。
    3. 自行处理配置变更,不重启Activity。

    下面会逐一介绍三种情况,其实保存一些变量对象很简单,难的是当Activity创建异步线程去加载数据时,旋转屏幕时,怎么保存线程的状态。比如,在线程的加载过程中,旋转屏幕,就会存在问题:此时数据没有完成加载,onCreate重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。下面会一一解决这些问题。本文较长,主要是代码多,可以先下载demo,源码下载:http://download.csdn.net/detail/jycboy/9720486对比着看

     二、使用onSaveInstanceState,onRestoreInstanceState​

    代码如下:

    /**
     * 使用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,在这里这个不是关键)。

    效果图:

     

    三、使用Fragment保留对象,恢复数据

    果重启 Activity 需要恢复大量数据、重新建立网络连接或执行其他密集操作,依靠系统通过onSaveInstanceState() 回调为您保存的 Bundle,可能无法完全恢复 Activity 状态,因为它并非设计用于携带大型对象(例如位图),而且其中的数据必须先序列化,再进行反序列化,这可能会消耗大量内存并使得配置变更速度缓慢。 在这种情况下,如果 Activity 因配置变更而重启,则可通过保留 Fragment 来减轻重新初始化 Activity 的负担。此片段可能包含对您要保留的有状态对象的引用。

    当 Android 系统因配置变更而关闭 Activity 时,不会销毁您已标记为要保留的 Activity 的片段。 您可以将此类片段添加到 Activity 以保留有状态的对象。

    要在运行时配置变更期间将有状态的对象保留在片段中,请执行以下操作:

    1. 扩展 Fragment 类并声明对有状态对象的引用。
    2. 在创建片段后调用 setRetainInstance(boolean)
    3. 将片段添加到 Activity。
    4. 重启 Activity 后,使用 FragmentManager 检索片段。

    例如,按如下方式定义片段:

    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 绑定的对象,例如,DrawableAdapterView 或其他任何与 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 boolean isCompleted;
    	/**
    	 * 进度框
    	 */
    	private LoadingDialog mLoadingDialog;
    	private List<String> items;
    
    	/**
    	 * 开始时,显示加载框
    	 */
    	@Override
    	protected void onPreExecute()
    	{
    		mLoadingDialog = new LoadingDialog();
    		FixProblemsActivity 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)
    	{
    		if(activity == null){
    			mLoadingDialog.dismiss();
    			return;
    		}
    		weakReference = new WeakReference<>(activity);
    		// 设置为当前的Activity
    		FixProblemsActivity fActivity = (FixProblemsActivity) weakReference.get();
    		// 开启一个与当前Activity绑定的等待框
    		if (activity != null && !isCompleted)
    		{
    			mLoadingDialog = new LoadingDialog();
    			mLoadingDialog.show(fActivity.getFragmentManager(), "LOADING");
    		}
    		// 如果完成,通知Activity
    		if (isCompleted)
    		{
    			notifyActivityTaskCompleted();
    		}
    	}
    
    	/**
    	 * 在Activity不可见时,关闭dialog
    	 */
        public void dialogDismiss(){
    		if(mLoadingDialog != null){
    			mLoadingDialog.dismiss();
    		}
    	}
    	private void notifyActivityTaskCompleted()
    	{
    		if (null != weakReference.get())
    		{
    			((FixProblemsActivity) weakReference.get()).onTaskCompleted();
    		}
    	}
    
    }

    主要在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 

  • 相关阅读:
    Excel:大小写转换
    Qt Quick开发环境搭建
    如何集成QML与C++?
    第十八章:QML扩展
    如何编写整洁的Qml代码
    Linux环境,4个版本的微信对比
    Oracle 按照 IN的顺序排序结果集
    __VA_ARGS__的用法
    手写Web图片懒加载~
    Selenuim+Python之元素定位总结及实例说明
  • 原文地址:https://www.cnblogs.com/jycboy/p/save_state_data.html
Copyright © 2020-2023  润新知