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.

ich Idiot
quelle

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.

Vorsehung
quelle
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();
Günay Gültekin
quelle
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.

Angelo Angeles
quelle
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 .

Bigstones
quelle
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.

Jonathan
quelle
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);
                                                        }
                                                    });
Bilal Mustafa
quelle
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);
David Dimalanta
quelle
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
}
Błażej
quelle
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);
Sankar Behera
quelle
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();
}
Hamid
quelle
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.

Uddhav Gautam
quelle
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.

Jonathan dos Santos
quelle
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...")
}
KenIchi
quelle
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);
Ifta
quelle
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

     }
Deepak Kataria
quelle
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 .

Hagbard
quelle
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.

Amit Yadav
quelle
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.

Sam
quelle
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

Nalin
quelle
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.

Gedankenleser
quelle
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.

Obst A.Suk
quelle
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)
    }
Tarun Kumar
quelle
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);

    }
Kaushal Sachan
quelle
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
    }
Obst
quelle
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();
Udara Kasun
quelle
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

canerkaseler
quelle
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)
}
Raheel Khan
quelle