Android "Seul le fil d'origine qui a créé une hiérarchie de vues peut toucher ses vues."


941

J'ai construit un simple lecteur de musique sous Android. La vue de chaque chanson contient un SeekBar, implémenté comme ceci:

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

Cela fonctionne bien. Maintenant, je veux une minuterie comptant les secondes / minutes de la progression de la chanson. Donc , je mets un TextViewdans la mise en page, obtenir avec findViewById()dans onCreate()et mettre cela en run()après 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);

Mais cette dernière ligne me donne l'exception:

android.view.ViewRoot $ CalledFromWrongThreadException: seul le thread d'origine qui a créé une hiérarchie de vues peut toucher ses vues.

Pourtant, je fais essentiellement la même chose ici que je fais avec le SeekBar- créer la vue dans onCreate, puis la toucher run()- et cela ne me donne pas cette plainte.

Réponses:


1897

Vous devez déplacer la partie de la tâche d'arrière-plan qui met à jour l'interface utilisateur sur le thread principal. Il existe un simple code pour cela:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Documentation pour Activity.runOnUiThread.

Il suffit d'imbriquer cela dans la méthode qui s'exécute en arrière-plan, puis de copier-coller le code qui implémente les mises à jour au milieu du bloc. N'incluez que la plus petite quantité de code possible, sinon vous commencez à contourner l'objectif du thread d'arrière-plan.


5
travaillé comme un charme. pour moi, le seul problème ici est que je voulais faire une error.setText(res.toString());méthode à l'intérieur de la méthode run (), mais je ne pouvais pas utiliser le res car ce n'était pas final .. dommage
noloman

64
Un bref commentaire à ce sujet. J'avais un thread séparé qui essayait de modifier l'interface utilisateur et le code ci-dessus fonctionnait, mais j'avais appelé runOnUiThread à partir de l'objet Activity. Je devais faire quelque chose comme myActivityObject.runOnUiThread(etc)
Kirby

1
@Kirby Merci pour cette référence. Vous pouvez simplement faire 'MainActivity.this' et cela devrait également fonctionner afin que vous n'ayez pas à garder de référence à votre classe d'activité.
JRomero

24
Il m'a fallu un certain temps pour comprendre que runOnUiThread()c'était une méthode d'activité. J'exécutais mon code dans un fragment. J'ai fini par faire getActivity().runOnUiThread(etc)et ça a marché. Fantastique!;
lejonl

Pouvons-nous arrêter l'exécution de la tâche qui est écrite dans le corps de la méthode 'runOnUiThread'?
Karan Sharma

143

J'ai résolu cela en mettant à l' runOnUiThread( new Runnable(){ ..intérieur 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
Celui-ci a basculé. Merci pour les informations qui peuvent également être utilisées dans n'importe quel autre fil.
Nabin

Merci, c'est vraiment triste de créer un fil de discussion pour revenir au fil de l'interface utilisateur mais seule cette solution a sauvé mon cas.
Pierre Maoui

2
Un aspect important est qu'il wait(5000);ne se trouve pas dans Runnable, sinon votre interface utilisateur se figera pendant la période d'attente. Vous devriez envisager d'utiliser à la AsyncTaskplace de Thread pour des opérations comme celles-ci.
Martin

c'est si mauvais pour une fuite de mémoire
Rafael Lima

Pourquoi s'embêter avec le bloc synchronisé? Le code qu'il contient semble raisonnablement sûr pour les threads (bien que je sois tout à fait prêt à manger mes mots).
David

69

Ma solution à cela:

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

Appelez cette méthode sur un thread d'arrière-plan.


Erreur: (73, 67) erreur: l'ensemble de méthodes non statique (chaîne) ne peut pas être référencé à partir d'un contexte statique

1
J'ai le même problème avec mes classes de test. Cela a fonctionné comme un charme pour moi. Cependant, en remplaçant runOnUiThreadpar runTestOnUiThread. Merci
DaddyMoe

28

Habituellement, toute action impliquant l'interface utilisateur doit être effectuée dans le thread principal ou d'interface utilisateur, c'est-à-dire celui dans lequel la onCreate()gestion des événements est exécutée. Une façon de s'en assurer est d'utiliser runOnUiThread () , une autre utilise des gestionnaires.

ProgressBar.setProgress() a un mécanisme pour lequel il s'exécutera toujours sur le thread principal, c'est pourquoi cela a fonctionné.

Voir Filetage sans douleur .


L'article sur le filetage indolore sur ce lien est maintenant un 404. Voici un lien vers un article de blog (plus ancien?) Sur le filetage indolore - android-developers.blogspot.com/2009/05/pireless-threading.html
Tony Adams

20

J'ai été dans cette situation, mais j'ai trouvé une solution avec l'objet gestionnaire.

Dans mon cas, je veux mettre à jour un ProgressDialog avec le modèle d'observateur . Ma vue implémente l'observateur et remplace la méthode de mise à jour.

Donc, mon thread principal crée la vue et un autre thread appelle la méthode de mise à jour qui met à jour ProgressDialop et ....:

Seul le thread d'origine qui a créé une hiérarchie de vues peut toucher ses vues.

Il est possible de résoudre le problème avec l'objet gestionnaire.

Ci-dessous, différentes parties de mon code:

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

Cette explication se trouve sur cette page , et vous devez lire "Exemple ProgressDialog avec un deuxième fil".


10

Vous pouvez utiliser le gestionnaire pour supprimer la vue sans perturber le thread d'interface utilisateur principal. Voici un exemple de code

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

7

Je vois que vous avez accepté la réponse de @ providence. Au cas où, vous pouvez également utiliser le gestionnaire! Tout d'abord, faites les champs int.

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

Ensuite, créez une instance de gestionnaire en tant que champ.

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

Faites une méthode.

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

Enfin, mettez cela à la onCreate()méthode.

showHandler(true);

7

J'ai eu un problème similaire, et ma solution est moche, mais cela fonctionne:

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 c'est agréable d'entendre ça. Depuis le moment où j'ai écrit cette réponse, vous pouvez probablement le faire mieux maintenant :)
Błażej

6

J'utilise Handleravec Looper.getMainLooper(). Cela a bien fonctionné pour moi.

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

5

Utilisez ce code, et pas besoin de runOnUiThreadfonctionner:

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

Cela renvoie explicitement une erreur. Il indique quel que soit le thread qui a créé une vue, seul celui qui peut toucher ses vues. C'est parce que la vue créée est à l'intérieur de l'espace de ce thread. La création de la vue (GUI) se produit dans le thread UI (principal). Ainsi, vous utilisez toujours le thread d'interface utilisateur pour accéder à ces méthodes.

Entrez la description de l'image ici

Dans l'image ci-dessus, la variable de progression se trouve à l'intérieur de l'espace du thread d'interface utilisateur. Ainsi, seul le thread d'interface utilisateur peut accéder à cette variable. Ici, vous accédez à la progression via le nouveau Thread (), et c'est pourquoi vous avez une erreur.


4

Cela est arrivé à mon quand j'ai appelé pour un changement d'interface utilisateur à partir d'un doInBackgroundau Asynctasklieu d'utiliseronPostExecute .

Gérer l'interface utilisateur a onPostExecuterésolu mon problème.


1
Merci Jonathan. C'était aussi mon problème mais j'ai dû faire un peu plus de lecture pour comprendre ce que vous vouliez dire ici. Pour quelqu'un d'autre, onPostExecutec'est aussi une méthode de AsyncTaskmais elle s'exécute sur le thread d'interface utilisateur. Voir ici: blog.teamtreehouse.com/all-about-android-asynctasks
ciaranodc

4

Les coroutines Kotlin peuvent rendre votre code plus concis et lisible comme ceci:

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

Ou vice versa:

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

3

Je travaillais avec une classe qui ne contenait aucune référence au contexte. Il ne m'a donc pas été possible d'utiliser ce que runOnUIThread();j'ai utilisé view.post();et cela a été résolu.

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

Quelle est l'analogie avec audioMessageet tvPlayDurationau code des questions?
gotwo

audioMessageest un objet support de la vue texte. tvPlayDurationest la vue texte que nous voulons mettre à jour à partir d'un thread non UI. Dans la question ci-dessus, currentTimeest la vue texte mais elle n'a pas d'objet support.
Ifta

3

Lors de l'utilisation de AsyncTask Mettre à jour l'interface utilisateur dans la méthode onPostExecute

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

     }

cela m'est arrivé. je mettais à jour l'interface utilisateur dans doinbackground de la tâche asynk.
mehmoodnisar125

3

J'étais confronté à un problème similaire et aucune des méthodes mentionnées ci-dessus ne fonctionnait pour moi. En fin de compte, cela a fait l'affaire pour moi:

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

J'ai trouvé ce bijou ici .


2

Ceci est la trace de pile de l'exception mentionnée

        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)

Donc, si vous allez creuser, vous découvrez

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

mThread est initialisé dans le constructeur comme ci-dessous

mThread = Thread.currentThread();

Tout ce que je veux dire, c'est que lorsque nous avons créé une vue particulière, nous l'avons créée sur le thread d'interface utilisateur et essayons plus tard de la modifier dans un thread de travail.

Nous pouvons le vérifier via l'extrait de code ci-dessous

Thread.currentThread().getName()

lorsque nous gonflons la mise en page et plus tard où vous obtenez une exception.


2

Si vous ne souhaitez pas utiliser l' runOnUiThreadAPI, vous pouvez en fait l'implémenter AsynTaskpour les opérations qui prennent quelques secondes à terminer. Mais dans ce cas, également après avoir traité votre travail doinBackground(), vous devez renvoyer la vue terminée dans onPostExecute(). L'implémentation Android permet uniquement au thread d'interface utilisateur principal d'interagir avec les vues.


2

Si vous souhaitez simplement invalider (appeler la fonction repeindre / redessiner) à partir de votre thread non UI, utilisez postInvalidate ()

myView.postInvalidate();

Cela affichera une demande d'invalidation sur le thread d'interface utilisateur.

Pour plus d'informations: what-does-postinvalidate-do


1

Pour moi, le problème était que j'appelais onProgressUpdate()explicitement à partir de mon code. Cela ne devrait pas être fait. J'ai appelé à la publishProgress()place et cela a résolu l'erreur.


1

Dans mon cas, j'ai EditText dans l'adaptateur, et c'est déjà dans le thread d'interface utilisateur. Cependant, lorsque cette activité se charge, elle se bloque avec cette erreur.

Ma solution est que je dois supprimer <requestFocus />de EditText en XML.


1

Pour les personnes en difficulté à Kotlin, cela fonctionne comme ceci:

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

Résolu: Mettez simplement cette méthode dans la classe doInBackround ... et passez le message

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

Dans mon cas, l'appelant appelle trop de fois en peu de temps obtiendra cette erreur, j'ai simplement mis la vérification du temps écoulé pour ne rien faire si trop court, par exemple ignorer si la fonction est appelée moins de 0,5 seconde:

    private long mLastClickTime = 0;

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

        //... do ui update
    }

La meilleure solution serait de désactiver le bouton au clic et de le réactiver à la fin de l'action.
lsrom

@lsrom Dans mon cas, ce n'est pas si simple car l'appelant est une bibliothèque tierce interne et hors de mon contrôle.
Fruit

0

Si vous ne parvenez pas à trouver un UIThread, vous pouvez utiliser cette méthode.

votre contexte actuel signifie que vous devez analyser le contexte actuel

 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 Answer

Nous devons utiliser UI Thread pour le travail avec vrai chemin. Nous pouvons utiliser UI Thread dans Kotlin:

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

@canerkaseler


0

Dans Kotlin, mettez simplement votre code dans la méthode d'activité 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)
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.