Android: 你能在DoInBackground中访问AsyncTask类成员吗?

3
DoInBackground中运行AsyncTask类成员的方法是否安全?还是需要使用处理程序?
private class MyAsyncTask extends AsyncTask<Void, Void, Void> {

    Object mObjA = null:

    private MyAsyncTask(Object objA) {
        mObjA = objA
    }

    protected void doInBackground(Void... params) {
        mObjA.aMethod()
    }

}

doInBackground方法中,如果mObjA没有作为参数传递,运行mObjA.aMethod()是否安全?是否存在多线程问题?

doInBackground方法中,应该只访问已经明确传递的对象,还是可以自由访问类的任何成员而不使用处理程序?


只需验证您的代码。简单明了。 - Петър Петров
代码可以运行,但我想知道它是否是多线程正确的,而不使用处理程序。 - Daniele B
它会抛出NullPointerException异常。所以我猜这不好 :) - Петър Петров
不,它不会抛出NullPointerException。 - Daniele B
@DanieleB:你看到我的回答了吗? - Yash Sampat
嗨@ZygoteInit,是的,我已经阅读了它,谢谢。你和ci_基本上在说同样的事情。除了避免UI对象之外,ci_还指出,不安全的对象也可能会有问题。 - Daniele B
3个回答

3

您确实可以在 doInBackground() 中访问您的 AsyncTask 的任何字段,前提是 mObjA 不是像 ViewPagerLinearLayout 或任何类型的 ViewGroup 等与 UI 相关的 Android 类。


UI类是最常见的罪魁祸首,我同意。然而,原帖的作者并没有指明具体是哪个类,所以可能是任何类,因此你不能笼统地说这样做是安全的,这取决于具体的类。 - ci_
@DanieleB:你在这方面取得了什么进展? - Yash Sampat
嗨 @ZygoteInit,是的我已经读过了,谢谢。你和ci_基本上是在说同样的事情。ci_还补充说,非线程安全的对象也可能会有问题。 - Daniele B

2
问题较为笼统,其中有几个方面需要处理,让我们逐一来看:
“在doInBackground中运行AsyncTask类成员的任何方法[未进一步指定Object]是否安全?”
安全是指什么?
“是否存在多线程问题?”
好的,所以从多线程的角度来看,它是否安全?答案通常是否定的,除非您“已经知道”特定对象上的特定方法可以安全地从多个线程调用。换句话说,仅将某些内容放入AsyncTask中,并不比使用任何其他Thread更安全。
例如:
public class MainActivity extends Activity {

    private void testLoop(String logTag, SimpleDateFormat sdf, String inputString) {
        Log.d(logTag, "Starting...");
        for (int i = 0; i < 100; i++) {
            try {
                String outputString;
                outputString = sdf.format(sdf.parse(inputString));
                if (!outputString.equals(inputString)) {
                    Log.d(logTag, "i: " + i + " inputString: " + inputString + " outputString: " + outputString);
                    break;
                }
            } catch (Exception e) {
                Log.d(logTag, "Boom! i: " + i, e);
                break;
            }
        }
        Log.d(logTag, "Done!");
    }

    public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        SimpleDateFormat sdf;
        public MyAsyncTask(SimpleDateFormat sdf) {
            this.sdf = sdf;
        }
        @Override
        protected Void doInBackground(Void... params) {
            testLoop("MyAsyncTask", sdf, "2014-12-24 12:34:56.789");
            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        new MyAsyncTask(sdf).execute();
        testLoop("MainActivity", sdf, "2015-04-01 23:23:23.232");
    }
}

输出:

08:52:37.462: D/MainActivity(13051): 开始...
08:52:37.466: D/MyAsyncTask(13051): 开始...
08:52:37.466: D/MainActivity(13051): i: 3 inputString: 2015-04-01 23:23:23.232 outputString: 2014-12-01 0012:34:23.789
08:52:37.467: D/MainActivity(13051): 完成!
08:52:37.467: D/MyAsyncTask(13051): i: 0 inputString: 2014-12-24 12:34:56.789 outputString: 2014-12-01 12:34:23.789
08:52:37.467: D/MyAsyncTask(13051): 完成!

哦,发生了一些“奇怪”的事情。

让我们再次运行它:

08:53:44.551: D/MainActivity(13286): 开始...
08:53:44.562: D/MyAsyncTask(13286): 开始...
08:53:44.563: D/MainActivity(13286): i: 11 inputString: 2015-04-01 23:23:23.232 outputString: 1970-01-24 12:00:23.232
08:53:44.563: D/MainActivity(13286): 完成!
08:53:44.567: D/MyAsyncTask(13286): i: 0 inputString: 2014-12-24 12:34:56.789 outputString: 2014-01-24 12:34:56.789
08:53:44.567: D/MyAsyncTask(13286): 完成!

仍然很奇怪,但是不同了。

让我们再运行一次:

08:54:23.560: D/MainActivity(13286): 开始...
08:54:23.579: D/MyAsyncTask(13286): 开始...
08:54:23.596: D/MainActivity(13286): i: 3 inputString: 2015-04-01 23:23:23.232 outputString: 2015-01-01 00:00:00.000
08:54:23.596: D/MainActivity(13286): 完成!
08:54:24.423: D/MyAsyncTask(13286): 完成!

这次不同,甚至没有在其中一个线程中表现出来。

明显是多线程问题。我们该如何解决?

从问题来看:“在doInBackground中,你是否只能访问已经被明确传递的对象”?

那么,我们试着改一下代码:

public class MyAsyncTask extends AsyncTask<SimpleDateFormat, Void, Void> {
    @Override
    protected Void doInBackground(SimpleDateFormat... params) {
        testLoop("MyAsyncTask", params[0], "2014-12-24 12:34:56.789");
        return null;
    }
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    new MyAsyncTask().execute(sdf);
    testLoop("MainActivity", sdf, "2015-04-01 23:23:23.232");
}

当我们运行这个代码时会发生什么呢?
09:11:43.734: D/MainActivity(15881): Starting... 09:11:43.763: D/MyAsyncTask(15881): Starting... 09:11:43.782: D/MainActivity(15881): i: 7 inputString: 2015-04-01 23:23:23.232 outputString: 2015-004-01 00:00:00.789 09:11:43.782: D/MainActivity(15881): 完成! 09:11:43.783: D/MyAsyncTask(15881): i: 5 inputString: 2014-12-24 12:34:56.789 outputString: 2014-012-24 12:34:56.789 09:11:43.783: D/MyAsyncTask(15881): 完成!
哇,还是很奇怪。看起来`AsyncTask`甚至不能保护我们充当参数传递的对象。
那么它能保护我们免受什么伤害呢?文档中写道:
“AsyncTask确保所有回调调用都是同步的,以使以下操作在没有显式同步的情况下安全: - 在构造函数或onPreExecute()中设置成员字段,并在doInBackground(Params...)中引用它们。 - 在doInBackground(Params...)中设置成员字段,并在onProgressUpdate(Progress...)和onPostExecute(Result)中引用它们。”
这就是它所做的全部内容,没有空白支票。
那么我们该如何解决呢?
有很多选择。在这个特定的例子中,显然的选择是不共享`sdf`,而是在`AsyncTask`中创建一个新实例。如果可能的话,这通常是一个好的选择。如果不行,开发人员必须确保访问得到了同步。对于我们的例子,您可以使用synchronized语句:
synchronized (sdf) {
    outputString = sdf.format(sdf.parse(inputString));
}

08:56:59.370: D/MainActivity(13876): 开始...
08:56:59.375: D/MyAsyncTask(13876): 开始...
08:57:00.287: D/MainActivity(13876): 完成!
08:57:01.216: D/MyAsyncTask(13876): 完成!

耶!终于成功了!

你也可以将整个方法同步化:

private synchronized void testLoop(String logTag, SimpleDateFormat sdf, String inputString) {
    // ...
}

08:59:11.237: D/MainActivity(14361): 开始...
08:59:12.036: D/MainActivity(14361): 完成!
08:59:12.036: D/MyAsyncTask(14361): 开始...
08:59:12.862: D/MyAsyncTask(14361): 完成!

仍然可以工作,但现在您基本上已经将我们希望使用多个线程并行执行的所有工作串行化了。虽然不是不安全的,但性能不佳。这是多线程编程的另一个陷阱,但与问题无关。(实际上,上面的同步语句版本在性能方面也没有太大改善,因为在我们的示例中我们实际上并没有做其他任何事情)。

那么处理程序呢?

从问题中得知:“你需要使用处理程序吗?”

让我们修改示例以使用 Handler

public class MyAsyncTask extends AsyncTask<Handler, Void, Void> {
    SimpleDateFormat sdf;
    public MyAsyncTask(SimpleDateFormat sdf) {
        this.sdf = sdf;
    }
    @Override
    protected Void doInBackground(Handler... params) {
        params[0].post(new Runnable() {
            @Override
            public void run() {
                testLoop("MyAsyncTask", sdf, "2014-12-24 12:34:56.789");
            }
        });

        return null;
    }
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    new MyAsyncTask(sdf).execute(new Handler());
    testLoop("MainActivity3", sdf, "2015-04-01 23:23:23.232");
}

10:36:15.899: D/MainActivity(17932): 开始...
10:36:16.028: D/MainActivity(17932): 完成!
10:36:16.038: D/MyAsyncTask(17932): 开始...
10:36:16.115: D/MyAsyncTask(17932): 完成!

在这种情况下,Handler同样适用(尽管示例现在看起来非常糟糕),因为它通过在另一个线程上运行代码,从根本上消除了特定代码的多线程问题。这也意味着如果您需要立即在AsyncTask中使用该代码的结果,则此方法实际上不起作用。

那么不要在doInBackground()中使用UI元素是什么意思?

新的示例:

public class MainActivity extends Activity {
    private TextView tv;

    public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        TextView tv;
        public MyAsyncTask(TextView tv) {
            this.tv = tv;
        }
        @Override
        protected Void doInBackground(Void... params) {
            tv.setText("Boom!");
            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        tv = new TextView(this);
        tv.setText("Hello world!");
        Button button = new Button(this);
        button.setText("Click!");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new MyAsyncTask(tv).execute();
            }
        });
        layout.addView(tv);
        layout.addView(button);
        setContentView(layout);
    }
}

运行此代码,点击按钮后,您的应用程序将停止,并在logcat中找到以下堆栈跟踪:

11:21:36.630:E/AndroidRuntime(23922):致命异常:AsyncTask#1
11:21:36.630:E/AndroidRuntime(23922):进程:com.example.testsothreadsafe,PID:23922
11:21:36.630:E/AndroidRuntime(23922):执行doInBackground()时发生错误
11:21:36.630:E/AndroidRuntime(23922):在android.os.AsyncTask $ 3.done(AsyncTask.java:304)
11:21:36.630:E/AndroidRuntime(23922):在java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
11:21:36.630:E/AndroidRuntime(23922):在java.util.concurrent.FutureTask.setException(FutureTask.java:222)
11:21:36.630:E/AndroidRuntime(23922):在java.util.concurrent.FutureTask.run(FutureTask.java:242)
11:21:36.630:E/AndroidRuntime(23922):在android.os.AsyncTask $ SerialExecutor $ 1.run(AsyncTask.java:231)
11:21:36.630:E/AndroidRuntime(23922):在java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
11:21:36.630:E/AndroidRuntime(23922):在java.util.concurrent.ThreadPoolExecutor $ Worker.run(ThreadPoolExecutor.java:587)
11:21:36.630:E/AndroidRuntime(23922):在java.lang.Thread.run(Thread.java:818)
11:21:36.630:E/AndroidRuntime(23922):由于android.view.ViewRootImpl $ CalledFromWrongThreadException而引起的错误:只有创建视图层次结构的原始线程才能触摸其视图。
11:21:36.630:E/AndroidRuntime(23922):在android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
11:21:36.630:E/AndroidRuntime(23922):在android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:874)
11:21:36.630:E/AndroidRuntime(23922):在android.view.View.requestLayout(View.java:17476)
11:21:36.630:E/AndroidRuntime(23922):在android.view.View.requestLayout(View.java:17476)
11:21:36.630:E/AndroidRuntime(23922):在android.view.View.requestLayout(View.java:17476)
11:21:36.630:E/AndroidRuntime(23922):在android.view.View.requestLayout(View.java:17476)
11:21:36.630:E/AndroidRuntime(23922):在android.view.View.requestLayout(View.java:17476)
11:21:36.630:E/AndroidRuntime(23922):在android.widget.TextView.checkForRelayout(TextView.java:6871)
11:21:36.630:E/AndroidRuntime(23922):在android.widget.TextView.setText(TextView.java:4057)
11:21:36.630:E/AndroidRuntime(23922):在android.widget.TextView.setText(TextView.java:3915)
11:21:36.630:E/AndroidRuntime(23922):在android.widget.TextView.setText(TextView.java:3890)
11:21:36.630:E/AndroidRuntime(23922):在com.example.testsothreadsafe.MainActivity $ MyAsyncTask.doInBackground(MainActivity.java:22)
11:21:36.630:E/AndroidRuntime(23922):在com.example.testsothreadsafe.MainActivity $ MyAsyncTask.doInBackground(MainActivity.java:1)
11:21:36.630:E/AndroidRuntime(23922):在android.os.AsyncTask $ 2.call(AsyncTask.java:292)
11:21:36.630:E/AndroidRuntime(23922):在java.util.concurrent.FutureTask.run(FutureTask.java:237)
11:21:36.630:E/AndroidRuntime(23922):... 4个更多

Android特意防止您从非创建它的线程中操纵视图层次结构。但是否禁止任何访问

将示例更改为:

@Override
protected Void doInBackground(Void... params) {
    Log.d("MyAsyncTask", tv.getText().toString());
    return null;
}

点击按钮,输出结果为:

11:25:20.950: D/MyAsyncTask(24329): Hello world!

看起来,有一些访问是允许的。

那么这里的信息是什么呢?多线程编程很棘手。仅仅因为Android阻止了某些操作,这并不意味着它没有阻止的一切都自动变得“安全”。


实际上,mObjA被多个线程访问。 - Daniele B
那就取决于mObjA本身是否是线程安全的,这个问题不能一概而论地回答,例如SimpleDateFormat就不是线程安全的(很多人不知道这一点)。 - ci_

0

AsyncTask是线程安全的。但我想只使用AsyncTask的通用性可能更方便。这样你就可以仅仅使用匿名内部类。

new AsyncTask<Object, Void, Void>() {

    protected Void doInBackground(Object... params){
        params[0].aMethod();
        return null;
    }
}.execute(obj);

但是就像其他人所说的那样,你传递的对象可能不是线程安全的。


我只是在提到AsyncTask本身。我更新了我的答案以反映这一点。 - P-a Bäckström

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接