zoukankan      html  css  js  c++  java
  • 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,是有必要拥有恢复数据的能力的
  • 相关阅读:
    队列分类梳理
    停止线程
    Docker和Kubernetes
    Future、Callback、Promise
    Static、Final、static final
    线程池梳理
    TCP四次挥手
    http1.0、http1.x、http 2和https梳理
    重排序
    java内存模型梳理
  • 原文地址:https://www.cnblogs.com/zx-blog/p/11836376.html
Copyright © 2011-2022 走看看