问题
Is it safe to run a method of an AsyncTask
class member inside the DoInBackground
? or do you need to use a handler?
private class MyAsyncTask extends AsyncTask<Void, Void, Void> {
Object mObjA = null:
private MyAsyncTask(Object objA) {
mObjA = objA
}
protected void doInBackground(Void... params) {
mObjA.aMethod()
}
}
in doInBackground
is it safe to run mObjA.aMethod()
if mObjA
wasn't passed as a parameter? is there a multithreading issue?
should you in doInBackground
only access objects which have been deliberately passed, or can you freely access any member of the class without using handlers?
回答1:
You can indeed access any field of your AsyncTask
inside doInBackground()
, with the condition that mObjA
is not a UI-related Android class like ViewPager
or LinearLayout
or any sort of ViewGroup
basically.
回答2:
The question is rather generic and there are a few parts to it, let's deal with them one at a time:
"Is it safe to run a [any] method of an AsyncTask
class member [not further specified Object
] inside doInBackground
?"
Is it safe with regards to what?
"Is there a multithreading issue?"
Ok, so is it safe with regards to multithreading? The answer is no, it is not generally safe, unless you know this particular method on this particular object is safe to call from multiple threads. In other words, just by putting something into an AsyncTask
doesn't make it safer than using any other Thread
.
Example:
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");
}
}
Output:
08:52:37.462: D/MainActivity(13051): Starting...
08:52:37.466: D/MyAsyncTask(13051): Starting...
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): Done!
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): Done!
Oh, something "weird" happened.
Let's run it again:
08:53:44.551: D/MainActivity(13286): Starting...
08:53:44.562: D/MyAsyncTask(13286): Starting...
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): Done!
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): Done!
Still weird, but different.
Let's run it one more time:
08:54:23.560: D/MainActivity(13286): Starting...
08:54:23.579: D/MyAsyncTask(13286): Starting...
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): Done!
08:54:24.423: D/MyAsyncTask(13286): Done!
Different again, this time it didn't even manifest itself in one of the threads.
Definitely a multithreading issue. How do we fix it?
From the question: "should you in doInBackground only access objects which have been deliberately passed"?
Well, let's try that, change the code around a bit:
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");
}
What happens when we run this?
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): Done!
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): Done!
Wow, still weird. So it looks like the AsyncTask
doesn't even protect us if we pass our Object
as a parameter.
So what does it protect us from? The docs state:
AsyncTask guarantees that all callback calls are synchronized in such a way that the following operations are safe without explicit synchronizations.
- Set member fields in the constructor or onPreExecute(), and refer to them in doInBackground(Params...).
- Set member fields in doInBackground(Params...), and refer to them in onProgressUpdate(Progress...) and onPostExecute(Result).
That's all it does, no blank cheque.
So how can we fix it then?
There are a number of options. In this particular contrived example, the obvious choice is to not share sdf
but create a new instance in the AsyncTask
. That's usually a good option if possible. If not, you (the developer) have to make sure that access is somehow synchronized. For our example you could use a synchronized statement:
synchronized (sdf) {
outputString = sdf.format(sdf.parse(inputString));
}
08:56:59.370: D/MainActivity(13876): Starting...
08:56:59.375: D/MyAsyncTask(13876): Starting...
08:57:00.287: D/MainActivity(13876): Done!
08:57:01.216: D/MyAsyncTask(13876): Done!
Yay! Finally!
You could also synchronize the whole method:
private synchronized void testLoop(String logTag, SimpleDateFormat sdf, String inputString) {
// ...
}
08:59:11.237: D/MainActivity(14361): Starting...
08:59:12.036: D/MainActivity(14361): Done!
08:59:12.036: D/MyAsyncTask(14361): Starting...
08:59:12.862: D/MyAsyncTask(14361): Done!
Still works, but now you have basically serialised all the work that we hoped to do in parallel with multiple threads. Not unsafe, but not good performance. Another pitfall of multithreaded programming, but not related to the question. (In fact the synchronized statement version above isn't a whole lot better performance wise, since we're not really doing anything else in our example anyway).
What about handlers?
From the question: "do you need to use a handler?"
Let's modify the example to use a 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): Starting...
10:36:16.028: D/MainActivity(17932): Done!
10:36:16.038: D/MyAsyncTask(17932): Starting...
10:36:16.115: D/MyAsyncTask(17932): Done!
A Handler
would work as well in this case (although the example starts to look very poor now), because it essentially removes multithreading issues for a particular piece of code by running it on the other thread altogether. That also means that this won't really work if you need to use a result of that piece of code in your AsyncTask
immediately.
So what's all this talk about not using UI elements in doInBackground()
?
New example:
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);
}
}
Run this, click the button, you're app will stop and you'll find the following stack trace in logcat:
11:21:36.630: E/AndroidRuntime(23922): FATAL EXCEPTION: AsyncTask #1
11:21:36.630: E/AndroidRuntime(23922): Process: com.example.testsothreadsafe, PID: 23922
11:21:36.630: E/AndroidRuntime(23922): java.lang.RuntimeException: An error occured while executing doInBackground()
11:21:36.630: E/AndroidRuntime(23922): at android.os.AsyncTask$3.done(AsyncTask.java:304)
11:21:36.630: E/AndroidRuntime(23922): at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
11:21:36.630: E/AndroidRuntime(23922): at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
11:21:36.630: E/AndroidRuntime(23922): at java.util.concurrent.FutureTask.run(FutureTask.java:242)
11:21:36.630: E/AndroidRuntime(23922): at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
11:21:36.630: E/AndroidRuntime(23922): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
11:21:36.630: E/AndroidRuntime(23922): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
11:21:36.630: E/AndroidRuntime(23922): at java.lang.Thread.run(Thread.java:818)
11:21:36.630: E/AndroidRuntime(23922): Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
11:21:36.630: E/AndroidRuntime(23922): at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
11:21:36.630: E/AndroidRuntime(23922): at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:874)
11:21:36.630: E/AndroidRuntime(23922): at android.view.View.requestLayout(View.java:17476)
11:21:36.630: E/AndroidRuntime(23922): at android.view.View.requestLayout(View.java:17476)
11:21:36.630: E/AndroidRuntime(23922): at android.view.View.requestLayout(View.java:17476)
11:21:36.630: E/AndroidRuntime(23922): at android.view.View.requestLayout(View.java:17476)
11:21:36.630: E/AndroidRuntime(23922): at android.view.View.requestLayout(View.java:17476)
11:21:36.630: E/AndroidRuntime(23922): at android.widget.TextView.checkForRelayout(TextView.java:6871)
11:21:36.630: E/AndroidRuntime(23922): at android.widget.TextView.setText(TextView.java:4057)
11:21:36.630: E/AndroidRuntime(23922): at android.widget.TextView.setText(TextView.java:3915)
11:21:36.630: E/AndroidRuntime(23922): at android.widget.TextView.setText(TextView.java:3890)
11:21:36.630: E/AndroidRuntime(23922): at com.example.testsothreadsafe.MainActivity$MyAsyncTask.doInBackground(MainActivity.java:22)
11:21:36.630: E/AndroidRuntime(23922): at com.example.testsothreadsafe.MainActivity$MyAsyncTask.doInBackground(MainActivity.java:1)
11:21:36.630: E/AndroidRuntime(23922): at android.os.AsyncTask$2.call(AsyncTask.java:292)
11:21:36.630: E/AndroidRuntime(23922): at java.util.concurrent.FutureTask.run(FutureTask.java:237)
11:21:36.630: E/AndroidRuntime(23922): ... 4 more
Android specifically prevents you from manipulating the view hierarchy from another thread than the one that created it. But is any access prevented?
Change the example to:
@Override
protected Void doInBackground(Void... params) {
Log.d("MyAsyncTask", tv.getText().toString());
return null;
}
Click the button, the output will be:
11:25:20.950: D/MyAsyncTask(24329): Hello world!
It appears, some access is allowed.
So what's the message here? Multithreaded programming is tricky. Just because Android prevents you from doing some things, that doesn't mean anything it doesn't prevent you from doing is automatically "safe".
回答3:
The AsyncTask is thread safe. But I guess it is more convenient to just use the generic nature of AsyncTask
. That way you can get away with just making anonymous inner classes.
new AsyncTask<Object, Void, Void>() {
protected Void doInBackground(Object... params){
params[0].aMethod();
return null;
}
}.execute(obj);
But like others are saying, the object you pass in might not be thread safe.
来源:https://stackoverflow.com/questions/28902941/android-can-you-access-asynctask-class-members-in-doinbackground