2015-03-03 2 views
3

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

По-видимому, есть краевой кейс, где 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 com.my.package.util.HandCards.onTouchEvent(SourceFile:~188) 
- waiting to lock <0x45b91988> (a android.view.SurfaceView$4) held by tid=18 (Thread-14297) 
at android.view.View.dispatchTouchEvent(View.java:7837) 
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) 
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) 
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) 
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) 
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) 
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) 
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) 
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) 
at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2075) 
at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1522) 
at android.app.Activity.dispatchTouchEvent(Activity.java:2458) 
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2023) 
at android.view.View.dispatchPointerEvent(View.java:8017) 
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:3966) 
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:3845) 
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3405) 
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3455) 
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3424) 
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3531) 
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3432) 
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3588) 
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3405) 
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3455) 
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3424) 
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3432) 
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3405) 
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5554) 
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5534) 
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5505) 
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:5634) 
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185) 
at android.os.MessageQueue.nativePollOnce(Native Method) 
at android.os.MessageQueue.next(MessageQueue.java:138) 
at android.os.Looper.loop(Looper.java:196) 
at android.app.ActivityThread.main(ActivityThread.java:5135) 
at java.lang.reflect.Method.invokeNative(Native Method) 
at java.lang.reflect.Method.invoke(Method.java:515) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:878) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694) 
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/libc.so (__futex_syscall3+8) 
#01 pc 0000f074 /system/lib/libc.so (__pthread_cond_timedwait_relative+48) 
#02 pc 0000f0d4 /system/lib/libc.so (__pthread_cond_timedwait+64) 
#03 pc 0005655f /system/lib/libdvm.so 
#04 pc 00056b21 /system/lib/libdvm.so (dvmChangeStatus(Thread*, ThreadStatus)+34) 
#05 pc 00050fd7 /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+406) 
#06 pc 00000214 /dev/ashmem/dalvik-jit-code-cache (deleted) 
at android.graphics.Canvas.native_drawBitmap(Native Method) 
at android.graphics.Canvas.drawBitmap(Canvas.java:1202) 
at com.my.package.util.HandCards.a(SourceFile:178) 
at com.my.package.util.HandCards.onDraw(SourceFile:136) 
at com.my.package.util.d.run(SourceFile:36) 

Где 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; 
    } 

} 

T курица, есть также вынести нить:

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

} 
+0

Это выстрел в темноте, ссылка SDK упоминает использование рабочих потоков вместо использования системного потока, чтобы предотвратить блокировку потока пользовательского интерфейса системы, вызывающего ANR. – user4317867

ответ

6

НРУ происходит потому, что ваш метод 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. Определите, почему ваш рендеринг, кажется, занимает много времени, и работает ли он медленно или вращается навсегда.
+0

Спасибо большое! Я добавил код моего потока рендеринга к вопросу. Как вы можете видеть, я действительно вызываю 'onDraw()' там. Что касается вашего второго «Но я отвлекся», я должен перестать вызывать 'onDraw()' из потока и просто надеяться, что система автоматически вызовет 'onDraw()' автоматически? Или мне нужно переименовать как определение, так и вызов только «draw()» без какого-либо переопределения метода onDraw()? Я думал, что если я использую поток рендеринга, не важно, длится ли чертеж или нет. Для этого нужен поток, не так ли? – caw

+0

И что касается вашего второго «Но я отвлекся», какие изменения я должен применить? Какие блокировки следует удалить и какие объекты следует синхронизировать, вместо этого? – caw

+0

Для # 1 'onDraw()' используется для пользовательских представлений (http://developer.android.com/training/custom-views/index.html). Поскольку вы используете Surface, а не View, вы не хотите переопределять 'onDraw()'. Просто переименуйте его. # 2 сложнее ответить, так как невозможно сказать, что вам нужно, не зная точно, что делает ваша программа. Однако нет необходимости блокировать SurfaceHolder - метод 'lockCanvas()' предотвращает SurfaceView от вытаскивания Surface из-под вас. Создайте объект, который доступен только для ваших потоков, и удерживайте блокировку как можно более короткое время. – fadden