问题较为笼统,其中有几个方面需要处理,让我们逐一来看:
“在
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阻止了某些操作,这并不意味着它没有阻止的一切都自动变得“安全”。