Android "Тільки оригінальна нитка, яка створила ієрархію перегляду, може торкатися її поглядів".


939

Я створив простий музичний плеєр в Android. Перегляд кожної пісні містить SeekBar, реалізований так:

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

Це чудово працює. Тепер я хочу, щоб таймер відлічив секунди / хвилини ходу пісні. Так що я поклав TextViewв макеті, отримати його findViewById()в onCreate(), і помістити його в run()після того, як 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);

Але останній рядок дає мені виняток:

android.view.ViewRoot $ CalledFromWrongThreadException: Тільки оригінальна нитка, яка створила ієрархію перегляду, може торкатися її поглядів.

Але я роблю в основному те саме, що я роблю з SeekBar- створюю вигляд onCreate, потім торкаюся його run()- і це не дає мені цієї скарги.

Відповіді:


1893

Ви повинні перемістити частину фонового завдання, яка оновлює інтерфейс користувача, на основний потік. Для цього є простий фрагмент коду:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Документація на Activity.runOnUiThread.

Просто вставте це всередині методу, який працює у фоновому режимі, а потім скопіюйте вставте код, який реалізує будь-які оновлення в середині блоку. Включіть лише найменшу кількість можливого коду, інакше ви почнете перемагати мету фонового потоку.


5
працював як шарм. для мене єдина проблема в тому , що я хотів зробити error.setText(res.toString());всередині методу Run (), але я не міг використовувати Рез , тому що це був не остаточний .. занадто погано
noloman

64
Короткий коментар до цього. У мене був окремий потік, який намагався змінити інтерфейс користувача, і вищевказаний код працював, але я викликав runOnUiThread від об’єкта Activity. Мені довелося зробити щось на кшталт myActivityObject.runOnUiThread(etc)
Кірбі

1
@Kirby Дякую за це посилання. Ви можете просто зробити "MainActivity.this", і він повинен працювати так само добре, що вам не доведеться зберігати посилання на свій клас активності.
JRomero

24
Мені знадобилося певний час, щоб зрозуміти, що runOnUiThread()це метод діяльності. Я запускав свій фрагмент коду. Я закінчив це робити, getActivity().runOnUiThread(etc)і це спрацювало. Фантастичний !;
lejonl

Чи можемо ми зупинити виконання завдання, написане в тілі методу 'runOnUiThread'?
Karan Sharma

143

Я вирішив це, помістивши runOnUiThread( new Runnable(){ ..всередину 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
Цей гойдався. Дякуємо за інформацію, що це також можна використовувати всередині будь-якої іншої теми.
Набін

Дякую, створити нитку, щоб повернутися до потоку інтерфейсу користувача, дуже сумно, але тільки це рішення врятувало мою справу.
П’єр Мауї

2
Одним з важливих аспектів є те, що wait(5000);він не знаходиться у програмі Runnable, інакше ваш інтерфейс буде замерзнути протягом періоду очікування. Вам слід розглянути можливість використання AsyncTaskзамість теми для таких операцій.
Мартін

це так погано для витоку пам’яті
Рафаель Ліма

Навіщо турбуватися із синхронізованим блоком? Код, який знаходиться всередині, виглядає досить безпечно для потоків (хоча я повністю готовий їсти свої слова).
Девід

69

Моє рішення для цього:

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

Викличте цей метод на фоновій нитці.


Помилка: (73, 67) помилка: набір нестатичного методу (String) не можна посилатись із статичного контексту

1
У мене те саме питання з моїми тестовими класами. Це спрацювало як шарм для мене. Однак замінивши runOnUiThreadна runTestOnUiThread. Спасибі
DaddyMoe

28

Зазвичай будь-які дії, пов’язані з користувальницьким інтерфейсом, повинні бути виконані в основному або потоці користувальницького інтерфейсу, тобто тієї, в якій onCreate()і виконується обробка подій. Один із способів бути впевненим у цьому - використання runOnUiThread () , інший - використання обробників.

ProgressBar.setProgress() має механізм, за допомогою якого він завжди буде виконуватися на головній нитці, тому саме він працював.

Див. Розділ Безболісна нитка .


Стаття "Безборна нитка" за цим посиланням зараз 404. Ось посилання на старий блог на тему "Безболісна нитка" - android-developers.blogspot.com/2009/05/painless-threading.html
Тоні Адамс

20

Я був у цій ситуації, але знайшов рішення з Об’єктом обробника.

У моєму випадку я хочу оновити ProgressDialog зі схемою спостерігачів . Мій погляд реалізує спостерігач і замінює метод оновлення.

Отже, мій основний потік створює подання, а інший потік викликає метод оновлення, який оновлює ProgressDialop і ....:

Лише оригінальна нитка, яка створила ієрархію перегляду, може торкатися її поглядів.

Можна вирішити проблему з Об'єктом обробника.

Нижче, різні частини мого коду:

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

Це пояснення можна знайти на цій сторінці , і ви повинні прочитати "Приклад ProgressDialog з другою темою".


10

Ви можете використовувати Handler для видалення перегляду, не порушуючи основну нитку інтерфейсу користувача. Ось приклад коду

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

7

Я бачу, що ви прийняли відповідь @ провидіння. Про всяк випадок, ви також можете використовувати обробник! Спочатку виконайте поля int.

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

Далі зробіть екземпляр обробника як поле.

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

Складіть метод.

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

Нарешті, покладіть це на onCreate()метод.

showHandler(true);

7

У мене була подібна проблема, і моє рішення некрасиве, але воно працює:

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 приємно це почути. З того моменту, як я написав цю відповідь, напевно, тепер ви можете зробити це краще :)
Błażej

6

Я використовую Handlerс Looper.getMainLooper(). Це добре працювало для мене.

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

5

Використовуйте цей код і не потрібно runOnUiThreadфункціонувати:

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

Це явно викликає помилку. У ній сказано, яка би нитка створила подання, лише те, що може торкатися його поглядів. Це тому, що створений вигляд знаходиться всередині простору цієї нитки. Створення перегляду (GUI) відбувається в (основному) потоці інтерфейсу користувача. Отже, ви завжди використовуєте потік інтерфейсу для доступу до цих методів.

Введіть тут опис зображення

На наведеному вище малюнку змінна прогрес знаходиться всередині простору потоку інтерфейсу. Отже, лише нитка інтерфейсу користувача може отримати доступ до цієї змінної. Тут ви отримуєте доступ до прогресу за допомогою нової теми (), і тому ви отримали помилку.


4

Це сталося з моїм , коли я назвав для зміни призначеного для користувача інтерфейсу з doInBackgroundз Asynctaskзамість використання onPostExecute.

Справа з інтерфейсом користувача onPostExecuteвирішила мою проблему.


1
Спасибі Джонатане. Це теж було моїм питанням, але мені довелося трохи читати, щоб зрозуміти, що ти тут маєш на увазі. Для всіх інших, onPostExecuteце також метод, AsyncTaskале він працює на потоці інтерфейсу користувача. Дивіться тут: blog.teamtreehouse.com/all-about-android-asynctasks
ciaranodc

4

Програми Kotlin можуть зробити ваш код більш стислим і читабельним так:

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

Або навпаки:

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

3

Я працював з класом, який не містив посилання на контекст. Тож я не міг використати, що runOnUIThread();я використовував, view.post();і це було вирішено.

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

Яка аналогія audioMessageта tvPlayDurationкод запитань?
gotwo

audioMessageє власником об'єкта подання тексту. tvPlayDurationце текстове подання, яке ми хочемо оновити з потоку, що не використовується інтерфейсом користувача. У вищезазначеному питанні currentTime- це перегляд тексту, але він не має об'єкта-власника.
Іфта

3

Під час використання AsyncTask Оновлення інтерфейсу користувача в методі onPostExecute

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

     }

це сталося зі мною. я оновлював інтерфейс користувача на doinbackground завдання asynk.
mehmoodnisar125

3

Я зіткнувся з подібною проблемою, і жоден із згаданих вище методів не працював на мене. Врешті-решт, це зробило для мене трюк:

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

Я знайшов тут цей самоцвіт .


2

Це слід стека згаданого винятку

        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)

Тож якщо ти підеш копати, то дізнаєшся

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

Де mThread ініціалізується в конструкторі, як показано нижче

mThread = Thread.currentThread();

Я маю на увазі сказати, що коли ми створили певний вигляд, ми створили його на UI Thread і пізніше спробуємо модифікувати в Worker Thread.

Ми можемо підтвердити це за допомогою фрагмента коду нижче

Thread.currentThread().getName()

коли ми надуваємо макет і пізніше, коли ви отримуєте виняток.


2

Якщо ви не хочете використовувати runOnUiThreadAPI, ви можете насправді здійснити AsynTaskоперації, на які потрібно кілька секунд. Але в цьому випадку, також після обробки вашої роботи doinBackground(), вам потрібно повернути готовий вигляд у onPostExecute(). Реалізація Android дозволяє взаємодіяти лише з основними потоками інтерфейсу користувача.


2

Якщо ви просто хочете визнати недійсним (функція перефарбовування / перемальовування виклику) з теми, що не використовується інтерфейсом користувача, використовуйте postInvalidate ()

myView.postInvalidate();

Це опублікує недійсний запит у потоці інтерфейсу користувача.

Для отримання додаткової інформації: що-що-робити-постінваліда-робити


1

Для мене проблема полягала в тому, що я дзвонив onProgressUpdate()явно зі свого коду. Цього не слід робити. Я зателефонував publishProgress()замість цього, і це вирішило помилку.


1

У моєму випадку у мене є EditTextадаптер, і це вже в потоці інтерфейсу. Однак, коли ця активність завантажується, ця помилка з цією помилкою.

Моє рішення - мені потрібно видалити <requestFocus />з EditText в XML.


1

Для людей, які борються в Котліні, це працює так:

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

Вирішено: Просто поставте цей метод у клас doInBackround ... і передайте повідомлення

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

У моєму випадку зателефонуючий дзвінок, який викликає занадто багато разів, за короткий час отримає цю помилку, я просто ставлю перевірку минулого часу, щоб нічого не робити, якщо занадто короткий, наприклад, ігнорувати, якщо функція отримує виклик менше 0,5 секунди:

    private long mLastClickTime = 0;

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

        //... do ui update
    }

Кращим рішенням було б відключити кнопку при натисканні та включити її знову після завершення дії.
lsrom

@lsrom У моєму випадку це не так просто, тому що абонент є бібліотекою сторонньої внутрішньої та поза моїм контролем.
Фрукти

0

Якщо ви не змогли знайти UIThread, ви можете скористатися цим способом.

yourcurrentcontext значить, що вам потрібно проаналізувати поточний контекст

 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

Відповідь Котліна

Ми повинні використовувати нитку UI для роботи справжнім способом. Ми можемо використовувати нитку інтерфейсу користувача у Котліні:

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

@canerkaseler


0

У Котліні просто введіть свій код у метод діяльності 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)
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.