2015-04-28 23 views
10

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"?

+1

Suppongo che il modo migliore per chiarire questo sarebbe il controllo del codice sorgente Android in base alla traccia dello stack che hai ottenuto. –

+0

[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 –

+1

condividere il codice attività asincrono – apk

risposta

2

in base alla risposta del RocketRandom ho fatto un po 'di scavo e si avvicinò con una risposta più completa, che sento è garantito qui.

Responsabile dell'eventuale eccezione è infatti ViewRootImpl.checkThread() che viene richiamato quando viene chiamato performLayout(). performLayout() supera la gerarchia di viste fino a quando non finisce in ViewRootImpl, ma ha origine in TextView.checkForRelayout(), che viene chiamato da setText(). Fin qui tutto bene. Quindi perché a volte l'eccezione non viene generata quando chiamiamo setText()?

TextView.checkForRelayout() viene chiamato solo se il TextView ha già un Layout (mLayout != null). (Questo controllo è ciò che inibisce l'eccezione di essere gettato in questo caso, non mHandlingLayoutInLayoutRequest in ViewRootImpl.)

Così, ancora una volta, perché il TextView volte non hanno un Layout? O meglio, dal momento che ovviamente inizia a non averne uno, quando e da dove viene preso?

Quando il TextView viene inizialmente aggiunto alla LinearLayout utilizzando layout.addView(tv);, ancora una volta, una catena di requestLayout() si chiama, viaggiando nella gerarchia View, finendo in ViewRootImpl, dove questa volta, viene lanciata non fa eccezione, perché siamo ancora sul thread dell'interfaccia utente. Qui, ViewRootImpl, quindi chiama scheduleTraversals().

La parte importante è che questo post un callback/Runnable sulle code di messaggi Choreographer, che viene lavorato "asincrono" al flusso principale di esecuzione:

mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 

Il Choreographer finirà per elaborare questo utilizzando un Handler ed eseguire qualunque RunnableViewRootImpl ha pubblicato qui, che finirà per chiamare performTraversals(), measureHierarchy(), e performMeasure() (su ViewRootImpl), che esegue una ulteriore serie di View.measure(), onMeasure() chiamate (E pochi altri), viaggiando lungo la gerarchia View giungere infine nostra TextView.onMeasure(), che chiede makeNewLayout(), che chiama makeSingleLayout(), che definisce infine nostra variabile mLayout membro:

mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize, 
      effectiveEllipsize, effectiveEllipsize == mEllipsize); 

Dopo questo caso, non è mLayout null più, e qualsiasi tentativo di modificare il TextView, cioè chiamando setText() come nel nostro esempio, porterà al ben noto CalledFromWrongThreadException.

Quindi quello che abbiamo qui è un bel po 'di condizione di gara, se la nostra AsyncTask può mettere le mani sul TextView prima che i Choreographer attraversamenti sono completi, è possibile modificarlo senza penalità. Naturalmente questa è ancora una cattiva pratica, e non dovrebbe essere fatta (ci sono molti altri post SO che trattano di questo), ma se viene fatto per errore o inconsapevolmente, lo CalledFromWrongThreadException non è una protezione perfetta.

Questo esempio forzato utilizza uno TextView ei dettagli possono variare per altre viste, ma il principio generale rimane lo stesso. Resta da vedere se qualche altra implementazione View (forse personalizzata) che non chiama requestLayout() in ogni caso può essere modificata senza penalità, il che potrebbe portare a problemi più grandi (nascosti).

0

È possibile scrivere in doInBackground su TextView se non è ancora parte della GUI.

È solo una parte della GUI dopo la dichiarazione setContentView(layout);.

Solo il mio pensiero.

+0

Ma eseguo _am_ l'AsyncTask _after_ setContentView (layout). –

+0

Hai determinato la sequenza reale di istruzioni con Log.d()? Non si tratta di quando si esegue l'asynctask ma quando si esegue doInBackground() dell'attività. – greenapps

+0

Bene, doInBackground() non può essere eseguito _before_ execute() viene chiamato. Ma ti umorerò e inserirò le dichiarazioni dei log quando tornerò su questo, non al PC in questo momento. –

2

Il metodo checkThread() di ViewRootImpl.java è responsabile dell'eliminazione di questa eccezione. Questo controllo viene soppresso usando mHandlingLayoutInLayoutRequest membro fino a performLayout() e tutti gli attraversamenti del disegno iniziale sono completi.

quindi genera eccezioni solo se utilizziamo il ritardo.

Non so se questo è un bug in Android o intenzionale :)

+0

Puoi aggiungere qualche altro sfondo/dettagli/puntatori e in particolare spiegare che cosa significa "tutti gli attraversamenti del disegno iniziale sono completi"? Ho aggiunto un altro esempio alle domande come aggiornamento. –

+0

Nell'esempio aggiornato, la nuova vista testo viene creata e aggiunta in onClick. Quindi l'aggiornamento avviene prima che la "nuova" vista testo venga visualizzata sullo schermo. – RocketRandom

+0

Ho svalutato la tua risposta perché mi ha messo sulla strada giusta. Ho scavato un po 'di più e ho trovato una risposta che mi sembra più completa. Molte grazie. –

Problemi correlati