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

源代码参考:360云盘中---自己的学习资料---Android总结过的项目---FragmentDemo.rar

一、概述

众所周知,Activity 在不明确指定屏幕方向和 configChanges 时,当用户旋转屏幕会重新启动。当然了,应对这种情况,Android 给出了几种方案:

1、如果是少量数据,可以通过 onSaveInstanceState() 和 onRestoreInstanceState() 进行保存与恢复。
Android 会在销毁你的 Activity 之前调用 onSaveInstanceState() 方法,于是,你可以在此方法中存储关于应用状态的数据。然后你可以在 onCreate() 或onRestoreInstanceState() 方法中恢复。

2、如果是大量数据,使用 Fragment 保持需要恢复的对象。

3、自已处理配置变化。
注:getLastNonConfigurationInstance() 已经被弃用,被上述方法二替代。

--------------------------------------------------------------------------------------------
二、难点

假设当前 Activity 在 onCreate 中启动一个异步线程去加在数据,当然为了给用户一个很好的体验,会有一个 ProgressDialog,当数据加载完成,ProgressDialog 消失,设置数据。

这里,如果在异步数据完成加载之后,旋转屏幕,使用上述1、2两种方法都不会很难,无非是保存数据和恢复数据。

但是,如果正在线程加载的时候,进行旋转,会存在以下问题:

1.此时数据没有完成加载,onCreate 重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。

2.关闭 ProgressDialog 的代码在线程的 onPostExecutez 中,但是上个线程如果已经杀死,无法关闭之前 ProgressDialog。

3.谷歌的官方不建议使用 ProgressDialog,这里我们会使用官方推荐的 DialogFragment 来创建我的加载框,如果你不了解:请看 Android 官方推荐 : DialogFragment 创建对话框。这样,其实给我们带来一个很大的问题,DialogFragment 说白了是 Fragment,和当前的 Activity 的生命周期会发生绑定,我们旋转屏幕会造成 Activity 的销毁,当然也会对 DialogFragment 造成影响。
下面我将使用几个例子,分别使用上面的 3 种方式,和如何最好的解决上述的问题。

--------------------------------------------------------------------------------------------
三、使用 onSaveInstanceState() 和 onRestoreInstanceState() 进行数据保存与恢复

/**
 * 不考虑加载时,进行旋转的情况,有意的避开这种情况,后面例子会介绍解决方案
 */
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,这个不是关键)。
效果图:

--------------------------------------------------------------------------------------------
四、使用 Fragment 来保存对象,用于恢复数据

如果重新启动你的 Activity 需要恢复大量的数据,重新建立网络连接,或者执行其他的密集型操作,这样因为配置发生变化而完全重新启动可能会是一个慢的用户体验。并且,使用系统提供的 onSaveIntanceState() 的回调中,使用 Bundle 来完全恢复你 Activity 的状态是可能是不现实的(Bundle 不是设计用来携带大量数据的(例如 bitmap),并且Bundle 中的数据必须能够被序列化和反序列化),这样会消耗大量的内存和导致配置变化缓慢。在这样的情况下,当你的 Activity 因为配置发生改变而重启,你可以通过保持一个Fragment 来缓解重新启动带来的负担。这个 Fragment 可以包含你想要保持的有状态的对象的引用。

当 Android 系统因为配置变化关闭你的 Activity 的时候,你的 Activity 中被标识保持的 Fragments 不会被销毁。你可以在你的 Activity 中添加这样的 Fragements 来保存有状态的对象。


在运行时配置发生变化时,在 Fragment 中保存有状态的对象

1.继承 Fragment,声明引用指向你的有状态的对象

2.当 Fragment 创建时调用 setRetainInstance(boolean)

3.把 Fragment 实例添加到 Activity 中

4.当 Activity 重新启动后,使用 FragmentManager 对 Fragment 进行恢复

/**
 * 使用本 Fragment 保存大数据
 */
public class RetainedFragment extends Fragment {

    // data object we want to retain
    private Bitmap mData;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // retain this fragment
        setRetainInstance(true);
    }

    public Bitmap getmData() {

        return mData;
    }

    public void setmData(Bitmap mData) {

        this.mData = mData;
    }
}

比较简单,只需要声明需要保存的数据对象,然后提供 getter 和 setter,注意,一定要在 onCreate 调用 setRetainInstance(true);

/**
 * 使用 Fragment 保存大数据的主 Activity
 */
public class FragmentRetainDataActivity extends Activity {

    private static final String TAG = "FragmentRetainDataActivity";
    private RetainedFragment mDataFragment;
    private DialogFragment mLoadingDialog;
    private LoadDataAsyncTask mLoadDataAsyncTask;

    private ImageView mImageView;
    private Bitmap mBitmap;

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_fragment_retain);
        Log.e(TAG, "onCreate");

        // find the retained fragment on activity restarts
        FragmentManager fm = getFragmentManager();
        mDataFragment = (RetainedFragment) fm.findFragmentByTag("data");

        // create the fragment and data the first time
        if (mDataFragment == null) {
            // add the fragment
            mDataFragment = new RetainedFragment();
            fm.beginTransaction().add(mDataFragment, "data").commit();
        }

        mBitmap = collectMyLoadedData();

        initData();
        // the data is available in mDataFragment.getData()
    }

    @Override
    public void onDestroy() {

        Log.e(TAG, "onDestroy");
        super.onDestroy();

        // store the data in the fragment
        mDataFragment.setmData(mBitmap);
    }
    
    /**
     * 初始化数据
     */
    private void initData() {

        mImageView = (ImageView) findViewById(R.id.ivFragmentRetain);

        if (mBitmap == null) {

            mLoadingDialog = new LoadingDialog();
            mLoadingDialog.show(getFragmentManager(), "LOADING_DIALOG");
            
            mLoadDataAsyncTask = new LoadDataAsyncTask();
            mLoadDataAsyncTask.execute();
            
//            RequestQueue tRequestQueue = Volley
//                    .newRequestQueue(FragmentRetainDataActivity.this);
            
//            ImageRequest imageRequest = new ImageRequest(
//                    "http://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
//                            mDataFragment.setmData(mBitmap);
//                            mLoadingDialog.dismiss();
//                        }
//                    }, 0, 0, Config.RGB_565, null);
//            tRequestQueue.add(imageRequest);
        } else {

            mImageView.setImageBitmap(mBitmap);
        }
    }

    private Bitmap collectMyLoadedData() {

        return mDataFragment.getmData();
    }

    private class LoadDataAsyncTask extends AsyncTask<Void, Void, Void> {

        @Override
        protected Void doInBackground(Void... params) {

            mBitmap = getImg();

            return null;
        }

        @Override
        protected void onPostExecute(Void result) {

            mLoadingDialog.dismiss();

            initImg();
        }
    }

    private Bitmap getImg() {

        try {

            Thread.sleep(2000);
        } catch (InterruptedException e) {

        }
        return FileUtils.getImage();
    }

    private void initImg() {

        mImageView.setImageBitmap(mBitmap);
    }
}

--------------------------------------------------------------------------------------------
五、配置 configChanges,自己对屏幕旋转的变化进行处理

在menifest中进行属性设置:

        <activity
            android:name="com.xjl.fragmentdemo.rotate_screen.config.ConfigChangesTestActivity"
            android:configChanges="screenSize|orientation"
            android:label="配置 configChanges,自己对屏幕旋转的变化进行处理" >
            <intent-filter>
                <action android:name="fragment_demo" />
            </intent-filter>
        </activity>
低版本的 API 只需要加入 orientation,而高版本的则需要加入 screenSize。

/**
 * 配置 configChanges,自己对屏幕旋转的变化进行处理
 */
public class ConfigChangesTestActivity extends Activity {

    private static final String TAG = "MainActivity";

    private DialogFragment mLoadingDialog;
    private LoadDataAsyncTask mLoadDataAsyncTask;

    private ImageView mImageView;
    private Bitmap mBitmap;

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        Log.e(TAG, "onCreate");
        initData(savedInstanceState);
    }

    @Override
    protected void onDestroy() {

        Log.e(TAG, "onDestroy");
        super.onDestroy();
    }

    /**
     * 初始化数据
     */
    private void initData(Bundle savedInstanceState) {

        setContentView(R.layout.activity_fragment_retain);

        mImageView = (ImageView) findViewById(R.id.ivFragmentRetain);
        
        mLoadingDialog = new LoadingDialog();
        mLoadingDialog.show(getFragmentManager(), "LoadingDialog");

        mLoadDataAsyncTask = new LoadDataAsyncTask();
        mLoadDataAsyncTask.execute();

    }

    /**
     * 当配置发生变化时,不会重新启动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) {

            mBitmap = getImg();

            return null;
        }

        @Override
        protected void onPostExecute(Void result) {

            mLoadingDialog.dismiss();

            initImg();
        }
    }
    
    /**
     * 模拟耗时操作
     * 
     * @return
     */
    private Bitmap getImg() {

        try {

            Thread.sleep(2000);
        } catch (InterruptedException e) {

        }
        return FileUtils.getImage();
    }
    
    /**
     * 加载图片
     */
    private void initImg() {

        mImageView.setImageBitmap(mBitmap);
    }
}

对第一种方式的代码进行了修改,去掉了保存与恢复的代码,重写了 onConfigurationChanged;此时,无论用户何时旋转屏幕都不会重新启动 Activity,并且onConfigurationChanged 中的代码可以得到调用。从效果图可以看到,无论如何旋转不会重启 Activity.

--------------------------------------------------------------------------------------------
六、旋转屏幕的最佳实践

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

首先说一下探索过程:

起初,我认为此时旋转无非是再启动一次线程,并不会造成异常,我只要即使的在onDestroy里面关闭上一个异步任务就可以了。事实上,如果我关闭了,上一次的对话框会一直存在;如果我不关闭,但是 activity 是一定会被销毁的,对话框的 dismiss 也会出异常。真心很蛋疼,并且即使对话框关闭了,任务关闭了;用户旋转还是会造成重新创建任务,从头开始加载数据。

下面我们希望有一种解决方案:在加载数据时旋转屏幕,不会对加载任务进行中断,且对用户而言,等待框在加载完成之前都正常显示:

当然我们还使用 Fragment 进行数据保存,毕竟这是官方推荐的:

/**
 * 保存对象的 Fragment
 */
public class OtherRetainedFragment extends Fragment {

    // data object we want to retain
    // 保存一个异步的任务
    private MyAsyncTask mData;

    // this method is only called once for this fragment
    @Override
    public void onCreate(Bundle savedInstanceState) {
        
        super.onCreate(savedInstanceState);
        
        // retain this fragment
        setRetainInstance(true);
    }

    public MyAsyncTask getmData() {
        return mData;
    }

    public void setmData(MyAsyncTask mData) {
        this.mData = mData;
    }
}

和上面的差别不大,唯一不同的就是它要保存的对象编程一个异步的任务了,相信看到这,已经知道经常上述问题的一个核心了,保存一个异步任务,在重启时,继续这个任务。

public class MyAsyncTask extends AsyncTask<Void, Void, Void> {

    private FixProblemsActivity mActivity;

    /**
     * 是否完成
     */
    private boolean mBolCompleted;

    /**
     * 进度框
     */
    private LoadingDialog mLoadingDialog;
    private List<String> mItems;

    public MyAsyncTask(FixProblemsActivity mActivity) {

        this.mActivity = mActivity;
    }

    /**
     * 开始时,显示加载框
     */
    @Override
    protected void onPreExecute() {

        mLoadingDialog = new LoadingDialog();
        mLoadingDialog.show(mActivity.getFragmentManager(), "LOADING");
    }

    /**
     * 加载数据
     */
    @Override
    protected Void doInBackground(Void... params) {

        mItems = loadingData();
        return null;
    }

    /**
     * 加载完成回调当前的mActivity
     */
    @Override
    protected void onPostExecute(Void unused) {

        mBolCompleted = true;

        notifymActivityTaskCompleted();

        if (mLoadingDialog != null) {

            mLoadingDialog.dismiss();
        }
    }

    public List<String> getmItems() {

        return mItems;
    }

    private List<String> loadingData() {

        try {

            Thread.sleep(5000);
        } catch (InterruptedException e) {

        }

        return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
                "onSaveInstanceState保存数据",
                "getLastNonConfigurationInstance已经被弃用", "RabbitMQ", "Hadoop",
                "Spark"));
    }

    /**
     * 设置mActivity,因为mActivity会一直变化
     * 
     * @param mActivity
     */
    public void setmActivity(FixProblemsActivity mActivity) {

        // 如果上一个mActivity销毁,将与上一个mActivity绑定的DialogFragment销毁
        if (mActivity == null) {

            mLoadingDialog.dismiss();
        }

        // 设置为当前的mActivity
        this.mActivity = mActivity;

        // 开启一个与当前mActivity绑定的等待框
        if (mActivity != null && !mBolCompleted) {

            mLoadingDialog = new LoadingDialog();
            mLoadingDialog.show(mActivity.getFragmentManager(), "LOADING");
        }
        // 如果完成,通知mActivity
        if (mBolCompleted) {

            notifymActivityTaskCompleted();
        }
    }

    private void notifymActivityTaskCompleted() {

        if (null != mActivity) {

            mActivity.onTaskCompleted();
        }
    }
}

异步任务中,管理一个对话框,当开始下载前,进度框显示,下载结束进度框消失,并为A ctivity 提供回调。当然了,运行过程中 Activity 不断的重启,我们也提供了setActivity 方法,onDestory 时,会 setActivity(null)防止内存泄漏,同时我们也会关闭与其绑定的加载框;当 onCreate 传入新的 Activity 时,我们会在再次打开一个加载框,当然了因为屏幕的旋转并不影响加载的数据,所有后台的数据一直继续在加载。是不是很完美

/**
 * 主 Activity
 */
public class FixProblemsActivity extends ListActivity {

    private static final String TAG = "MainActivity";
    
    private OtherRetainedFragment mDataFragment;
    private MyAsyncTask mMyTask;

    private ListAdapter mAdapter;
    private List<String> mDatas;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        
        super.onCreate(savedInstanceState);

        Log.e(TAG, "onCreate");
        
        initControl(); // 加载控件
    }

    private void initControl() {

        // find the retained fragment on activity restarts
        FragmentManager tFManager = getFragmentManager();
        mDataFragment = (OtherRetainedFragment) tFManager.findFragmentByTag("data");

        // create the fragment and data the first time
        if (mDataFragment == null) {
            
            // add the fragment
            mDataFragment = new OtherRetainedFragment();
            tFManager.beginTransaction().add(mDataFragment, "data").commit();
        }
        
        mMyTask = mDataFragment.getmData();
        
        if (mMyTask != null) {
            
            mMyTask.setmActivity(this);
        } else {
            
            mMyTask = new MyAsyncTask(this);
            mDataFragment.setmData(mMyTask);
            mMyTask.execute();
        }
        // the data is available in mDataFragment.getData()
    }

    @Override
    protected void onDestroy() {

        Log.e(TAG, "onDestroy");
        super.onDestroy();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {

        super.onSaveInstanceState(outState);

        mMyTask.setmActivity(null);

        Log.e(TAG, "onSaveInstanceState");
    }

    @Override
    protected void onRestoreInstanceState(Bundle state) {

        super.onRestoreInstanceState(state);

        Log.e(TAG, "onRestoreInstanceState");
    }

    /**
     * 回调
     */
    public void onTaskCompleted() {

        mDatas = mMyTask.getmItems();

        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,是有必要拥有恢复数据的能力的
原文地址:https://www.cnblogs.com/zx-blog/p/11836376.html