Android 屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案

转载请表明出处:http://blog.csdn.net/lmj623565791/article/details/37936275

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;
/**
 * 不考虑载入时。进行旋转的情况。有意的避开这样的情况,后面样例会介绍解决方式
 * @author zhy
 *
 */
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);

	}

	/**
	 * 模拟耗时操作
	 * 
	 * @return
	 */
	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
{
	// 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;
	}
}

比較简单,仅仅须要声明须要保存的数据对象,然后提供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");

		// 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(
					"//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);
							// 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进行读取和设置。这样的方式适用于比較大的数据的存储与恢复。

注:这里也没有考虑载入时旋转屏幕,问题与上面的一致。

效果图:


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;

/**
 * @author zhy
 * 
 */
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,而且onConfigurationChanged中的代码能够得到调用。从效果图能够看到,不管怎样旋转不会重新启动Activity.

效果图:


6、旋转屏幕的最佳实践

以下要開始今天的难点了,就是处理文章開始时所说的,当异步任务在执行时,进行旋转,假设解决上面的问题。

首先说一下探索过程:

起初。我觉得此时旋转无非是再启动一次线程。并不会造成异常。我仅仅要即使的在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<Void, Void, Void>
{
	private FixProblemsActivity activity;
	/**
	 * 是否完毕
	 */
	private boolean isCompleted;
	/**
	 * 进度框
	 */
	private LoadingDialog mLoadingDialog;
	private List<String> 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<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(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提供回调。当然了,执行过程中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");

		// 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<String>(FixProblemsActivity.this,
				android.R.layout.simple_list_item_1, mDatas);
		setListAdapter(mAdapter);
	}

}

在onCreate中,假设没有开启任务(第一次进入),开启任务;假设已经开启了,调用setActivity(this);

在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/


有不论什么问题,欢迎留言 


源代码点击下载






posted @ 2017-08-16 10:52  cxchanpin  阅读(183)  评论(0编辑  收藏  举报