问题
Here is the basic life cycle of my application. It targets SDK version 8 by now, since I am still running Android 2.3.3 on my device.
- The application starts,
onResume()
is called
The methodshow()
is called to display cached data. - A background service gets started which downloads and stores data. It uses
AsyncTask
instances to accomplish its work. - One of the tasks stores downloaded data in a SQLite database.
- A broadcast intent is sent in
onPostExecute()
when the storing task has finished. - The
MapActivity
receives the intent and handles it.
The methodshow()
is called to display cached and new data.
Within the method show()
the map view gets invalidated after the overlay has been added. This works fine when show()
has been called from the MapActivity itself. It raises an exception, however, when the asynchonous task is the source of the method call (indirectly).
As far as I understand, I am at the UI thread when I trigger show()
in both cases. Is this true?
public class CustomMapActivity extends MapChangeActivity {
private boolean showIsActive = false;
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(IntentActions.FINISHED_STORING)) {
onFinishedStoring(intent);
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
registerReceiver(mReceiver, new IntentFilter(IntentActions.FINISHED_STORING));
}
@Override
protected void onResume() {
super.onResume();
show();
}
@Override
protected void onMapZoomPan() {
loadData();
show();
}
@Override
protected void onMapPan() {
loadData();
show();
}
@Override
protected void onMapZoom() {
loadData();
show();
}
private void onFinishedStoring(Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
boolean success = extras.getBoolean(BundleKeys.STORING_STATE);
if (success) {
show();
}
}
private void loadData() {
// Downloads data in a AsyncTask
// Stores data in AsyncTask
}
private void show() {
if (showIsActive) {
return;
}
showIsActive = true;
Uri uri = UriHelper.getUri();
if (uri == null) {
showIsActive = false;
return;
}
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
List<Overlay> mapOverlays = mapView.getOverlays();
CustomItemizedOverlay overlay = ItemizedOverlayFactory.getCustomizedOverlay(this, cursor);
if (overlay != null) {
mapOverlays.clear();
mapOverlays.add(overlay);
}
}
cursor.close();
mapView.invalidate(); // throws CalledFromWrongThreadException
showIsActive = false;
}
}
Here is the stack trace ...
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRoot.checkThread(ViewRoot.java:3020)
at android.view.ViewRoot.invalidateChild(ViewRoot.java:647)
at android.view.ViewRoot.invalidateChildInParent(ViewRoot.java:673)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:2511)
at android.view.View.invalidate(View.java:5332)
at info.metadude.trees.activities.CustomMapActivity.showTrees(CustomMapActivity.java:278)
at info.metadude.trees.activities.CustomMapActivity.onMapPan(CustomMapActivity.java:126)
at info.metadude.trees.activities.MapChangeActivity$MapViewChangeListener.onChange(MapChangeActivity.java:50)
at com.bricolsoftconsulting.mapchange.MyMapView$1.run(MyMapView.java:131)
at java.util.Timer$TimerImpl.run(Timer.java:284)
Note: I use the MapChange project in order to receive notifications on map events.
EDIT:
From what I now read in the documentation about AsyncTask (scroll down a bit), I am not sure if I use it the correct way. As previously mentioned I start AsyncTask
instances from within a Service
class. In contrary, the documentation states ...
AsyncTask allows you to perform asynchronous work on your user interface. It performs the blocking operations in a worker thread and then publishes the results on the UI thread, without requiring you to handle threads and/or handlers yourself.
... which sounds as if AsyncTask
should only be used within an Activity
not within a Service
?!
回答1:
The reason for your crash is because of the way that the MapChange library you are using is implemented. Under the hood, this library uses Timer
and TimerTask
implementations to delay firing the change event and reduce the number of calls your application gets to onMapChanged()
. However, you can see from the docs on Timer
that it runs its tasks in created threads:
Each timer has one thread on which tasks are executed sequentially. When this thread is busy running a task, runnable tasks may be subject to delays.
Since the MapChange library does nothing to ensure that callbacks are posted to your application on the main thread (a serious bug IMO, especially on Android), you have to protect the code you call as a result of this listener. You can see this in the example MyMapActivity
bundled with the library, everything from that callback gets funneled through a Handler
which posts the calls back to the main thread for you.
In your application, the code inside onMapPan()
and subsequently showTrees()
is being called on a background thread so it is not safe to manipulate the UI there. Using either a Handler
or runOnUiThread()
from your Activity
will guarantee your code is called in the right place.
With regards to your second questions about AsyncTask
, there is nothing stopping you from using it inside of any application component, not just Activity
. Even though it's a "background" component, by default a Service
is still running on the main thread as well, so AsyncTask
is still necessary to offload long-term processing to another thread temporarily.
回答2:
If it's getting called on the wrong thread, then it's likely not on the UI thread. Have you tried this:
runOnUiThread(new Runnable() {
public void run() {
mapView.invalidate();
}});
来源:https://stackoverflow.com/questions/11461281/android-calledfromwrongthreadexception-thrown-when-broadcast-intent-is-handled