2015-03-03 60 views
3

在Android上,我已將子類別SurfaceView並在大多數情況下生成的視圖工作正常。但是,大約有1%的用戶在這個實現中報告了ANR問題。Android中的SurfaceView方法「onTouchEvent(...)」中的ANR

顯然,有一個邊緣情況,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; 
    } 

} 

ŧ母雞,另外還有渲染線程:

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的系統UI線程。 – user4317867 2015-03-03 03:28:13

回答

6

的ANR正在發生的事情,因爲你的onTouchEvent()方法對TID = 18,僅已知線程14297一位不願透露姓名的線程持有的鎖同步。

許多人都遵循這樣的模式:在鎖定SurfaceView畫布的時候,他們也鎖定了SurfaceHolder對象。在具有公開可見性的對象上同步是一個糟糕的主意,而且與GUI框架共享的對象同步的想法更加糟糕,所以這種模式依然存在,這很可悲。 (但是我離題了)

您正在重寫onDraw()方法,如果您從渲染器線程進行繪製時沒有任何意義 - 方法由View層次結構使用,並且將調用onDraw()方法從UI線程,但這裏顯然是從其他地方調用。你應該把它叫做別的,也許只是myDraw()。 (但我離題了)。

線程14297處於「掛起」狀態,這意味着它正在執行但在捕獲到堆棧跟蹤時停止。由於最上面的幀是本地方法 - 不會被VM暫停 - 它可能正在進入或退出幀。線程的系統和用戶時間以「utm =」和「stm =」值顯示,相當低,表明它沒有執行過多的CPU工作。當然,除非你的渲染線程是一次性的,在這種情況下它相當繁忙(並且可能還沒有完成)。

好消息是,你似乎並沒有陷入僵局。渲染線程正在緩慢運行。或者,也許你有一個無法退出的循環(雖然從發佈的代碼中看不到)。在一臺速度較慢的設備上,系統上有很多其他活動,以及一個大的列表,它可能會因爲CPU而餓死,並且無法快速響應。假設您在抓取Canvas時遵循常見模式並鎖定了SurfaceHolder,則onTouchEvent()將在整個繪製期間鎖定UI線程。 logcat中的ANR總結通常會列出最近的線程活動級別;如果您有權訪問該信息,則該信息可能會告訴您渲染線程的忙碌程度。

並非所有的ANR都是致命的。如果應用程序永久無響應,則與用戶點擊「等待」時清除的臨時ANR完全不同。你知道這是哪種嗎?

您需要:

  1. 重新評估您的數據同步。使用較短的窗口,並可能使用讀寫鎖來傳遞數據。在java.util.concurrent中打個比方。延長UI線程很長時間是不好的。
  2. 確定渲染看起來花費很長時間的原因,以及它是否運行緩慢或永久旋轉。
+0

非常感謝!我已將渲染線程的代碼添加到問題中。正如你所看到的,我確實在那裏調用'onDraw()'。至於你的第二個「但我離題了」,我應該停止從線程調用'onDraw()',並希望系統自動調用'onDraw()'?或者,我應該將定義和調用重命名爲「draw()',而沒有任何重寫'onDraw()'方法呢?我認爲,如果我使用渲染線程,繪圖是否需要很長時間並不重要。這是線程的目的,不是嗎? – caw 2015-03-03 14:56:31

+0

至於你的第二個「但我離題」,我應該改變什麼?我應該刪除哪些鎖以及應該同步哪些對象? – caw 2015-03-03 14:57:45

+0

對於#1,'onDraw()'用於自定義視圖(http://developer.android.com/training/custom-views/index.html)。由於你在Surface上繪圖,而不是視圖,所以你不想重寫'onDraw()'。只需重命名它。 #2更難回答,因爲如果不知道程序的功能,就無法說出所需的全部內容。然而,沒有必要鎖定SurfaceHolder - 'lockCanvas()'方法可以防止SurfaceView將Surface拖出你。創建一個只對你的線程可見的對象,並儘可能短地保持鎖定。 – fadden 2015-03-03 16:44:04