ANR в методе SurfaceView «onTouchEvent (…)» на Android

На Android я подклассифицировал SurfaceView и в SurfaceView итоге представление работает нормально в большинстве случаев. Однако примерно 1% всех пользователей сообщают о проблеме ANR с этой реализацией.

По-видимому, есть SurfaceView случай, когда SurfaceView терпит неудачу из-за некоторой проблемы, возможно, тупика.

К сожалению, я не знаю, что случилось с моей реализацией onDraw(...) и onTouchEvent(...) или как улучшить код. Вы можете помочь?

 "main" prio=5 tid=1 MONITOR | group="main" sCount=1 dsCount=0 obj=0x41920e88 self=0x4190f8d0 | sysTid=13407 nice=0 sched=0/0 cgrp=apps handle=1074618708 | state=S schedstat=( 50780242971 27570770290 130442 ) utm=4254 stm=824 core=0 at - waiting to lock <0x45b91988> (a android.view.SurfaceView$4) held by tid=18 (Thread-14297) at android.view.View.dispatchTouchEvent( at android.view.ViewGroup.dispatchTransformedTouchEvent( at android.view.ViewGroup.dispatchTouchEvent( at android.view.ViewGroup.dispatchTransformedTouchEvent( at android.view.ViewGroup.dispatchTouchEvent( at android.view.ViewGroup.dispatchTransformedTouchEvent( at android.view.ViewGroup.dispatchTouchEvent( at android.view.ViewGroup.dispatchTransformedTouchEvent( at android.view.ViewGroup.dispatchTouchEvent( at$DecorView.superDispatchTouchEvent( at at at$DecorView.dispatchTouchEvent( at android.view.View.dispatchPointerEvent( at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent( at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess( at android.view.ViewRootImpl$InputStage.deliver( at android.view.ViewRootImpl$InputStage.onDeliverToNext( at android.view.ViewRootImpl$InputStage.forward( at android.view.ViewRootImpl$AsyncInputStage.forward( at android.view.ViewRootImpl$InputStage.apply( at android.view.ViewRootImpl$AsyncInputStage.apply( at android.view.ViewRootImpl$InputStage.deliver( at android.view.ViewRootImpl$InputStage.onDeliverToNext( at android.view.ViewRootImpl$InputStage.forward( at android.view.ViewRootImpl$InputStage.apply( at android.view.ViewRootImpl$InputStage.deliver( at android.view.ViewRootImpl.deliverInputEvent( at android.view.ViewRootImpl.doProcessInputEvents( at android.view.ViewRootImpl.enqueueInputEvent( at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent( at android.view.InputEventReceiver.dispatchInputEvent( at android.os.MessageQueue.nativePollOnce(Native Method) at at android.os.Looper.loop( at at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke( at$ at at dalvik.system.NativeStart.main(Native Method) ... "Thread-14297" prio=5 tid=18 SUSPENDED | group="main" sCount=1 dsCount=0 obj=0x45ba6358 self=0x76036b38 | sysTid=21120 nice=0 sched=0/0 cgrp=apps handle=1979936656 | state=S schedstat=( 48296386737 3088012659 22649 ) utm=4691 stm=138 core=0 #00 pc 00021adc /system/lib/ (__futex_syscall3+8) #01 pc 0000f074 /system/lib/ (__pthread_cond_timedwait_relative+48) #02 pc 0000f0d4 /system/lib/ (__pthread_cond_timedwait+64) #03 pc 0005655f /system/lib/ #04 pc 00056b21 /system/lib/ (dvmChangeStatus(Thread*, ThreadStatus)+34) #05 pc 00050fd7 /system/lib/ (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+406) #06 pc 00000214 /dev/ashmem/dalvik-jit-code-cache (deleted) at Method) at at at at 

Где HandCards.onTouchEvent(SourceFile:~188) :

 synchronized (mRenderThread.getSurfaceHolder()) { 

И HandCards.a(SourceFile:178) :

 canvas.drawBitmap(drawCardBitmap, null, mDrawingRect, mGraphicsPaint); 

Полный код для подкласса SurfaceView :

 public class HandCards extends SurfaceView implements SurfaceHolder.Callback { /** Opacity of the shadow layer that hides other cards when one card is highlighted and covers all cards when it's another player's turn (where 0 is transparent and 255 is opaque) */ private static final int SHADOW_ALPHA = 150; private static SparseArray<Bitmap> mCardCache = new SparseArray<Bitmap>(); // cache array for own card bitmaps private HandThread mRenderThread; private volatile List<Card> mCards; private volatile int mCardCount; private volatile int mScreenWidth; private volatile int mScreenHeight; private volatile int mCardWidth; private volatile int mCardHeight; private volatile int mHighlightedCard = -1; private CardClickCallback mCardClickCallback; private volatile int mBlattID = 1; private volatile int mCurrentCardSpacing; private final Paint mGraphicsPaint; private final Paint mShadowPaint; private final Rect mDrawingRect; private volatile int mTouchEventAction; private volatile int mTouchEventCard; private Bitmap drawCardBitmap; private volatile int mOnDrawX1; private final BitmapFactory.Options mBitmapOptions; private volatile boolean mIsActive = true; private final int[] mCardSelection = new int[GameState.MAX_SWAP_CARDS]; /** Indicates that the card view is currently used for choosing some cards to create a selection */ private volatile boolean mIsChooseMode; /** Holds the index of the selected card that will be replaced next if all selection slots are full */ private volatile int mNextReplacePosition; /** Used only locally in drawCard() but is declared here to save repeated allocations */ private volatile int mCardOffsetY; private volatile int mRequiredSelectionCount; public HandCards(Context activityContext, AttributeSet attributeSet) { super(activityContext, attributeSet); getHolder().addCallback(this); setFocusable(true); // touch events should be processed by this class mCards = new ArrayList<Card>(); mGraphicsPaint = new Paint(); mGraphicsPaint.setAntiAlias(true); mGraphicsPaint.setFilterBitmap(true); mShadowPaint = new Paint(); mShadowPaint.setARGB(SHADOW_ALPHA, 20, 20, 20); mShadowPaint.setAntiAlias(true); mBitmapOptions = new BitmapFactory.Options(); mBitmapOptions.inInputShareable = true; mBitmapOptions.inPurgeable = true; mDrawingRect = new Rect(); } public Card getCard(int location) throws Exception { if (mCards != null) { synchronized (mCards) { return mCards.get(location); // card may not be found (throw exception then) } } return null; } public static Bitmap cardCacheGet(int key) { synchronized (mCardCache) { return mCardCache.get(key); } } public static void cardCachePut(int key, Bitmap object) { synchronized (mCardCache) { mCardCache.put(key, object); } } public int[] getSelectedCards() { return mCardSelection; } public void setActive(boolean active) { if (mCardSelection != null) { for (int i = 0; i < GameState.MAX_SWAP_CARDS; i++) { // loop through all slots for selected cards mCardSelection[i] = -1; // unset the slot so that it is empty by default } } mIsActive = active; } public boolean isActive() { return mIsActive; } public void setChooseMode(boolean active, int swapCardCount) { mNextReplacePosition = 0; mIsChooseMode = active; mRequiredSelectionCount = swapCardCount; } public boolean isChooseMode() { return mIsChooseMode; } public void stopThread() { if (mRenderThread != null) { mRenderThread.setRunning(false); } } @Override public void onDraw(Canvas canvas) { if (canvas != null) { synchronized (mCards) { mCardCount = mCards.size(); canvas.drawColor(Color.BLACK); if (mCardCount > 0) { mCurrentCardSpacing = Math.min(mScreenWidth/mCardCount, mCardWidth); for (int c = 0; c < mCardCount; c++) { if (c != mHighlightedCard || !isActive()) { try { drawCard(canvas, mCards.get(c).getDrawableID(mBlattID), false, c*mCurrentCardSpacing, c*mCurrentCardSpacing+mCardWidth, c); } catch (Exception e) { } } } if (mHighlightedCard > -1 && isActive()) { mOnDrawX1 = Math.min(mHighlightedCard*mCurrentCardSpacing, mScreenWidth-mCardWidth); try { drawCard(canvas, mCards.get(mHighlightedCard).getDrawableID(mBlattID), true, mOnDrawX1, mOnDrawX1+mCardWidth, mHighlightedCard); } catch (Exception e) { } } else if (!isActive()) { drawCard(canvas, 0, true, 0, mScreenWidth, 0); } } } } } private void drawCard(Canvas canvas, int resourceID, boolean highlighted, int xLeft, int xRight, int cardPosition) { if (canvas != null) { try { if (highlighted) { canvas.drawRect(0, 0, mScreenWidth, mScreenHeight, mShadowPaint); } if (resourceID != 0) { drawCardBitmap = cardCacheGet(resourceID); if (drawCardBitmap == null) { drawCardBitmap = BitmapFactory.decodeResource(getResources(), resourceID, mBitmapOptions); cardCachePut(resourceID, drawCardBitmap); } mCardOffsetY = 0; // by default draw all cards right at the bottom (without highlighting by position) if (mCardSelection != null) { for (int i = 0; i < GameState.MAX_SWAP_CARDS; i++) { // loop through all slots for selected cards if (mCardSelection[i] == cardPosition) { // if current card has been selected (in that slot) mCardOffsetY = mScreenHeight*1/4; // lift the card by one quarter to highlight it break; // card has already been detected to be selected so stop here } } } mDrawingRect.set(xLeft, mCardOffsetY, xRight, mCardHeight+mCardOffsetY); canvas.drawBitmap(drawCardBitmap, null, mDrawingRect, mGraphicsPaint); } } catch (Exception e) { } } } @Override public boolean onTouchEvent(MotionEvent event) { if (mRenderThread == null) { return false; } synchronized (mRenderThread.getSurfaceHolder()) { // synchronized so that there are no concurrent accesses mTouchEventAction = event.getAction(); if (isActive()) { if (mTouchEventAction == MotionEvent.ACTION_DOWN || mTouchEventAction == MotionEvent.ACTION_MOVE) { if (event.getY() >= 0 && event.getY() < mScreenHeight) { mTouchEventCard = (int) event.getX()/mCurrentCardSpacing; if (mTouchEventCard > -1 && mTouchEventCard < mCardCount) { mHighlightedCard = mTouchEventCard; } else { mHighlightedCard = -1; } } else { mHighlightedCard = -1; } } else if (mTouchEventAction == MotionEvent.ACTION_UP) { if (mCardClickCallback != null && mHighlightedCard > -1 && mHighlightedCard < mCardCount) { if (isChooseMode()) { // card has been chosen as a swap card int freeSelectionIndex = -1; // remember the index of a free selection slot (default = none available) for (int i = 0; i < mRequiredSelectionCount; i++) { // loop through all allowed slots for selected cards if (mCardSelection[i] == mHighlightedCard) { // if this card has already been selected mCardSelection[i] = -1; // unselect the card freeSelectionIndex = -2; // mark that there is no need to select a new card break; // slot of current card has already been found so stop here } else if (mCardSelection[i] == -1 && freeSelectionIndex == -1) { // if slot is still available and no free slot has been found yet freeSelectionIndex = i; // remember the index of this free slot } } if (freeSelectionIndex > -2) { // if a new card is to be placed in the selection array if (freeSelectionIndex >= 0) { // if a free slot was available mCardSelection[freeSelectionIndex] = mHighlightedCard; // just place the card there } else { // if no free slot was available anymore mCardSelection[mNextReplacePosition] = mHighlightedCard; // replace another card in one of the slots mNextReplacePosition = (mNextReplacePosition+1) % mRequiredSelectionCount; // advance the cursor that points to the slot which will be replaced next } } } else { // card has been selected to be played on the table try { mCardClickCallback.chooseCard(mCards.get(mHighlightedCard)); } catch (Exception e) { // index was out of mCards' bounds (just ignore this, user may tap on card again) } } } mHighlightedCard = -1; } } else { try { mCardClickCallback.resyncManually(); } catch (Exception e) { } } } return true; } @Override public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { } public void setCards(List<Card> currentCards) { synchronized (mCards) { mCards.clear(); mCards.addAll(currentCards); } } @Override public void surfaceCreated(SurfaceHolder arg0) { mScreenWidth = getWidth(); mScreenHeight = getHeight(); mCardHeight = mScreenHeight; mCardWidth = mCardHeight*99/150; mCurrentCardSpacing = mCardWidth; mRenderThread = new HandThread(getHolder(), this); mRenderThread.setRunning(true); mRenderThread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { boolean retry = true; mRenderThread.setRunning(false); // stop thread while (retry) { // wait for thread to close try { mRenderThread.join(); retry = false; } catch (InterruptedException e) { } } } public synchronized void setCardClickCallback(CardClickCallback callback) { mCardClickCallback = callback; } public void setBlattID(int blattID) { mBlattID = blattID; } } 

Затем есть также поток рендеринга:

 public class HandThread extends Thread { private final SurfaceHolder mSurfaceHolder; private final HandCards mSurface; private volatile boolean mRunning = false; public HandThread(SurfaceHolder surfaceHolder, HandCards surface) { mSurfaceHolder = surfaceHolder; mSurface = surface; } public SurfaceHolder getSurfaceHolder() { return mSurfaceHolder; } public void setRunning(boolean run) { mRunning = run; } @Override public void run() { Canvas c; while (mRunning) { c = null; try { c = mSurfaceHolder.lockCanvas(null); synchronized (mSurfaceHolder) { if (c != null) { mSurface.onDraw(c); } } } finally { // when exception is thrown above we may not leave the surface in an inconsistent state if (c != null) { try { mSurfaceHolder.unlockCanvasAndPost(c); } catch (Exception e) { } } } } } } 

ANR происходит потому, что ваш onTouchEvent() синхронизируется с блокировкой, удерживаемой tid = 18, неназванным потоком, известным только как Thread-14297.

Многие люди следуют шаблону, где в момент блокировки холста SurfaceView они также блокируют объект SurfaceHolder. Плохая идея синхронизировать объекты с общедоступной видимостью и еще худшую идею синхронизации на объектах, совместно используемых с графическим интерфейсом, поэтому печально, что этот шаблон сохраняется. (Но я отвлекаюсь.)

Вы рисуете переопределенный onDraw() , который не имеет смысла, если вы рисуете из потока рендеринга – метод onDraw() используется иерархией View и вызывается из потока пользовательского интерфейса, но Здесь его явно называют из другого места. Вы должны назвать это чем-то другим, может быть, просто myDraw() . (Но я отвлекаюсь.)

Thread-14297 находится в состоянии «приостановлено», что означает, что он выполнялся, но останавливался при захвате трассировки стека. Поскольку самый верхний кадр является нативным методом, который не подведет VM, он, вероятно, входит в кадр или выходит из него. Системное и пользовательское время потока, показанное в тиках как значения «utm =» и «stm =», довольно низки, что говорит о том, что он не делает чрезмерной работы ЦП. Если, конечно, поток рендеринга не является одним выстрелом, и в этом случае он был довольно занят (и, возможно, еще не сделан).

Хорошей новостью является то, что вы, похоже, не зашли в тупик. Поток рендеринга работает медленно. Или, может быть, у вас есть цикл, который не может выйти (хотя из опубликованного кода ничего не видно). На медленном устройстве с большим количеством других действий в системе и большом списке mCards он может стать голодным для процессора и не сможет быстро реагировать. Предполагая, что вы следуете общей схеме и блокируете SurfaceHolder, когда вы захватываете Canvas, ваш onTouchEvent() будет блокировать поток пользовательского интерфейса для полной продолжительности розыгрыша. В сводке ANR в logcat обычно перечислены последние уровни активности потоков; Если у вас есть доступ к этому, информация может рассказать вам, насколько занят поток рендеринга.

Не все ANR смертельны. Если приложение перестает отвечать на запросы, это сильно отличается от временного ANR, который очищается, когда пользователь нажимает «wait». Вы знаете, какой это вид?

Тебе нужно:

  1. Переоцените синхронизацию данных. Используйте более короткие окна и, возможно, блокировку чтения и записи для передачи данных. Постучите в java.util.concurrent. Задержка потока пользовательского интерфейса в течение длительного периода – это плохо.
  2. Определите, почему ваш рендеринг, кажется, занимает много времени, и работает ли он медленно или вращается навсегда.
Intereting Posts
Как исключить зависимость для аромата в градиенте Как создать сертификат для моего Android Market APK? Android Не удается привязать кнопку FAB к BottomSheet Не может получить доступ к XmlRpcController, XmlRpcHttpRequestConfig FirebaseRecyclerAdapter и умножить типы элементов на android Как я могу эмулировать высоту кнопок (тень) в Android API ниже 21? Настройка цвета панели действий и текста, когда приложение отображается в списке фоновых приложений Есть ли простой способ получить уведомление об авторских правах, автоматически добавленное студией Android? Как записать экран Android с Android Studio? As3 AIR for android – Содержимое приложения-песочницы ОШИБКА Получить имя файла и путь от URI от медиастора Как избежать отображения перекрытия в относительной компоновке в android? Комплект разработчика BTLE (Bluetooth Low energy) – должен иметь профиль близости Проверьте, подключено ли приложение Android к серверу, которое вы сделали Как сделать режим чтения мягкой информации для фрагментов