Android "Solo el hilo original que creó una jerarquía de vistas puede tocar sus vistas".


941

He construido un reproductor de música simple en Android. La vista para cada canción contiene un SeekBar, implementado así:

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

Esto funciona bien Ahora quiero un temporizador que cuente los segundos / minutos del progreso de la canción. Así que puse una TextViewen el diseño, lo consigue con findViewById()en onCreate()y poner esto en run()despué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);

Pero esa última línea me da la excepción:

android.view.ViewRoot $ CalledFromWrongThreadException: solo el hilo original que creó una jerarquía de vistas puede tocar sus vistas.

Sin embargo, estoy haciendo básicamente lo mismo que estoy haciendo con SeekBar: crear la vista onCreatey luego tocarla run(), y no me da esta queja.

Respuestas:


1897

Debe mover la parte de la tarea en segundo plano que actualiza la interfaz de usuario en el hilo principal. Hay un código simple para esto:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Documentación para Activity.runOnUiThread.

Simplemente anide esto dentro del método que se ejecuta en segundo plano y luego copie y pegue el código que implementa las actualizaciones en el medio del bloque. Incluya solo la menor cantidad de código posible, de lo contrario, comenzará a anular el propósito del hilo de fondo.


55
trabajado como un encanto. para mí el único problema aquí es que lo que quería hacer una error.setText(res.toString());dentro del método run (), pero que no podía usar la res porque no era definitiva .. demasiado malo
noloman

64
Un breve comentario sobre esto. Tenía un hilo separado que intentaba modificar la interfaz de usuario, y el código anterior funcionó, pero había llamado a runOnUiThread desde el objeto Activity. Tenía que hacer algo como myActivityObject.runOnUiThread(etc)
Kirby

1
@ Kirby Gracias por esta referencia. Simplemente puede hacer 'MainActivity.this' y debería funcionar también para no tener que hacer referencia a su clase de actividad.
JRomero 05 de

24
Me tomó un tiempo descubrir que runOnUiThread()es un método de Actividad. Estaba ejecutando mi código en un fragmento. Terminé haciendo getActivity().runOnUiThread(etc)y funcionó. ¡Fantástico!;
lejonl

¿Podemos detener la tarea realizada que está escrita en el cuerpo del método 'runOnUiThread'?
Karan Sharma

143

Resolví esto poniendo 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
Este se meció. Gracias por la información que, esto también se puede usar dentro de cualquier otro hilo.
Nabin

Gracias, es realmente triste crear un hilo para volver al hilo de la interfaz de usuario, pero solo esta solución salvó mi caso.
Pierre Maoui

2
Un aspecto importante es que wait(5000);no está dentro del Runnable, de lo contrario su IU se congelará durante el período de espera. Debería considerar usar en AsyncTasklugar de Thread para operaciones como estas.
Martin

esto es tan malo para la pérdida de memoria
Rafael Lima

¿Por qué molestarse con el bloque sincronizado? El código en su interior parece razonablemente seguro para subprocesos (aunque estoy completamente preparado para comer mis palabras).
David

69

Mi solución a esto:

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

Llame a este método en un hilo de fondo.


Error: (73, 67) error: el conjunto de métodos no estáticos (String) no puede ser referenciado desde un contexto estático

1
Tengo el mismo problema con mis clases de prueba. Esto funcionó como un encanto para mí. Sin embargo, reemplazando runOnUiThreadcon runTestOnUiThread. Gracias
DaddyMoe

28

Por lo general, cualquier acción relacionada con la interfaz de usuario debe realizarse en el subproceso principal o en la interfaz de usuario, que es aquella en la que onCreate()se ejecuta el manejo de eventos. Una forma de estar seguro de eso es usar runOnUiThread () , otra está usando Handlers.

ProgressBar.setProgress() tiene un mecanismo para el que siempre se ejecutará en el hilo principal, por eso funcionó.

Ver Roscado sin dolor .


El artículo de Subproceso sin dolor en ese enlace ahora es un 404. Aquí hay un enlace a un artículo de blog (¿más antiguo?) Sobre Subproceso sin
Tony Adams

20

He estado en esta situación, pero encontré una solución con el Handler Object.

En mi caso, quiero actualizar un ProgressDialog con el patrón de observador . Mi vista implementa observador y anula el método de actualización.

Entonces, mi hilo principal crea la vista y otro hilo llama al método de actualización que actualiza el ProgressDialop y ...:

Solo el hilo original que creó una jerarquía de vistas puede tocar sus vistas.

Es posible resolver el problema con el objeto controlador.

A continuación, diferentes partes de mi código:

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

Esta explicación se puede encontrar en esta página y debe leer el "Ejemplo de Diálogo de progreso con un segundo hilo".


10

Puede usar el controlador para eliminar la vista sin alterar el hilo principal de la interfaz de usuario. Aquí hay un código de ejemplo

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

7

Veo que has aceptado la respuesta de @ providence. Por si acaso, ¡también puedes usar el controlador! Primero, haz los campos int.

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

A continuación, cree una instancia de controlador como 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;
            }
            }
        }
    };

Haz un método.

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

Finalmente, pon esto en el onCreate()método.

showHandler(true);

7

Tuve un problema similar y mi solución es fea, pero funciona:

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 bueno escuchar eso. Desde el momento en que escribí esa respuesta, probablemente ahora puedas hacerlo mejor :)
Błażej

6

Yo uso Handlercon Looper.getMainLooper(). Funcionó bien para mí.

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

5

Use este código y no es necesario que runOnUiThreadfuncione:

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

Esto arroja explícitamente un error. Dice cualquier hilo que haya creado una vista, solo eso puede tocar sus vistas. Es porque la vista creada está dentro del espacio de ese hilo. La creación de la vista (GUI) ocurre en el hilo de la interfaz de usuario (principal). Por lo tanto, siempre usa el hilo de la interfaz de usuario para acceder a esos métodos.

Ingrese la descripción de la imagen aquí

En la imagen de arriba, la variable de progreso está dentro del espacio del hilo de la interfaz de usuario. Entonces, solo el hilo de la interfaz de usuario puede acceder a esta variable. Aquí, está accediendo al progreso a través del nuevo Thread (), y es por eso que recibió un error.


4

Esto le sucedió a mi cuando solicité un cambio de UI de a doInBackgrounddesde en Asynctasklugar de usar onPostExecute.

Tratar con la IU onPostExecuteresolvió mi problema.


1
Gracias Jonathan Este fue mi problema también, pero tuve que leer un poco más para entender lo que querías decir aquí. Para cualquier otra persona, onPostExecutetambién es un método AsyncTaskpero se ejecuta en el hilo de la interfaz de usuario. Ver aquí: blog.teamtreehouse.com/all-about-android-asynctasks
ciaranodc

4

Las rutinas de Kotlin pueden hacer que su código sea más conciso y legible de esta manera:

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

O viceversa:

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

3

Estaba trabajando con una clase que no contenía una referencia al contexto. Así que no me fue posible usar lo runOnUIThread();que usé view.post();y se resolvió.

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

¿Cuál es la analogía de audioMessagey tvPlayDurationpara el código de preguntas?
gotwo

audioMessagees un objeto titular de la vista de texto. tvPlayDurationes la vista de texto que queremos actualizar desde un subproceso sin interfaz de usuario. En la pregunta anterior, currentTimees la vista de texto pero no tiene un objeto titular.
Ifta

3

Cuando utilice AsyncTask, actualice la IU en el método onPostExecute

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

     }

esto me paso a mi Estaba actualizando la interfaz de usuario en doinbackground de la tarea asynk.
mehmoodnisar125

3

Estaba enfrentando un problema similar y ninguno de los métodos mencionados anteriormente funcionó para mí. Al final, esto hizo el truco para mí:

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

Encontré esta joya aquí .


2

Este es el seguimiento de la pila de la excepción mencionada

        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)

Entonces, si vas y cavas, entonces sabrás

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

Donde mThread se inicializa en el constructor como a continuación

mThread = Thread.currentThread();

Todo lo que quiero decir es que cuando creamos una vista particular, la creamos en UI Thread y luego intentamos modificarla en un Worker Thread.

Podemos verificarlo a través del fragmento de código a continuación

Thread.currentThread().getName()

cuando inflamos el diseño y más tarde donde obtiene una excepción.


2

Si no desea utilizar la runOnUiThreadAPI, puede implementar AsynTaskpara las operaciones que tardan unos segundos en completarse. Pero en ese caso, también después de procesar su trabajo doinBackground(), debe devolver la vista finalizada onPostExecute(). La implementación de Android solo permite que el hilo principal de la interfaz de usuario interactúe con las vistas.


2

Si simplemente desea invalidar (llamar a la función repintar / volver a dibujar) desde su subproceso no UI, use postInvalidate ()

myView.postInvalidate();

Esto publicará una solicitud de invalidación en el subproceso de interfaz de usuario.

Para más información: what-does-postinvalidate-do


1

Para mí, el problema era que estaba llamando onProgressUpdate()explícitamente desde mi código. Esto no debe hacerse. Llamé en su publishProgress()lugar y eso resolvió el error.


1

En mi caso, tengo EditTexten Adaptador, y ya está en el hilo de la interfaz de usuario. Sin embargo, cuando se carga esta Actividad, se bloquea con este error.

Mi solución es que necesito eliminarlo <requestFocus />de EditText en XML.


1

Para las personas que luchan en Kotlin, funciona así:

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

Resuelto: simplemente coloque este método en doInBackround Class ... y pase el mensaje

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

En mi caso, la persona que llama demasiadas veces en poco tiempo recibirá este error, simplemente pongo la verificación del tiempo transcurrido para no hacer nada si es demasiado corto, por ejemplo, ignorar si la función se llama menos de 0,5 segundos:

    private long mLastClickTime = 0;

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

        //... do ui update
    }

La mejor solución sería deshabilitar el botón al hacer clic y habilitarlo nuevamente cuando se complete la acción.
lsrom

@lsrom En mi caso no es tan simple porque la persona que llama es una biblioteca de terceros interna y está fuera de mi control.
Fruta

0

Si no puede encontrar un UIThread, puede usarlo de esta manera.

su contexto actual significa que necesita analizar el contexto actual

 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

Respuesta de Kotlin

Tenemos que usar UI Thread para el trabajo de manera verdadera. Podemos usar UI Thread en Kotlin:

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

@canerkaseler


0

En Kotlin simplemente ponga su código en el método de actividad 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)
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.