Android Runnable не выполняется MainLooper
Краткое описание приложения:
- У меня есть приложение Cordova/Ionic и пользовательский плагин Cordova для выполнения собственного кода.
- Плагин содержит отдельную CameraActivity (расширяет FragmentActivity) для работы с камерой (части кода, основанные на примере Camera2Basic).
- При запуске Activity отображает AnaliseFragment, где приложение захватывает каждый кадр камеры и передает изображение в анализатор в фоновом потоке.
Шаги выполнения:
- Пользователь нажимает кнопку на Cordova UI
- Cordova выполняет собственный метод плагина с помощью cordova.exec(..)
- Собственный плагин запускает CameraActivity для результата через cordova.startActivityForResult (..)
- CameraActivity отображает AnaliseFragment
- AnaliseFragment начинает сеанс захвата камеры с двух поверхностей: первая отображается в TextureView, а вторая - в ImageAnaliser.
Проблема:
Редко и случайно пользовательский интерфейс перестает реагировать на пользователя и запускаемые элементы не выполняются в потоке пользовательского интерфейса. В то же время фоновые потоки продолжают работать в обычном режиме: выходные данные камеры видны в TextureView, а ImageAnaliser продолжает получать изображения с камеры.
Кто-нибудь есть какие-либо предложения, как найти / отладить причину такого поведения? Или есть идеи, что может вызвать это?
Я уже попробовал:
- регистрировать каждое событие жизненного цикла CameraActivity / AnaliseFragment = нет вызовов между нормальным состоянием приложения и ANR
- добавьте WAKELOCK, чтобы сохранить работоспособность Cordova MainActivity = не помогло
- регистрировать (отслеживать) каждый метод в AnalilseFragment и ImageAnaliser = ничего подозрительного
Вот упрощенный код AnaliseFragment:
public class AnaliseFragment extends Fragment {
private HandlerThread mBackgroundThread;
private Handler mBackgroundHandler;
private ImageAnalyser mImageAnalyser;
// listener is attached to camera capture session and receives every frame
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image nextImage = reader.acquireLatestImage();
mBackgroundHandler.post(() ->
try {
mImageAnalyser.AnalizeNextImage(mImage);
}
finally {
mImage.close();
}
);
}
};
@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
mImageAnalyser = new ImageAnalyser();
mImageAnalyser.onResultAvailable(boolResult -> {
// Runnable posted, but never executed
new Handler(Looper.getMainLooper()).post(() -> reportToActivityAndUpdateUI(boolResult));
});
}
@Override
public void onResume() {
super.onResume();
startBackgroundThread();
}
@Override
public void onPause() {
stopBackgroundThread();
super.onPause();
}
private void startBackgroundThread() {
if (mBackgroundThread == null) {
mBackgroundThread = new HandlerThread("MyBackground");
mBackgroundThread.start();
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}
}
private void stopBackgroundThread() {
mBackgroundThread.quitSafely();
try {
mBackgroundThread.join();
mBackgroundThread = null;
mBackgroundHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Упрощенный код для ImageAnalyser:
public class ImageAnalyser {
public interface ResultAvailableListener {
void onResult(bool boolResult);
}
private ResultAvailableListener mResultAvailableListener;
public void onResultAvailable(ResultAvailableListener listener) { mResultAvailableListener = listener; }
public void AnalizeNextImage(Image image) {
// Do heavy analysis and put result into theResult
mResultAvailableListener.onResult(theResult);
}
}
2 ответа
После нескольких часов профилирования, отладки и проверки кода я обнаружил, что
проблема была вызвана неправильным просмотром недействительности из фонового потока
Должен использоваться метод View.postInvalidate() - этот метод проверяет, все ли прикреплено View к окну, а затем делает его недействительным. Вместо этого я неправильно использовал View.invalidate(), когда обрабатывал мое пользовательское сообщение из MainLooper, что редко вызывало сбои и заставляло MainLooper прекратить обрабатывать больше сообщений.
Для тех, у кого может быть та же проблема, я добавил и правильный, и неправильный код.
ПРАВИЛЬНЫЙ:
public class GraphicOverlayView extends View { ... }
// Somewhere in background thread logic:
private GraphicOverlayView mGraphicOverlayView;
private void invalidateGraphicOverlayViewFromBackgroundThread(){
mGraphicOverlayView.postInvalidate();
};
НЕПРАВИЛЬНО:
public class GraphicOverlayView extends View { ... }
// Somewhere in background thread logic:
private GraphicOverlayView mGraphicOverlayView;
private final int MSG_INVALIDATE_GRAPHICS_OVERLAY = 1;
private Handler mUIHandler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE_GRAPHICS_OVERLAY:{
GraphicOverlayView overlay = (GraphicOverlayView)msg.obj;
// Next line can cause MainLooper stop processing other messages
overlay.invalidate();
break;
}
default:
super.handleMessage(msg);
}
}
};
private void invalidateGraphicOverlayViewFromBackgroundThread(){
Message msg = new Message();
msg.obj = mGraphicOverlayView;
msg.what = MSG_INVALIDATE_GRAPHICS_OVERLAY;
mUIHandler.dispatchMessage(msg);
};
В UI-потоке есть длительная операция. Попробуйте профилировать свое приложение, чтобы выяснить, что блокирует ваш основной поток.