Mi sono imbattuto in qualche comportamento inaspettato quando ho giocato con qualche codice di esempio.Modifica delle viste in AsyncTask doInBackground() non genera (sempre) l'eccezione
Come "tutti sanno" non è possibile modificare gli elementi dell'interfaccia utente da un altro thread, ad es. il doInBackground()
di un AsyncTask
.
Ad esempio:
public class MainActivity extends Activity {
private TextView tv;
public class MyAsyncTask extends AsyncTask<TextView, Void, Void> {
@Override
protected Void doInBackground(TextView... params) {
params[0].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().execute(tv);
}
});
layout.addView(tv);
layout.addView(button);
setContentView(layout);
}
}
Se si esegue questo, e fa clic sul pulsante, sei app si fermerà come previsto e troverete la seguente analisi dello stack nel logcat:
11: 21: 36.630: E/AndroidRuntime (23922): FATAL EXCEPTION: AsyncTask # 1
...
11: 21: 36.630: E/AndroidRuntime (23922): java.lang.RuntimeException: si è verificato un errore durante l'esecuzione doInBackground()
...
11: 21: 36.630: E/AndroidRuntime (23922): Causato da: android.view.ViewRootImpl $ CalledFromWrongThreadException: solo il thread originale che ha creato una gerarchia di viste può toccare le sue viste.
11: 21: 36,630: E/AndroidRuntime (23922): a android.view.ViewRootImpl.checkThread (ViewRootImpl.java:6357)
Fin qui tutto bene.
Ora ho modificato il per eseguire immediatamente AsyncTask
e non attendere il clic del pulsante.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// same as above...
new MyAsyncTask().execute(tv);
}
L'applicazione non si chiude, nulla nei log, TextView
ora mostra "Boom!" sullo schermo. Wow. Non me l'aspettavo.
Forse troppo presto nel ciclo di vita Activity
? Spostiamo l'esecuzione su onResume()
.
@Override
protected void onResume() {
super.onResume();
new MyAsyncTask().execute(tv);
}
Stesso comportamento come sopra.
Ok, attacciamolo su un Handler
.
@Override
protected void onResume() {
super.onResume();
Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
new MyAsyncTask().execute(tv);
}
});
}
Stesso comportamento. Sono a corto di idee e cercare postDelayed()
con un ritardo di 1 secondo:
@Override
protected void onResume() {
super.onResume();
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
new MyAsyncTask().execute(tv);
}
}, 1000);
}
Finalmente! L'eccezione prevista:
11: 21: 36,630: E/AndroidRuntime (23922): causato da: android.view.ViewRootImpl $ CalledFromWrongThreadException: Solo il thread originale che ha creato una gerarchia vista può toccare i suoi panorami.
Wow, questa è la tempistica? Io provo ritardi diversi e sembra che per questa particolare esecuzione di prova, su questo particolare dispositivo (Nexus 4, in esecuzione 5.1) il numero magico sia 60ms, cioè a volte sia un'eccezione, a volte aggiorna lo TextView
come se nulla era successo.
Suppongo che ciò accada quando la gerarchia della vista non è stata completamente creata nel punto in cui è stata modificata dal AsyncTask
. È corretto? C'è una spiegazione migliore per questo? Esiste una richiamata su Activity
che può essere utilizzata per assicurarsi che la gerarchia della vista sia stata completamente creata? Le questioni relative ai tempi sono spaventose.
Ho trovato una domanda simile qui Altering UI thread's Views in AsyncTask in doInBackground, CalledFromWrongThreadException not always thrown ma non c'è alcuna spiegazione.
Aggiornamento:
causa di una richiesta di commenti e di una proposta di risposta, ho aggiunto un po 'di registrazione di debug per accertare la catena di eventi ...
public class MainActivity extends Activity {
private TextView tv;
public class MyAsyncTask extends AsyncTask<TextView, Void, Void> {
@Override
protected Void doInBackground(TextView... params) {
Log.d("MyAsyncTask", "before setText");
params[0].setText("Boom!");
Log.d("MyAsyncTask", "after setText");
return null;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout layout = new LinearLayout(this);
tv = new TextView(this);
tv.setText("Hello world!");
layout.addView(tv);
Log.d("MainActivity", "before setContentView");
setContentView(layout);
Log.d("MainActivity", "after setContentView, before execute");
new MyAsyncTask().execute(tv);
Log.d("MainActivity", "after execute");
}
}
uscita:
10: 01: 33.126: D/MainActivity (18386): prima setContentView
10: 01: 33.137: D/MainActivity (18386): dopo l'impostazione ContentView, prima di eseguire
10: 01: 33,148: D/MainActivity (18386): eseguire dopo
10: 01: 33,153: D/MyAsyncTask (18386): prima setText
10: 01: 33,153: D/MyAsyncTask (18386): dopo setText
Tutto come previsto, niente di insolito qui, setContentView()
completata prima execute()
è chiamato, che a sua volta viene completata prima setText()
viene chiamato da doInBackground()
. Quindi non è quello.
Aggiornamento:
Un altro esempio:
public class MainActivity extends Activity {
private LinearLayout layout;
private TextView tv;
public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
tv.setText("Boom!");
return null;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
layout = new LinearLayout(this);
Button button = new Button(this);
button.setText("Click!");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tv = new TextView(MainActivity5.this);
tv.setText("Hello world!");
layout.addView(tv);
new MyAsyncTask().execute();
}
});
layout.addView(button);
setContentView(layout);
}
}
Questa volta, sto aggiungendo il TextView
nel onClick()
del Button
immediatamente prima di chiamare execute()
sul AsyncTask
. A questo punto l'iniziale Layout
(senza il TextView
) è stato visualizzato correttamente (ad esempio, posso vedere il pulsante e fare clic). Di nuovo, nessuna eccezione generata.
E l'esempio contatore, se aggiungo Thread.sleep(100);
nella execute()
prima setText()
in doInBackground()
si butta la solita eccezione.
Un'altra cosa che ho appena notato è che, appena prima dell'eccezione, il testo dello TextView
viene effettivamente aggiornato e viene visualizzato correttamente, per una frazione di secondo, finché l'app non si chiude automaticamente.
immagino qualcosa deve accadere (in modo asincrono, vale a dire indipendente da qualsiasi ciclo di vita metodi/callback) al mio TextView
che in qualche modo "attacca" a ViewRootImpl
, che rende quest'ultimo generare l'eccezione. Qualcuno ha una spiegazione o dei riferimenti a un'ulteriore documentazione su cosa sia quel "qualcosa"?
Suppongo che il modo migliore per chiarire questo sarebbe il controllo del codice sorgente Android in base alla traccia dello stack che hai ottenuto. –
[Debug di un asyncTask] (http://stackoverflow.com/questions/4770587/how-do-i-use-the-eclipse-debugger-in-an-asynctask-when-developing-for-android) dovrebbe aiutarti –
condividere il codice attività asincrono – apk