Android "Nur der ursprüngliche Thread, der eine Ansichtshierarchie erstellt hat, kann seine Ansichten berühren."


940

Ich habe einen einfachen Musik-Player in Android gebaut. Die Ansicht für jedes Lied enthält eine SeekBar, die wie folgt implementiert ist:

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);
    }
}

Das funktioniert gut. Jetzt möchte ich einen Timer, der die Sekunden / Minuten des Fortschritts des Songs zählt. Also habe ich ein TextViewin das Layout eingefügt, es mit findViewById()in aufgenommen onCreate()und dieses nach run()eingefügt 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);

Aber diese letzte Zeile gibt mir die Ausnahme:

android.view.ViewRoot $ CalledFromWrongThreadException: Nur der ursprüngliche Thread, der eine Ansichtshierarchie erstellt hat, kann seine Ansichten berühren.

Trotzdem mache ich hier im Grunde das Gleiche wie beim SeekBar- Erstellen der Ansicht onCreate, dann Berühren run()- und es gibt mir keine Beschwerde.

Antworten:


1894

Sie müssen den Teil der Hintergrundaufgabe, der die Benutzeroberfläche aktualisiert, in den Hauptthread verschieben. Hierfür gibt es einen einfachen Code:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Dokumentation für Activity.runOnUiThread.

Verschachteln Sie dies einfach in der Methode, die im Hintergrund ausgeführt wird, und kopieren Sie dann den Code, der alle Aktualisierungen implementiert, in die Mitte des Blocks. Fügen Sie nur die kleinstmögliche Menge an Code ein, da Sie sonst den Zweck des Hintergrundthreads zunichte machen.


5
Lief wie am Schnürchen. Für mich ist das einzige Problem hier, dass ich eine error.setText(res.toString());Inside-the-Run () -Methode machen wollte, aber ich konnte die Res nicht verwenden, weil sie nicht endgültig war.
Schade

64
Ein kurzer Kommentar dazu. Ich hatte einen separaten Thread, der versuchte, die Benutzeroberfläche zu ändern, und der obige Code funktionierte, aber ich hatte runOnUiThread vom Activity-Objekt aus aufgerufen. Ich musste so etwas tun myActivityObject.runOnUiThread(etc)
Kirby

1
@ Kirby Vielen Dank für diesen Hinweis. Sie können einfach 'MainActivity.this' ausführen und es sollte auch funktionieren, damit Sie nicht auf Ihre Aktivitätsklasse verweisen müssen.
JRomero

24
Ich habe eine Weile gebraucht, um herauszufinden, dass dies runOnUiThread()eine Methode der Aktivität ist. Ich habe meinen Code in einem Fragment ausgeführt. Am Ende habe ich es getan getActivity().runOnUiThread(etc)und es hat funktioniert. Fantastisch!;
Lejonl

Können wir die Ausführung der Aufgabe stoppen, die im Hauptteil der Methode 'runOnUiThread' geschrieben ist?
Karan Sharma

143

Ich habe das gelöst, indem ich runOnUiThread( new Runnable(){ ..hineingesteckt habe 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
Dieser rockte. Vielen Dank für die Information, dass dies auch in jedem anderen Thread verwendet werden kann.
Nabin

Vielen Dank, es ist wirklich traurig, einen Thread zu erstellen, um zum UI-Thread zurückzukehren, aber nur diese Lösung hat meinen Fall gerettet.
Pierre Maoui

2
Ein wichtiger Aspekt ist, dass er wait(5000);sich nicht in Runnable befindet, da sonst Ihre Benutzeroberfläche während der Wartezeit einfriert. Sie sollten in Betracht ziehen, AsyncTaskfür solche Vorgänge anstelle von Thread zu verwenden.
Martin

Das ist so schlimm für Speicherverlust
Rafael Lima

Warum sich mit dem synchronisierten Block beschäftigen? Der Code darin sieht einigermaßen threadsicher aus (obwohl ich voll und ganz darauf vorbereitet bin, meine Worte zu essen).
David

69

Meine Lösung dafür:

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

Rufen Sie diese Methode in einem Hintergrundthread auf.


Fehler: (73, 67) Fehler: Nicht statischer Methodensatz (String) kann nicht aus einem statischen Kontext

1
Ich habe das gleiche Problem mit meinen Testklassen. Das hat für mich wie ein Zauber gewirkt. Ersetzen runOnUiThreaddurch runTestOnUiThread. Danke
DaddyMoe

28

Normalerweise muss jede Aktion, die die Benutzeroberfläche betrifft, im Haupt- oder UI-Thread ausgeführt werden, dh in dem, in dem die onCreate()Ereignisbehandlung ausgeführt wird. Eine Möglichkeit, dies sicherzustellen, ist die Verwendung von runOnUiThread () , eine andere die Verwendung von Handlern.

ProgressBar.setProgress() hat einen Mechanismus, für den es immer auf dem Hauptthread ausgeführt wird, deshalb hat es funktioniert.

Siehe Schmerzloses Einfädeln .


Der Artikel zu Painless Threading unter diesem Link ist jetzt 404. Hier ist ein Link zu einem (älteren?) Blog-Artikel über Painless Threading - android-developers.blogspot.com/2009/05/painless-threading.html
Tony Adams

20

Ich war in dieser Situation, habe aber mit dem Handler-Objekt eine Lösung gefunden.

In meinem Fall möchte ich einen ProgressDialog mit dem Beobachtermuster aktualisieren . Meine Ansicht implementiert Beobachter und überschreibt die Aktualisierungsmethode.

Also, mein Haupt-Thread erstellt die Ansicht und ein anderer Thread ruft die Update-Methode auf, die ProgressDialop aktualisiert und ....:

Nur der ursprüngliche Thread, der eine Ansichtshierarchie erstellt hat, kann seine Ansichten berühren.

Es ist möglich, das Problem mit dem Handler-Objekt zu lösen.

Unten verschiedene Teile meines Codes:

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);
    }
}

Diese Erklärung finden Sie auf dieser Seite , und Sie müssen den "Beispiel-Fortschrittsdialog mit einem zweiten Thread" lesen.


10

Sie können den Handler verwenden, um die Ansicht zu löschen, ohne den Haupt-UI-Thread zu stören. Hier ist ein Beispielcode

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

7

Ich sehe, dass Sie die Antwort von @ providence akzeptiert haben. Für alle Fälle können Sie auch den Handler verwenden! Führen Sie zuerst die int-Felder aus.

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

Erstellen Sie als Nächstes eine Handlerinstanz als Feld.

    //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;
            }
            }
        }
    };

Mach eine Methode.

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

Zum Schluss setzen Sie dies auf onCreate()Methode.

showHandler(true);

7

Ich hatte ein ähnliches Problem und meine Lösung ist hässlich, aber es funktioniert:

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 es ist schön das zu hören. Seit dem Moment, als ich diese Antwort geschrieben habe, kannst du es wahrscheinlich jetzt besser machen :)
Błażej

6

Ich benutze Handlermit Looper.getMainLooper(). Es hat gut für mich funktioniert.

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

5

Verwenden Sie diesen Code und Sie müssen nicht runOnUiThreadfunktionieren:

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

Dies löst explizit einen Fehler aus. Es heißt, welcher Thread eine Ansicht erstellt hat, nur dieser kann seine Ansichten berühren. Dies liegt daran, dass sich die erstellte Ansicht im Bereich des Threads befindet. Die Ansichtserstellung (GUI) erfolgt im UI-Thread (Hauptthread). Sie verwenden also immer den UI-Thread, um auf diese Methoden zuzugreifen.

Geben Sie hier die Bildbeschreibung ein

Im obigen Bild befindet sich die Fortschrittsvariable im Bereich des UI-Threads. Daher kann nur der UI-Thread auf diese Variable zugreifen. Hier greifen Sie über den neuen Thread () auf den Fortschritt zu, und deshalb haben Sie eine Fehlermeldung erhalten.


4

Dies geschah mit meinem, als ich eine Änderung der Benutzeroberfläche von einem doInBackgroundvon Asynctaskanstelle von forderte onPostExecute.

Der Umgang mit der Benutzeroberfläche hat onPostExecutemein Problem gelöst.


1
Danke Jonathan. Dies war auch mein Problem, aber ich musste etwas mehr lesen, um zu verstehen, was Sie hier meinten. Für alle anderen onPostExecuteist auch eine Methode, AsyncTaskaber es läuft auf dem UI-Thread. Siehe hier: blog.teamtreehouse.com/all-about-android-asynctasks
ciaranodc

4

Kotlin-Coroutinen können Ihren Code so übersichtlicher und lesbarer machen:

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

Oder umgekehrt:

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

3

Ich habe mit einer Klasse gearbeitet, die keinen Verweis auf den Kontext enthielt. So war es mir nicht möglich, runOnUIThread();ich benutzte view.post();und es wurde gelöst.

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);

Was ist die Analogie von audioMessageund tvPlayDurationzum Fragencode?
Ich bin

audioMessageist ein Halterobjekt der Textansicht. tvPlayDurationist die Textansicht, die wir von einem Nicht-UI-Thread aktualisieren möchten. In der obigen Frage currentTimehandelt es sich um die Textansicht, die jedoch kein Inhaberobjekt enthält.
Ifta

3

Bei Verwendung von AsyncTask Aktualisieren Sie die Benutzeroberfläche in der onPostExecute- Methode

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

     }

das ist mir passiert Ich habe die Benutzeroberfläche im Hintergrund der Asynk-Aufgabe aktualisiert.
mehmoodnisar125

3

Ich hatte ein ähnliches Problem und keine der oben genannten Methoden funktionierte für mich. Am Ende hat dies den Trick für mich getan:

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

Ich habe dieses Juwel hier gefunden .


2

Dies ist die Stapelverfolgung der genannten Ausnahme

        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)

Wenn Sie also graben, lernen Sie es kennen

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

Wobei mThread im Konstruktor wie unten initialisiert wird

mThread = Thread.currentThread();

Ich möchte nur sagen, dass wir beim Erstellen einer bestimmten Ansicht diese im UI-Thread erstellt haben und später versuchen, sie in einem Worker-Thread zu ändern.

Wir können es über das folgende Code-Snippet überprüfen

Thread.currentThread().getName()

wenn wir das Layout aufblasen und später, wo Sie eine Ausnahme bekommen.


2

Wenn Sie die runOnUiThreadAPI nicht verwenden möchten , können Sie sie tatsächlich AsynTaskfür die Vorgänge implementieren , deren Abschluss einige Sekunden dauert. In diesem Fall müssen Sie jedoch auch nach der Bearbeitung Ihrer Arbeit in doinBackground()die fertige Ansicht zurückgeben onPostExecute(). Die Android-Implementierung ermöglicht es nur dem Haupt-UI-Thread, mit Ansichten zu interagieren.


2

Wenn Sie einfach ungültig machen (Repaint / Redraw-Funktion aufrufen) möchten, verwenden Sie postInvalidate ()

myView.postInvalidate();

Dies wird eine ungültige Anfrage auf dem UI-Thread posten.

Für weitere Informationen: Was-macht-Postinvalidieren-tun


1

Für mich war das Problem, dass ich onProgressUpdate()explizit von meinem Code aus anrief . Dies sollte nicht getan werden. Ich habe publishProgress()stattdessen angerufen und das hat den Fehler behoben.


1

In meinem Fall habe ich EditTextin Adapter, und es ist bereits im UI-Thread. Wenn diese Aktivität geladen wird, stürzt sie jedoch mit diesem Fehler ab.

Meine Lösung ist, dass ich <requestFocus />EditText in XML entfernen muss.


1

Für die Menschen in Kotlin funktioniert das folgendermaßen:

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

Gelöst: Fügen Sie diese Methode einfach in die doInBackround-Klasse ein ... und übergeben Sie die Nachricht

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

In meinem Fall wird der Fehler angezeigt, wenn der Anrufer in kurzer Zeit zu oft anruft. Ich habe einfach die verstrichene Zeit überprüft, um nichts zu tun, wenn es zu kurz ist, z. B. zu ignorieren, wenn die Funktion weniger als 0,5 Sekunden aufgerufen wird:

    private long mLastClickTime = 0;

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

        //... do ui update
    }

Eine bessere Lösung wäre, die Schaltfläche beim Klicken zu deaktivieren und nach Abschluss der Aktion wieder zu aktivieren.
Ab dem

@lsrom In meinem Fall ist das nicht so einfach, da der Anrufer eine interne Bibliothek eines Drittanbieters ist und außerhalb meiner Kontrolle liegt.
Obst

0

Wenn Sie keinen UIThread gefunden haben, können Sie diesen Weg verwenden.

Ihr aktueller Kontext bedeutet, dass Sie den aktuellen Kontext analysieren müssen

 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

Kotlin Antwort

Wir müssen UI-Thread für den Job auf echte Weise verwenden. Wir können UI-Thread in Kotlin verwenden:

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

@canerkaseler


0

In Kotlin geben Sie einfach Ihren Code in die Aktivitätsmethode runOnUiThread ein

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)
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.