最佳实践:方向更改期间的AsyncTask

caw*_*caw 149 multithreading android exception

AsyncTask 在另一个线程中运行复杂任务是一件好事.

但是当AsyncTask仍在运行时有方向更改或其他配置更改时,将Activity消除并重新启动电流.并且当实例AsyncTask连接到该活动时,它会失败并导致"强制关闭"消息窗口.

所以,我正在寻找某种"最佳实践"来避免这些错误并防止AsyncTask失败.

到目前为止我看到的是:

  • 禁用方向更改.(当然不是你应该如何处理它.)
  • 让任务存活并通过新的活动实例更新它 onRetainNonConfigurationInstance
  • 只需在销毁时取消任务,ActivityActivity在再次创建时重新启动它.
  • 将任务绑定到应用程序类而不是活动实例.
  • "shelf"项目中使用的一些方法(通过onRestoreInstanceState)

一些代码示例:

屏幕旋转期间的Android AsyncTasks,第一部分第二部分

ShelvesActivity.java

你能帮我找到最好的方法来解决问题并且易于实现吗?代码本身也很重要,因为我不知道如何正确解决这个问题.

Ale*_*ood 135

千万不要使用android:configChanges来解决这个问题.这是非常糟糕的做法.

千万不要使用Activity#onRetainNonConfigurationInstance()两种.这种模块化程度较低,不适合Fragment基于应用程序.

您可以阅读我的文章,描述如何使用保留的Fragments 处理配置更改.它解决了保持AsyncTask旋转变化很好的问题.你基本上需要托管AsyncTask里面Fragment,叫setRetainInstance(true)Fragment,报告AsyncTask的进度/结果返回给它Activity通过保留Fragment.

  • 好主意,但不是每个人都使用Fragments.在Fragments是一个选项之前很久就写了很多遗留代码. (25认同)
  • @ScottBiggs片段可通过支持库一直返回到Android 1.6.您是否可以举例说明一些仍在积极使用的遗留代码,这些代码在使用支持库片段时会遇到麻烦?因为我老实说不认为这是一个问题. (13认同)
  • @tactoth我觉得不需要在我的回答中解决这些问题,因为99.9%的人不再使用`TabActivity`.说实话,我不确定为什么我们甚至在谈论这个...每个人都同意"片段"是要走的路.:) (4认同)
  • @AlexLockwood - "每个人都同意片段是可行的方式." Squared的开发者不同意! (3认同)
  • 如果必须从嵌套的片段调用AsyncTask怎么办? (2认同)

Zso*_*agy 34

我通常通过让我的AsyncTasks在.onPostExecute()回调中触发广播Intent来解决这个问题,因此他们不会修改直接启动它们的Activity.活动使用动态BroadcastReceivers监听这些广播,并采取相应措施.

这样,AsyncTasks就不必关心处理其结果的特定Activity实例.他们只是在他们完成时"大喊",如果一个活动在那个时间(活跃并且专注/处于恢复状态),这对任务的结果感兴趣,那么它将被处理.

这涉及更多的开销,因为运行时需要处理广播,但我通常不介意.我认为使用LocalBroadcastManager而不是默认的系统范围可以加快速度.

  • 这可能是解决方案的一部分,但它似乎不会解决在方向更改后重新创建AsyncTask的问题. (7认同)
  • 如果你可以在答案中添加一个例子,那会更有帮助 (6认同)
  • 如果你运气不好而且广播期间没有活动怎么办?(即你是旋转中期) (3认同)

Joh*_*ley 24

下面是AsyncTask的另一个示例,它使用a Fragment来处理运行时配置更改(如用户旋转屏幕时)setRetainInstance(true).还演示了确定(定期更新)的进度条.

该示例部分基于官方文档,在配置更改期间保留对象.

在这个例子中,需要后台线程的工作仅仅是将图像从互联网加载到UI中.

Alex Lockwood似乎是正确的,当使用"保留片段"使用AsyncTasks处理运行时配置更改时,这是最佳做法.onRetainNonConfigurationInstance()在Android Studio的Lint中被弃用.官方文档警告我们使用android:configChanges,从处理配置改变自己,...

自己处理配置更改会使使用备用资源变得更加困难,因为系统不会自动为您应用它们.当您必须避免因配置更改而重新启动时,此技术应被视为最后的手段,并且不建议用于大多数应用程序.

那么是否应该使用AsyncTask作为后台线程的问题.

AsyncTask官方参考警告......

理想情况下,AsyncTasks应该用于短操作(最多几秒钟.)如果需要保持线程长时间运行,强烈建议您使用java.util.concurrent pacakge提供的各种API,例如Executor,ThreadPoolExecutor和FutureTask.

或者,可以使用服务,加载器(使用CursorLoader或AsyncTaskLoader)或内容提供程序来执行异步操作.

我打破了帖子的其余部分:

  • 程序,流程; 和
  • 上述程序的所有代码.

程序,流程

  1. 从一个基本的AsyncTask开始,作为一个活动的内部类(它不需要是一个内部类,但它可能很方便).在此阶段,AsyncTask不处理运行时配置更改.

    public class ThreadsActivity extends ActionBarActivity {
    
        private ImageView mPictureImageView;
    
        private class LoadImageFromNetworkAsyncTask
                              extends AsyncTask<String, Void, Bitmap> {
    
            @Override
            protected Bitmap doInBackground(String... urls) {
                return loadImageFromNetwork(urls[0]);
            }
    
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                mPictureImageView.setImageBitmap(bitmap);
            }
        }
    
        /**
         * Requires in AndroidManifext.xml
         *  <uses-permission android:name="android.permission.INTERNET" />
         */
        private Bitmap loadImageFromNetwork(String url) {
            Bitmap bitmap = null;
            try {
                bitmap = BitmapFactory.decodeStream((InputStream)
                                              new URL(url).getContent());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_threads);
    
            mPictureImageView =
                (ImageView) findViewById(R.id.imageView_picture);
        }
    
        public void getPicture(View view) {
            new LoadImageFromNetworkAsyncTask()
                .execute("http://i.imgur.com/SikTbWe.jpg");
        }
    
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 添加一个嵌套类RetainedFragment,它扩展了Fragement类,但没有自己的UI.将setRetainInstance(true)添加到此Fragment的onCreate事件中.提供设置和获取数据的过程.

    public class ThreadsActivity extends Activity {
    
        private ImageView mPictureImageView;
        private RetainedFragment mRetainedFragment = null;
        ...
    
        public static class RetainedFragment extends Fragment {
    
            private Bitmap mBitmap;
    
            @Override
            public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
    
                // The key to making data survive
                // runtime configuration changes.
                setRetainInstance(true);
            }
    
            public Bitmap getData() {
                return this.mBitmap;
            }
    
            public void setData(Bitmap bitmapToRetain) {
                this.mBitmap = bitmapToRetain;
            }
        }
    
        private class LoadImageFromNetworkAsyncTask
                        extends AsyncTask<String, Integer,Bitmap> {
        ....
    
    Run Code Online (Sandbox Code Playgroud)
  3. 在最外层的Activity类的onCreate()处理RetainedFragment:如果它已经存在则引用它(如果Activity正在重新启动); 创建并添加它,如果它不存在; 然后,如果它已经存在,则从RetainedFragment获取数据并使用该数据设置UI.

    public class ThreadsActivity extends Activity {
    
        ...
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_threads);
    
            final String retainedFragmentTag = "RetainedFragmentTag";
    
            mPictureImageView =
                      (ImageView) findViewById(R.id.imageView_picture);
            mLoadingProgressBar =
                    (ProgressBar) findViewById(R.id.progressBar_loading);
    
            // Find the RetainedFragment on Activity restarts
            FragmentManager fm = getFragmentManager();
            // The RetainedFragment has no UI so we must
            // reference it with a tag.
            mRetainedFragment =
              (RetainedFragment) fm.findFragmentByTag(retainedFragmentTag);
    
            // if Retained Fragment doesn't exist create and add it.
            if (mRetainedFragment == null) {
    
                // Add the fragment
                mRetainedFragment = new RetainedFragment();
                fm.beginTransaction()
                    .add(mRetainedFragment, retainedFragmentTag).commit();
    
            // The Retained Fragment exists
            } else {
    
                mPictureImageView
                    .setImageBitmap(mRetainedFragment.getData());
            }
        }
    
    Run Code Online (Sandbox Code Playgroud)
  4. 从UI启动AsyncTask

    public void getPicture(View view) {
        new LoadImageFromNetworkAsyncTask().execute(
                "http://i.imgur.com/SikTbWe.jpg");
    }
    
    Run Code Online (Sandbox Code Playgroud)
  5. 添加并编码确定的进度条:

    • 向UI布局添加进度条;
    • 在Activity oncreate()中获取对它的引用;
    • 在流程的开始和结束时使其可见且不可见;
    • 在onProgressUpdate中定义要向UI报告的进度.
    • 将AsyncTask 2nd Generic参数从Void更改为可以处理进度更新的类型(例如Integer).
    • publishProgress在doInBackground()中的常规点.

上述程序的所有代码

活动布局.

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.mysecondapp.ThreadsActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">

        <ImageView
            android:id="@+id/imageView_picture"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:background="@android:color/black" />

        <Button
            android:id="@+id/button_get_picture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@id/imageView_picture"
            android:onClick="getPicture"
            android:text="Get Picture" />

        <Button
            android:id="@+id/button_clear_picture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@id/button_get_picture"
            android:layout_toEndOf="@id/button_get_picture"
            android:layout_toRightOf="@id/button_get_picture"
            android:onClick="clearPicture"
            android:text="Clear Picture" />

        <ProgressBar
            android:id="@+id/progressBar_loading"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/button_get_picture"
            android:progress="0"
            android:indeterminateOnly="false"
            android:visibility="invisible" />

    </RelativeLayout>
</ScrollView>
Run Code Online (Sandbox Code Playgroud)

Activity with:子类化AsyncTask内部类; 子类化RetainedFragment内部类,用于处理运行时配置更改(例如,当用户旋转屏幕时); 以及定期更新的确定进度条....

public class ThreadsActivity extends Activity {

    private ImageView mPictureImageView;
    private RetainedFragment mRetainedFragment = null;
    private ProgressBar mLoadingProgressBar;

    public static class RetainedFragment extends Fragment {

        private Bitmap mBitmap;

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

            // The key to making data survive runtime configuration changes.
            setRetainInstance(true);
        }

        public Bitmap getData() {
            return this.mBitmap;
        }

        public void setData(Bitmap bitmapToRetain) {
            this.mBitmap = bitmapToRetain;
        }
    }

    private class LoadImageFromNetworkAsyncTask extends AsyncTask<String,
            Integer, Bitmap> {

        @Override
        protected Bitmap doInBackground(String... urls) {
            // Simulate a burdensome load.
            int sleepSeconds = 4;
            for (int i = 1; i <= sleepSeconds; i++) {
                SystemClock.sleep(1000); // milliseconds
                publishProgress(i * 20); // Adjust for a scale to 100
            }

            return com.example.standardapplibrary.android.Network
                    .loadImageFromNetwork(
                    urls[0]);
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            mLoadingProgressBar.setProgress(progress[0]);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            publishProgress(100);
            mRetainedFragment.setData(bitmap);
            mPictureImageView.setImageBitmap(bitmap);
            mLoadingProgressBar.setVisibility(View.INVISIBLE);
            publishProgress(0);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_threads);

        final String retainedFragmentTag = "RetainedFragmentTag";

        mPictureImageView = (ImageView) findViewById(R.id.imageView_picture);
        mLoadingProgressBar = (ProgressBar) findViewById(R.id.progressBar_loading);

        // Find the RetainedFragment on Activity restarts
        FragmentManager fm = getFragmentManager();
        // The RetainedFragment has no UI so we must reference it with a tag.
        mRetainedFragment = (RetainedFragment) fm.findFragmentByTag(
                retainedFragmentTag);

        // if Retained Fragment doesn't exist create and add it.
        if (mRetainedFragment == null) {

            // Add the fragment
            mRetainedFragment = new RetainedFragment();
            fm.beginTransaction().add(mRetainedFragment,
                                      retainedFragmentTag).commit();

            // The Retained Fragment exists
        } else {

            mPictureImageView.setImageBitmap(mRetainedFragment.getData());
        }
    }

    public void getPicture(View view) {
        mLoadingProgressBar.setVisibility(View.VISIBLE);
        new LoadImageFromNetworkAsyncTask().execute(
                "http://i.imgur.com/SikTbWe.jpg");
    }

    public void clearPicture(View view) {
        mRetainedFragment.setData(null);
        mPictureImageView.setImageBitmap(null);
    }
}
Run Code Online (Sandbox Code Playgroud)

在这个例子中,库函数(上面引用了显式包前缀com.example.standardapplibrary.android.Network)可以实现真正的工作......

public static Bitmap loadImageFromNetwork(String url) {
    Bitmap bitmap = null;
    try {
        bitmap = BitmapFactory.decodeStream((InputStream) new URL(url)
                .getContent());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return bitmap;
}
Run Code Online (Sandbox Code Playgroud)

将后台任务所需的任何权限添加到AndroidManifest.xml ...

<manifest>
...
    <uses-permission android:name="android.permission.INTERNET" />
Run Code Online (Sandbox Code Playgroud)

将您的活动添加到AndroidManifest.xml ...

<manifest>
...
    <application>
        <activity
            android:name=".ThreadsActivity"
            android:label="@string/title_activity_threads"
            android:parentActivityName=".MainActivity">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.example.mysecondapp.MainActivity" />
        </activity>
Run Code Online (Sandbox Code Playgroud)

  • @AKh.你的意思是建议我的答案在Stackoverflow上占用太多空间吗? (2认同)