Android „Tylko oryginalny wątek, który utworzył hierarchię widoków, może dotykać jej widoków”.

939

Zbudowałem prosty odtwarzacz muzyki na Androida. Widok każdej piosenki zawiera SeekBar, zaimplementowany w następujący sposób:

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

To działa dobrze. Teraz chcę licznik sekund / minut postępu utworu. Więc umieścić TextVieww układzie, dostać go findViewById()w onCreate(), i umieścić to w run()po 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);

Ale ten ostatni wiersz daje mi wyjątek:

android.view.ViewRoot $ CalledFromWrongThreadException: Tylko oryginalny wątek, który utworzył hierarchię widoków, może dotykać jego widoków.

Ale robię tu zasadniczo to samo, co robię SeekBar- tworząc widok onCreate, a następnie dotykając go run()- i to nie daje mi tej skargi.

herpderp
źródło

Odpowiedzi:

1893

Musisz przenieść część zadania w tle, które aktualizuje interfejs użytkownika do głównego wątku. Jest na to prosty kod:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Dokumentacja dla Activity.runOnUiThread.

Po prostu zagnieżdż to w metodzie działającej w tle, a następnie skopiuj wklej kod, który implementuje wszelkie aktualizacje w środku bloku. Podaj tylko najmniejszą możliwą ilość kodu, w przeciwnym razie zaczniesz niszczyć cel wątku w tle.

opatrzność
źródło
5
działał jak urok. dla mnie jedynym problemem jest to, że chciałem wykonać metodę error.setText(res.toString());run (), ale nie mogłem użyć res, ponieważ nie była to ostateczna .. szkoda
noloman
64
Jeden krótki komentarz na ten temat. Miałem osobny wątek, który próbował zmodyfikować interfejs użytkownika, i powyższy kod działał, ale miałem wywołanie runOnUiThread z obiektu Activity. Musiałem zrobić coś takiego myActivityObject.runOnUiThread(etc)
Kirby,
1
@ Kirby Dziękuję za to odniesienie. Możesz po prostu zrobić „MainActivity.this” i powinno to również działać, więc nie musisz zachowywać odniesienia do swojej klasy aktywności.
JRomero
24
Zajęło mi trochę czasu, aby odkryć, że runOnUiThread()jest to metoda działania. Uruchomiłem mój kod we fragmencie. Skończyło się na tym getActivity().runOnUiThread(etc)i zadziałało. Fantastyczny!;
lejonl,
Czy możemy zatrzymać wykonywanie zadania zapisanego w treści metody „runOnUiThread”?
Karan Sharma
143

Rozwiązałem to, umieszczając w runOnUiThread( new Runnable(){ ..środku 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
źródło
2
Ten się zachwiał. Dziękujemy za informację, że można tego również użyć w dowolnym innym wątku.
Nabin
Dziękuję, naprawdę przykro jest tworzyć wątek, aby wrócić do wątku interfejsu użytkownika, ale tylko to rozwiązanie uratowało moją sprawę.
Pierre Maoui
2
Jednym ważnym aspektem jest to, że wait(5000);nie ma go w Runnable, w przeciwnym razie interfejs użytkownika zawiesi się podczas okresu oczekiwania. Powinieneś rozważyć użycie AsyncTaskzamiast Wątku do takich operacji.
Martin
to jest tak złe dla wycieku pamięci
Rafael Lima
Po co zawracać sobie głowę synchronizowanym blokiem? Kod w nim wygląda na dość bezpieczny dla wątków (chociaż jestem w pełni przygotowany do zjedzenia moich słów).
David
69

Moje rozwiązanie tego:

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

Wywołaj tę metodę w wątku w tle.

Angelo Angeles
źródło
Błąd: (73, 67) błąd: nie można odwoływać się do zestawu metod niestatycznych (ciąg) ze statycznego kontekstu
1
Mam ten sam problem z moimi klasami testowymi. To działało dla mnie jak urok. Jednak zastępując runOnUiThreadgo runTestOnUiThread. Dzięki
DaddyMoe
28

Zwykle wszelkie działania związane z interfejsem użytkownika muszą być wykonywane w wątku głównym lub interfejsie użytkownika, czyli w tym onCreate() wykonywana jest obsługa zdarzeń. Jednym ze sposobów, aby być tego pewnym, jest użycie runOnUiThread () , innym sposobem jest użycie handlerów .

ProgressBar.setProgress() ma mechanizm, dla którego zawsze będzie wykonywany w głównym wątku, dlatego działał.

Zobacz Bezbolesne nawlekanie .

duże kamienie
źródło
Artykuł „Bezbolesne wątki” pod tym linkiem ma teraz numer 404. Oto link do (starszego?) Bloga na temat „Bezbolesne wątki”
Tony Adams
20

Byłem w tej sytuacji, ale znalazłem rozwiązanie z Handler Object.

W moim przypadku chcę zaktualizować ProgressDialog o wzorzec obserwatora . Mój widok implementuje obserwatora i przesłania metodę aktualizacji.

Tak więc mój główny wątek tworzy widok, a inny wątek wywołuje metodę aktualizacji, która aktualizuje ProgressDialop i ....:

Tylko oryginalny wątek, który utworzył hierarchię widoków, może dotykać jego widoków.

Możliwe jest rozwiązanie problemu z Obiektem Handler.

Poniżej różne części mojego kodu:

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

To wyjaśnienie można znaleźć na tej stronie i należy przeczytać „Przykładowy program ProgressDialog z drugim wątkiem”.

Jonathan
źródło
10

Za pomocą modułu obsługi można usunąć widok bez zakłócania głównego wątku interfejsu użytkownika. Oto przykładowy kod

new Handler(Looper.getMainLooper()).post(new Runnable() {
                                                        @Override
                                                        public void run() {
                                                           //do stuff like remove view etc
                                                            adapter.remove(selecteditem);
                                                        }
                                                    });
Bilal Mustafa
źródło
7

Widzę, że zaakceptowałeś odpowiedź @ opatrzności. Na wszelki wypadek możesz również użyć obsługi! Najpierw wykonaj pola int.

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

Następnie utwórz instancję modułu obsługi jako pole.

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

Zrób metodę.

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

Na koniec umieść to w onCreate()metodzie.

showHandler(true);
David Dimalanta
źródło
7

Miałem podobny problem i moje rozwiązanie jest brzydkie, ale działa:

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
źródło
2
@ R.jzadeh miło to słyszeć. Od momentu, kiedy napisałem tę odpowiedź, prawdopodobnie teraz możesz to zrobić lepiej :)
Błażej
6

Używam Handlerz Looper.getMainLooper(). Dla mnie działało dobrze.

    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
źródło
5

Użyj tego kodu i nie musisz runOnUiThreaddziałać:

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
źródło
5

To jawnie generuje błąd. Mówi, który wątek utworzył widok, ale tylko ten może dotknąć jego widoków. Jest tak, ponieważ utworzony widok znajduje się w przestrzeni tego wątku. Tworzenie widoku (GUI) odbywa się w wątku interfejsu użytkownika (głównym). Tak więc zawsze używasz wątku interfejsu użytkownika, aby uzyskać dostęp do tych metod.

Wpisz opis zdjęcia tutaj

Na powyższym zdjęciu zmienna progress znajduje się w przestrzeni wątku interfejsu użytkownika. Tak więc tylko wątek interfejsu użytkownika może uzyskać dostęp do tej zmiennej. Tutaj uzyskujesz dostęp do postępu za pomocą nowego Thread () i dlatego wystąpił błąd.

Uddhav Gautam
źródło
4

Stało się tak, gdy wezwałem do zmiany interfejsu użytkownika doInBackgroundz Asynctaskzamiast zamiast używania onPostExecute.

Radzenie sobie z interfejsem użytkownika onPostExecuterozwiązało mój problem.

Jonathan dos Santos
źródło
1
Dzięki Jonathan. To też był mój problem, ale musiałem przeczytać trochę więcej, aby zrozumieć, co miałeś na myśli. Dla każdego innego onPostExecutejest to metoda, AsyncTaskale działa w wątku interfejsu użytkownika. Zobacz tutaj: blog.teamtreehouse.com/all-about-android-asynctasks
ciaranodc
4

Kortyny Kotlina mogą uczynić twój kod bardziej zwięzłym i czytelnym w następujący sposób:

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

Lub odwrotnie:

GlobalScope.launch {
    //TODO("Background processing...")
    withContext(Dispatchers.Main) {
        // TODO("Update UI here!")
    }
    TODO("Continue background processing...")
}
KenIchi
źródło
3

Pracowałem z klasą, która nie zawierała odniesienia do kontekstu. Więc nie mogłem użyć, runOnUIThread();którego użyłem view.post();i został rozwiązany.

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
źródło
Co to jest analogia audioMessagei tvPlayDurationdo kodu pytania?
gotwo
audioMessagejest obiektem posiadacza widoku tekstowego. tvPlayDurationto widok tekstu, który chcemy zaktualizować z wątku innego niż interfejs użytkownika. W powyższym pytaniu currentTimejest widok tekstu, ale nie ma on obiektu uchwytu.
Ifta
3

Podczas korzystania z AsyncTask zaktualizuj interfejs użytkownika w metodzie onPostExecute

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

     }
Deepak Kataria
źródło
to mi się przydarzyło. aktualizowałem interfejs użytkownika w doinbackground zadania asynk.
mehmoodnisar125
3

Miałem podobny problem i żadna z wyżej wymienionych metod nie działała dla mnie. W końcu załatwiłem sprawę:

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

Znalazłem ten klejnot tutaj .

Hagbard
źródło
2

Jest to ślad stosu wspomnianego wyjątku

        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)

Więc jeśli idziesz i kopiesz, to wiesz

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

Gdzie mThread jest inicjowany w konstruktorze jak poniżej

mThread = Thread.currentThread();

Chciałbym tylko powiedzieć, że kiedy stworzyliśmy konkretny widok, utworzyliśmy go w wątku interfejsu użytkownika, a później próbowaliśmy zmodyfikować w wątku roboczym.

Możemy to zweryfikować za pomocą fragmentu kodu poniżej

Thread.currentThread().getName()

kiedy nadmuchujemy układ i później, kiedy dostajesz wyjątek.

Amit Yadav
źródło
2

Jeśli nie chcesz używać runOnUiThreadinterfejsu API, możesz w rzeczywistości zaimplementować AsynTaskoperacje, których wykonanie zajmuje kilka sekund. Ale w takim przypadku, również po przetworzeniu pracy doinBackground(), musisz zwrócić gotowy widok onPostExecute(). Implementacja Androida pozwala na interakcję tylko z głównym wątkiem interfejsu użytkownika z widokami.

Sam
źródło
2

Jeśli chcesz po prostu unieważnić (funkcja odmalowania / przerysowania) w swoim wątku innym niż interfejs użytkownika, użyj postInvalidate ()

myView.postInvalidate();

Spowoduje to opublikowanie nieprawidłowego żądania w wątku interfejsu użytkownika.

Aby uzyskać więcej informacji: what-does-postinvalidate-do

Nalin
źródło
1

Dla mnie problemem było to, że dzwoniłem onProgressUpdate()jawnie z mojego kodu. Nie należy tego robić. Zadzwoniłem publishProgress()zamiast tego i to rozwiązało błąd.

czytający w myślach
źródło
1

W moim przypadku mam EditText w adapterze i jest już w wątku interfejsu użytkownika. Jednak po załadowaniu tego działania następuje awaria z tym błędem.

Moje rozwiązanie polega na tym, że muszę usunąć <requestFocus />z EditText w XML.

Sruit A.Suk
źródło
1

Dla osób walczących w Kotlinie działa to w następujący sposób:

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
źródło
0

Rozwiązane: wystarczy umieścić tę metodę w klasie doInBackround ... i przekazać wiadomość

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
źródło
0

W moim przypadku dzwoniący zbyt wiele razy w krótkim czasie otrzyma ten błąd, po prostu odkładam sprawdzanie czasu, aby nic nie robić, jeśli jest zbyt krótki, np. Zignoruj, jeśli funkcja zostanie wywołana krócej niż 0,5 sekundy:

    private long mLastClickTime = 0;

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

        //... do ui update
    }
Owoc
źródło
Lepszym rozwiązaniem byłoby wyłączenie przycisku po kliknięciu i włączenie go ponownie po zakończeniu akcji.
lsrom
@lsrom W moim przypadku nie jest to takie proste, ponieważ program wywołujący jest biblioteką zewnętrzną wewnętrzną i poza moją kontrolą.
Owoce
0

Jeśli nie możesz znaleźć UIThread, możesz użyć tego sposobu.

Twój aktualny kontekst oznacza, że ​​musisz przeanalizować bieżący kontekst

 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
źródło
0

Odpowiedź Kotlina

Musimy używać Wątku interfejsu użytkownika do zadania w prawdziwy sposób. Możemy użyć wątku interfejsu użytkownika w Kotlin:

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

@canerkaseler

canerkaseler
źródło
0

W Kotlin po prostu umieść kod w metodzie działania 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)
}
Raheel Khan
źródło