Android "Solo il thread originale che ha creato una gerarchia di visualizzazioni può toccare le sue visualizzazioni."


941

Ho creato un semplice lettore musicale in Android. La vista per ogni brano contiene un SeekBar, implementato in questo modo:

public class Song extends Activity implements OnClickListener,Runnable {
    private SeekBar progress;
    private MediaPlayer mp;

    // ...

    private ServiceConnection onService = new ServiceConnection() {
          public void onServiceConnected(ComponentName className,
            IBinder rawBinder) {
              appService = ((MPService.LocalBinder)rawBinder).getService(); // service that handles the MediaPlayer
              progress.setVisibility(SeekBar.VISIBLE);
              progress.setProgress(0);
              mp = appService.getMP();
              appService.playSong(title);
              progress.setMax(mp.getDuration());
              new Thread(Song.this).start();
          }
          public void onServiceDisconnected(ComponentName classname) {
              appService = null;
          }
    };

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.song);

        // ...

        progress = (SeekBar) findViewById(R.id.progress);

        // ...
    }

    public void run() {
    int pos = 0;
    int total = mp.getDuration();
    while (mp != null && pos<total) {
        try {
            Thread.sleep(1000);
            pos = appService.getSongPosition();
        } catch (InterruptedException e) {
            return;
        } catch (Exception e) {
            return;
        }
        progress.setProgress(pos);
    }
}

Funziona benissimo. Ora voglio un timer che conti i secondi / minuti dell'avanzamento del brano. Così ho messo un TextViewnel layout, ottenere con findViewById()a onCreate(), e mettere questo in run()dopo progress.setProgress(pos):

String time = String.format("%d:%d",
            TimeUnit.MILLISECONDS.toMinutes(pos),
            TimeUnit.MILLISECONDS.toSeconds(pos),
            TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(
                    pos))
            );
currentTime.setText(time);  // currentTime = (TextView) findViewById(R.id.current_time);

Ma quest'ultima riga mi dà l'eccezione:

android.view.ViewRoot $ CalledFromWrongThreadException: solo il thread originale che ha creato una gerarchia di viste può toccare le sue viste.

Eppure sto facendo praticamente la stessa cosa qui con il SeekBar- creare la vista onCreate, quindi toccarla run()- e non mi dà questa lamentela.

Risposte:


1897

Devi spostare la parte dell'attività in background che aggiorna l'interfaccia utente sul thread principale. C'è un semplice pezzo di codice per questo:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Documentazione per Activity.runOnUiThread.

Basta annidarlo all'interno del metodo in esecuzione in background, quindi copiare incollare il codice che implementa eventuali aggiornamenti nel mezzo del blocco. Includi solo la minima quantità di codice possibile, altrimenti inizi a vanificare lo scopo del thread in background.


5
ha funzionato come un fascino. per me l'unico problema qui è che volevo fare un error.setText(res.toString());metodo inside the run (), ma non potevo usare la res perché non era definitiva ..
peccato

64
Un breve commento su questo. Avevo un thread separato che stava cercando di modificare l'interfaccia utente e il codice sopra funzionava, ma avevo chiamato runOnUiThread dall'oggetto Activity. Ho dovuto fare qualcosa del genere myActivityObject.runOnUiThread(etc)
Kirby, il

1
@Kirby Grazie per questo riferimento. Puoi semplicemente fare 'MainActivity.this' e dovrebbe funzionare anche in modo da non dover fare riferimento alla tua classe di attività.
JRomero,

24
Mi ci è voluto un po 'per capire che runOnUiThread()è un metodo di attività. Stavo eseguendo il mio codice in un frammento. Ho finito per farlo getActivity().runOnUiThread(etc)e ha funzionato. Fantastico!;
lejonl,

Possiamo interrompere l'attività che viene scritta nel corpo del metodo 'runOnUiThread'?
Karan Sharma,

143

Ho risolto questo mettendo runOnUiThread( new Runnable(){ ..dentro run():

thread = new Thread(){
        @Override
        public void run() {
            try {
                synchronized (this) {
                    wait(5000);

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            dbloadingInfo.setVisibility(View.VISIBLE);
                            bar.setVisibility(View.INVISIBLE);
                            loadingText.setVisibility(View.INVISIBLE);
                        }
                    });

                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Intent mainActivity = new Intent(getApplicationContext(),MainActivity.class);
            startActivity(mainActivity);
        };
    };  
    thread.start();

2
Questo ha scosso. Grazie per l'informazione, questo può essere utilizzato anche all'interno di qualsiasi altro thread.
Nabin,

Grazie, è davvero triste creare un thread per tornare al thread dell'interfaccia utente, ma solo questa soluzione ha salvato il mio caso.
Pierre Maoui,

2
Un aspetto importante è che wait(5000);non è all'interno del Runnable, altrimenti l'interfaccia utente si bloccherà durante il periodo di attesa. Si consiglia di utilizzare AsyncTaskinvece di Thread per operazioni come queste.
Martin,

questo è così male per la perdita di memoria
Rafael Lima,

Perché preoccuparsi del blocco sincronizzato? Il codice al suo interno sembra ragionevolmente sicuro (anche se sono completamente pronto a mangiare le mie parole).
David

69

La mia soluzione a questo:

private void setText(final TextView text,final String value){
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            text.setText(value);
        }
    });
}

Chiamare questo metodo su un thread in background.


Errore: (73, 67) errore: non è possibile fare riferimento a un set di metodi non statici (String) da un contesto statico

1
Ho lo stesso problema con le mie lezioni di prova. Questo ha funzionato come un incanto per me. Tuttavia, sostituendo runOnUiThreadcon runTestOnUiThread. Grazie
DaddyMoe,

28

Di solito, qualsiasi azione che coinvolge l'interfaccia utente deve essere eseguita nel thread principale o dell'interfaccia utente, ovvero quella in cui onCreate()vengono eseguite la gestione degli eventi. Un modo per essere sicuri di ciò è usare runOnUiThread () , un altro è usare Handlers.

ProgressBar.setProgress() ha un meccanismo per il quale verrà sempre eseguito sul thread principale, quindi è per questo che ha funzionato.

Vedi Threading indolore .


L'articolo di Threading indolore su quel link è ora un 404. Ecco un link a un (più vecchio?) Articolo del
Tony Adams,

20

Sono stato in questa situazione, ma ho trovato una soluzione con l'Oggetto gestore.

Nel mio caso, voglio aggiornare un ProgressDialog con il modello di osservatore . La mia vista implementa l'osservatore e sovrascrive il metodo di aggiornamento.

Quindi, il mio thread principale crea la vista e un altro thread chiama il metodo di aggiornamento che aggiorna ProgressDialop e ....:

Solo il thread originale che ha creato una gerarchia di viste può toccare le sue viste.

È possibile risolvere il problema con l'oggetto gestore.

Di seguito, diverse parti del mio codice:

public class ViewExecution extends Activity implements Observer{

    static final int PROGRESS_DIALOG = 0;
    ProgressDialog progressDialog;
    int currentNumber;

    public void onCreate(Bundle savedInstanceState) {

        currentNumber = 0;
        final Button launchPolicyButton =  ((Button) this.findViewById(R.id.launchButton));
        launchPolicyButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                showDialog(PROGRESS_DIALOG);
            }
        });
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        switch(id) {
        case PROGRESS_DIALOG:
            progressDialog = new ProgressDialog(this);
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progressDialog.setMessage("Loading");
            progressDialog.setCancelable(true);
            return progressDialog;
        default:
            return null;
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        switch(id) {
        case PROGRESS_DIALOG:
            progressDialog.setProgress(0);
        }

    }

    // Define the Handler that receives messages from the thread and update the progress
    final Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            int current = msg.arg1;
            progressDialog.setProgress(current);
            if (current >= 100){
                removeDialog (PROGRESS_DIALOG);
            }
        }
    };

    // The method called by the observer (the second thread)
    @Override
    public void update(Observable obs, Object arg1) {

        Message msg = handler.obtainMessage();
        msg.arg1 = ++currentPluginNumber;
        handler.sendMessage(msg);
    }
}

Questa spiegazione è disponibile in questa pagina ed è necessario leggere "Esempio ProgressDialog con un secondo thread".


10

È possibile utilizzare Handler per eliminare la vista senza disturbare il thread dell'interfaccia utente principale. Ecco un esempio di codice

new Handler(Looper.getMainLooper()).post(new Runnable() {
                                                        @Override
                                                        public void run() {
                                                           //do stuff like remove view etc
                                                            adapter.remove(selecteditem);
                                                        }
                                                    });

7

Vedo che hai accettato la risposta di @ provvidenza. Per ogni evenienza, puoi usare anche il gestore! Innanzitutto, esegui i campi int.

    private static final int SHOW_LOG = 1;
    private static final int HIDE_LOG = 0;

Quindi, creare un'istanza del gestore come campo.

    //TODO __________[ Handler ]__________
    @SuppressLint("HandlerLeak")
    protected Handler handler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            // Put code here...

            // Set a switch statement to toggle it on or off.
            switch(msg.what)
            {
            case SHOW_LOG:
            {
                ads.setVisibility(View.VISIBLE);
                break;
            }
            case HIDE_LOG:
            {
                ads.setVisibility(View.GONE);
                break;
            }
            }
        }
    };

Crea un metodo.

//TODO __________[ Callbacks ]__________
@Override
public void showHandler(boolean show)
{
    handler.sendEmptyMessage(show ? SHOW_LOG : HIDE_LOG);
}

Infine, mettilo al onCreate()metodo.

showHandler(true);

7

Ho avuto un problema simile e la mia soluzione è brutta, ma funziona:

void showCode() {
    hideRegisterMessage(); // Hides view 
    final Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            showRegisterMessage(); // Shows view
        }
    }, 3000); // After 3 seconds
}

2
@ R.jzadeh è bello sentirlo. Dal momento in cui ho scritto quella risposta, probabilmente ora puoi farlo meglio :)
Błażej

6

Io uso Handlercon Looper.getMainLooper(). Ha funzionato bene per me.

    Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
              // Any UI task, example
              textView.setText("your text");
        }
    };
    handler.sendEmptyMessage(1);

5

Usa questo codice e non è necessario che runOnUiThreadfunzioni:

private Handler handler;
private Runnable handlerTask;

void StartTimer(){
    handler = new Handler();   
    handlerTask = new Runnable()
    {
        @Override 
        public void run() { 
            // do something  
            textView.setText("some text");
            handler.postDelayed(handlerTask, 1000);    
        }
    };
    handlerTask.run();
}

5

Questo sta esplicitamente generando un errore. Dice che qualunque thread ha creato una vista, solo che può toccare le sue viste. È perché la vista creata si trova nello spazio di quel thread. La creazione della vista (GUI) avviene nel thread dell'interfaccia utente (principale). Quindi, usi sempre il thread dell'interfaccia utente per accedere a questi metodi.

Inserisci qui la descrizione dell'immagine

Nell'immagine sopra, la variabile progress è all'interno dello spazio del thread dell'interfaccia utente. Pertanto, solo il thread dell'interfaccia utente può accedere a questa variabile. Qui stai accedendo ai progressi tramite il nuovo thread () ed è per questo che hai ricevuto un errore.


4

Questo è accaduto a mia quando ho chiamato per un cambiamento UI da un doInBackgroundda Asynctaskinvece di utilizzare onPostExecute.

Gestire l'interfaccia utente ha onPostExecuterisolto il mio problema.


1
Grazie Jonathan. Anche questo era il mio problema, ma ho dovuto leggere un po 'di più per capire cosa intendevi qui. Per chiunque altro, onPostExecuteè anche un metodo di AsyncTaskma viene eseguito sul thread dell'interfaccia utente. Vedi qui: blog.teamtreehouse.com/all-about-android-asynctasks
ciaranodc

4

Le coroutine di Kotlin possono rendere il tuo codice più conciso e leggibile in questo modo:

MainScope().launch {
    withContext(Dispatchers.Default) {
        //TODO("Background processing...")
    }
    TODO("Update UI here!")
}

O vice versa:

GlobalScope.launch {
    //TODO("Background processing...")
    withContext(Dispatchers.Main) {
        // TODO("Update UI here!")
    }
    TODO("Continue background processing...")
}

3

Stavo lavorando con una classe che non conteneva un riferimento al contesto. Quindi non è stato possibile per me usare quello che runOnUIThread();ho usato view.post();ed è stato risolto.

timer.scheduleAtFixedRate(new TimerTask() {

    @Override
    public void run() {
        final int currentPosition = mediaPlayer.getCurrentPosition();
        audioMessage.seekBar.setProgress(currentPosition / 1000);
        audioMessage.tvPlayDuration.post(new Runnable() {
            @Override
            public void run() {
                audioMessage.tvPlayDuration.setText(ChatDateTimeFormatter.getDuration(currentPosition));
            }
        });
    }
}, 0, 1000);

Qual è l'analogia audioMessagee tvPlayDurationil codice delle domande?
Gotwo

audioMessageè un oggetto titolare della vista testo. tvPlayDurationè la vista di testo che vogliamo aggiornare da un thread non UI. Nella domanda sopra, currentTimeè la vista di testo ma non ha un oggetto titolare.
Ifta,

3

Quando si utilizza AsyncTask Aggiorna l'interfaccia utente nel metodo onPostExecute

    @Override
    protected void onPostExecute(String s) {
   // Update UI here

     }

questo mi è successo. ti stavo aggiornando nel doinbackground dell'attività asynk.
mehmoodnisar125,

3

Stavo affrontando un problema simile e nessuno dei metodi sopra menzionati ha funzionato per me. Alla fine, questo ha fatto il trucco per me:

Device.BeginInvokeOnMainThread(() =>
    {
        myMethod();
    });

Ho trovato questo gioiello qui .


2

Questa è la traccia dello stack dell'eccezione menzionata

        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6149)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:843)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:352)
        at android.view.View.requestLayout(View.java:16474)
        at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:352)
        at android.view.View.setFlags(View.java:8938)
        at android.view.View.setVisibility(View.java:6066)

Quindi se vai a scavare, allora vieni a sapere

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

Dove mThread è inizializzato nel costruttore come di seguito

mThread = Thread.currentThread();

Tutto ciò che intendo dire è che quando abbiamo creato una vista particolare, l'abbiamo creata su UI Thread e successivamente abbiamo provato a modificare in un thread di lavoro.

Possiamo verificarlo tramite lo snippet di codice seguente

Thread.currentThread().getName()

quando gonfiamo il layout e successivamente dove stai ottenendo un'eccezione.


2

Se non si desidera utilizzare l' runOnUiThreadAPI, è infatti possibile implementare AsynTaskper il completamento delle operazioni che richiedono alcuni secondi. Ma in quel caso, anche dopo aver elaborato il tuo lavoro doinBackground(), devi restituire la vista finita onPostExecute(). L'implementazione Android consente solo al thread dell'interfaccia utente principale di interagire con le visualizzazioni.


2

Se vuoi semplicemente invalidare (richiama la funzione ridisegna / ridisegna) dal tuo thread non UI, usa postInvalidate ()

myView.postInvalidate();

Questo invierà una richiesta non valida sul thread dell'interfaccia utente.

Per ulteriori informazioni: what-does-postinvalidate-do


1

Per me il problema era che stavo chiamando onProgressUpdate()esplicitamente dal mio codice. Questo non dovrebbe essere fatto. Ho chiamato publishProgress()invece e questo ha risolto l'errore.


1

Nel mio caso, l'ho fatto EditText in Adapter, ed è già nel thread dell'interfaccia utente. Tuttavia, quando questa attività viene caricata, si blocca con questo errore.

La mia soluzione è che devo rimuovere <requestFocus />da EditText in XML.


1

Per le persone che lottano a Kotlin, funziona così:

lateinit var runnable: Runnable //global variable

 runOnUiThread { //Lambda
            runnable = Runnable {

                //do something here

                runDelayedHandler(5000)
            }
        }

        runnable.run()

 //you need to keep the handler outside the runnable body to work in kotlin
 fun runDelayedHandler(timeToWait: Long) {

        //Keep it running
        val handler = Handler()
        handler.postDelayed(runnable, timeToWait)
    }

0

Risolto: basta inserire questo metodo nella classe doInBackround ... e passare il messaggio

public void setProgressText(final String progressText){
        Handler handler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // Any UI task, example
                progressDialog.setMessage(progressText);
            }
        };
        handler.sendEmptyMessage(1);

    }

0

Nel mio caso, il chiamante chiama troppe volte in breve tempo otterrà questo errore, ho semplicemente messo il tempo trascorso controllando di non fare nulla se troppo breve, ad esempio ignora se la funzione viene chiamata meno di 0,5 secondi:

    private long mLastClickTime = 0;

    public boolean foo() {
        if ( (SystemClock.elapsedRealtime() - mLastClickTime) < 500) {
            return false;
        }
        mLastClickTime = SystemClock.elapsedRealtime();

        //... do ui update
    }

La soluzione migliore sarebbe disabilitare il pulsante al clic e riattivarlo al termine dell'azione.
dal

@lsrom Nel mio caso non è così semplice perché il chiamante è una libreria di terze parti interna e fuori dal mio controllo.
Frutta

0

Se non riesci a trovare un UIThread puoi usarlo in questo modo.

il tuo mediacontesto medio, devi analizzare il contesto corrente

 new Thread(new Runnable() {
        public void run() {
            while (true) {
                (Activity) yourcurrentcontext).runOnUiThread(new Runnable() {
                    public void run() { 
                        Log.d("Thread Log","I am from UI Thread");
                    }
                });
                try {
                    Thread.sleep(1000);
                } catch (Exception ex) {

                }
            }
        }
    }).start();

0

Risposta di Kotlin

Dobbiamo usare UI Thread per il lavoro in modo vero. Possiamo usare UI Thread in Kotlin:

runOnUiThread(Runnable {
   //TODO: Your job is here..!
})

@canerkaseler


0

In Kotlin inserisci semplicemente il tuo codice nel metodo di attività runOnUiThread

runOnUiThread{
    // write your code here, for example
    val task = Runnable {
            Handler().postDelayed({
                var smzHtcList = mDb?.smzHtcReferralDao()?.getAll()
                tv_showSmzHtcList.text = smzHtcList.toString()
            }, 10)

        }
    mDbWorkerThread.postTask(task)
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.