Android: can you access AsyncTask class members in DoInBackground?

天大地大妈咪最大 提交于 2019-12-12 13:43:43

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!