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.

herpderp
fuente

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.

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

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

piedras grandes
fuente
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".

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

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

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

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

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

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

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

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

lector de mente
fuente
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.

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

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

Canerkaseler
fuente
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)
}
Raheel Khan
fuente