[Android] Изменение вида в AsyncTask doInBackground () не исключает (всегда) исключение



Answers

Основываясь на ответе RocketRandom, я сделал еще несколько копаний и придумал более полный ответ, который мне кажется оправданным здесь.

Ответственный за возможное исключение - это действительно ViewRootImpl.checkThread() который вызывается при performLayout() . performLayout() перемещает иерархию представления до тех пор, пока она не ViewRootImpl в ViewRootImpl , но она возникла в TextView.checkForRelayout() , которая вызывается setText() . Все идет нормально. Итак, почему исключение иногда не получается, когда мы вызываем setText() ?

TextView.checkForRelayout() вызывается только в том случае, если TextView уже имеет Layout ( mLayout != null ). (Эта проверка запрещает исключение из этого исключения, а не mHandlingLayoutInLayoutRequest в ViewRootImpl .)

Итак, опять же, почему TextView иногда не имеет Layout ? Или лучше, поскольку, очевидно, у него начинается не наличие одного, когда и откуда оно получается?

Когда TextView первоначально добавляется в LinearLayout используя layout.addView(tv); , снова requestLayout() цепочка requestLayout() , перемещаясь вверх по иерархии View , заканчивая в ViewRootImpl , где на этот раз не ViewRootImpl исключение, потому что мы все еще находимся в потоке пользовательского интерфейса. Здесь ViewRootImpl затем вызывает scheduleTraversals() .

Важная часть здесь заключается в том, что это сообщение обратного вызова / Runnable на очереди сообщений Choreographer , которое обрабатывается «асинхронно» с основным потоком выполнения:

mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

Choreographer конечном итоге обработает это с помощью Handler и запустит все Runnable ViewRootImpl , которое в конечном итоге вызовет функции performTraversals() , measureHierarchy() и performMeasure() (на ViewRootImpl ), которые будут выполнять еще одну серию View.measure() , onMeasure() (и несколько других), перемещаясь по иерархии View до тех пор, пока она наконец не достигнет нашего TextView.onMeasure() , который вызывает makeNewLayout() , который вызывает makeSingleLayout() , который, наконец, устанавливает нашу переменную-член mLayout :

mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
            effectiveEllipsize, effectiveEllipsize == mEllipsize);

После этого mLayout больше не имеет значения, и любая попытка изменить TextView , то есть вызов setText() как в нашем примере, приведет к известному CalledFromWrongThreadException .

Итак, у нас здесь хорошее состояние гонки, если наша AsyncTask может получить AsyncTask к TextView до завершения Choreographer , он может изменить его без штрафов. Конечно, это по-прежнему плохая практика, и ее не следует делать (есть много других сообщений SO, посвященных этому), но если это происходит случайно или неосознанно, CalledFromWrongThreadException не является идеальной защитой.

Этот надуманный пример использует TextView и детали могут отличаться для других представлений, но общий принцип остается тем же. Остается requestLayout() если какая-либо другая реализация представления (возможно, requestLayout() ), которая не вызывает requestLayout() в каждом случае может быть изменена без штрафов, что может привести к большим (скрытым) проблемам.

Question

Я просто натолкнулся на какое-то неожиданное поведение при игре с некоторым примером кода.

Как «все знают», вы не можете изменять элементы пользовательского интерфейса из другого потока, например doInBackground() для AsyncTask .

Например:

public class MainActivity extends Activity {
    private TextView tv;

    public class MyAsyncTask extends AsyncTask<TextView, Void, Void> {
        @Override
        protected Void doInBackground(TextView... params) {
            params[0].setText("Boom!");
            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        tv = new TextView(this);
        tv.setText("Hello world!");
        Button button = new Button(this);
        button.setText("Click!");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new MyAsyncTask().execute(tv);
            }
        });
        layout.addView(tv);
        layout.addView(button);
        setContentView(layout);
    }
}

Если вы запустите это и нажмите кнопку, приложение будет остановлено, как ожидалось, и вы увидите следующую трассировку стека в logcat:

11: 21: 36.630: E / AndroidRuntime (23922): FATAL EXCEPTION: AsyncTask # 1
...
11: 21: 36.630: E / AndroidRuntime (23922): java.lang.RuntimeException: Произошла ошибка при выполнении doInBackground ()
...
11: 21: 36.630: E / AndroidRuntime (23922): Caused by: android.view.ViewRootImpl $ CalledFromWrongThreadException: только исходный поток, создавший иерархию представлений, может коснуться его представлений.
11: 21: 36.630: E / AndroidRuntime (23922): на android.view.ViewRootImpl.checkThread (ViewRootImpl.java:6357)

Все идет нормально.

Теперь я изменил onCreate() чтобы немедленно запустить AsyncTask , и не дожидаться нажатия кнопки.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // same as above...
    new MyAsyncTask().execute(tv);
}

Приложение не закрывается, ничего в журналах, TextView теперь показывает «Boom!». на экране. Вау. Не ожидал этого.

Может быть, слишком рано в жизненном цикле Activity ? Переместите выполнение в onResume() .

@Override
protected void onResume() {
    super.onResume();
    new MyAsyncTask().execute(tv);
}

Такое же поведение, как указано выше.

Хорошо, давайте наденем его на Handler .

@Override
protected void onResume() {
    super.onResume();
    Handler handler = new Handler();
    handler.post(new Runnable() {
        @Override
        public void run() {
            new MyAsyncTask().execute(tv);
        }
    });
}

Такое же поведение снова. У меня заканчиваются идеи и вы можете попробовать postDelayed() с задержкой в ​​1 секунду:

@Override
protected void onResume() {
    super.onResume();
    Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            new MyAsyncTask().execute(tv);
        }
    }, 1000);
}

В заключение! Ожидаемое исключение:

11: 21: 36.630: E / AndroidRuntime (23922): Caused by: android.view.ViewRootImpl $ CalledFromWrongThreadException: только исходный поток, создавший иерархию представлений, может коснуться его представлений.

Вау, это связано с хронометром?

Я пробую разные задержки, и кажется, что для этого конкретного тестового прогона на этом конкретном устройстве (Nexus 4, работает 5.1) магическое число составляет 60 мс, то есть иногда генерирует исключение, иногда оно обновляет TextView как будто ничего не произошло.

Я предполагаю, что это происходит, когда иерархия представлений не была полностью создана в точке, где она была изменена AsyncTask . Это верно? Есть ли для этого лучшее объяснение? Есть ли обратный вызов для Activity который можно использовать для обеспечения полного создания иерархии представлений? Сроки связаны с проблемами.

Я нашел аналогичный вопрос здесь. Развертывание представлений потока пользовательского интерфейса в AsyncTask в doInBackground, CalledFromWrongThreadException не всегда выбрасывается, но объяснений нет.

Обновить:

Из-за запроса в комментариях и предлагаемого ответа я добавил несколько протоколов отладки, чтобы выяснить цепочку событий ...

public class MainActivity extends Activity {
    private TextView tv;

    public class MyAsyncTask extends AsyncTask<TextView, Void, Void> {
        @Override
        protected Void doInBackground(TextView... params) {
            Log.d("MyAsyncTask", "before setText");
            params[0].setText("Boom!");
            Log.d("MyAsyncTask", "after setText");
            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout layout = new LinearLayout(this);
        tv = new TextView(this);
        tv.setText("Hello world!");
        layout.addView(tv);
        Log.d("MainActivity", "before setContentView");
        setContentView(layout);
        Log.d("MainActivity", "after setContentView, before execute");
        new MyAsyncTask().execute(tv);
        Log.d("MainActivity", "after execute");
    }
}

Вывод:

10: 01: 33.126: D / MainActivity (18386): перед setContentView
10: 01: 33.137: D / MainActivity (18386): после setContentView перед выполнением
10: 01: 33.148: D / MainActivity (18386): после выполнения
10: 01: 33.153: D / MyAsyncTask (18386): перед setText
10: 01: 33.153: D / MyAsyncTask (18386): после setText

Все, как ожидалось, ничего необычного здесь, setContentView() завершено до setContentView() execute() , которое, в свою очередь, завершается до того, как setText() вызывается из doInBackground() . Вот и все.

Обновить:

Другой пример:

public class MainActivity extends Activity {
    private LinearLayout layout;
    private TextView tv;

    public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... params) {
            tv.setText("Boom!");
            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        layout = new LinearLayout(this);
        Button button = new Button(this);
        button.setText("Click!");
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv = new TextView(MainActivity5.this);
                tv.setText("Hello world!");
                layout.addView(tv);
                new MyAsyncTask().execute();
            }
        });
        layout.addView(button);
        setContentView(layout);
    }
}

На этот раз я добавляю TextView в onClick() Button непосредственно перед вызовом execute() в AsyncTask . На этом этапе исходный Layout (без TextView ) отображается правильно (т. Е. Я могу увидеть кнопку и щелкнуть ее). Опять же, никакого исключения не было.

И пример счетчика, если я добавлю Thread.sleep(100); в execute() перед setText() в doInBackground() обычное исключение.

Еще одна вещь, которую я только что заметил сейчас, заключается в том, что перед тем, как исключение будет выброшено, текст TextView фактически обновится и отобразится правильно, всего за долю секунды, пока приложение не закроется автоматически.

Я предполагаю, что что-то должно происходить (асинхронно, т.е. отделяться от любых методов / обратных вызовов жизненного цикла) к моему TextView который каким-то образом «прикрепляет» его к ViewRootImpl , что заставляет последнее генерировать исключение. У кого-нибудь есть объяснение или указатели на дальнейшую документацию о том, что это такое?