简体   繁体   English

Android上的SurfaceView方法“onTouchEvent(...)”中的ANR

[英]ANR in SurfaceView method “onTouchEvent(…)” on Android

On Android, I've subclassed SurfaceView and the resulting view is working fine in most cases. 在Android上,我已经将SurfaceView子类化,并且结果视图在大多数情况下SurfaceView正常工作。 However, roughly 1% of all users report an ANR problem with this implementation. 但是,大约1%的用户报告了此实现的ANR问题。

Apparently, there's an edge case where the SurfaceView fails due to some problem, probably a deadlock. 显然,有一个边缘情况, SurfaceView因某些问题而失败,可能是死锁。

Unfortunately, I don't know what's wrong with my implementation of onDraw(...) and onTouchEvent(...) or how to improve the code. 不幸的是,我不知道我的onDraw(...)onTouchEvent(...)实现有什么问题,或者如何改进代码。 Can you help? 你能帮我吗?

"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)

Where HandCards.onTouchEvent(SourceFile:~188) is: HandCards.onTouchEvent(SourceFile:~188)是:

synchronized (mRenderThread.getSurfaceHolder()) {

And HandCards.a(SourceFile:178) is: HandCards.a(SourceFile:178)是:

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

The full code for the SurfaceView subclass is: 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;
    }

}

Then, there's also the render thread: 然后,还有渲染线程:

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

}

The ANR is happening because your onTouchEvent() method is synchronizing on a lock held by tid=18, an unnamed thread known only as Thread-14297. ANR正在发生,因为你的onTouchEvent()方法正在由tid = 18持有的锁上进行同步,这是一个只知道为Thread-14297的未命名线程。

Many people follow a pattern where, at the point they lock the SurfaceView canvas, they also lock the SurfaceHolder object. 许多人都遵循一种模式,在这种模式下,当他们锁定SurfaceView画布时,他们还会锁定SurfaceHolder对象。 It's a bad idea to synchronize on objects with public visibility, and an even worse idea to synchronize on objects shared with the GUI framework, so it's sad that this pattern persists. 在具有公共可见性的对象上进行同步是一个坏主意,甚至更糟糕的想法是在与GUI框架共享的对象上进行同步,因此很难过这种模式持续存在。 (But I digress.) (但我离题了。)

You're drawing in an overridden onDraw() method, which doesn't make sense if you're drawing from a renderer thread -- the onDraw() method is used by the View hierarchy and would be called from the UI thread, but here it's clearly being called from elsewhere. 您正在绘制一个重写的onDraw()方法,如果您从渲染器线程绘制则没有意义 - 视图层次结构使用onDraw()方法并且将从UI线程调用,但是这里显然是从别处打来的。 You should call it something else, maybe just myDraw() . 你应该把它叫做别的东西,也许只是myDraw() (But I digress.) (但我离题了。)

Thread-14297 is in the "suspended" state, which means it was executing but stopped when the stack trace was captured. Thread-14297处于“挂起”状态,这意味着它正在执行但在捕获堆栈跟踪时停止。 Since the topmost frame is a native method -- which don't get suspended by the VM -- it was probably entering or exiting the frame. 由于最顶层的帧是本机方法 - 它不会被VM暂停 - 它可能正在进入或退出帧。 The system and user time for the thread, shown in ticks as "utm=" and "stm=" values, are fairly low, suggesting that it's not doing excessive CPU work. 线程的系统和用户时间(以“utm =”和“stm =”值为单位显示)相当低,表明它没有做过多的CPU工作。 Unless, of course, your render thread is a one-shot, in which case it was fairly busy (and may not be done yet). 当然,除非你的渲染线程是一次性的,在这种情况下,它相当繁忙(可能还没有完成)。

The good news is that you don't appear to be deadlocked. 好消息是你似乎没有陷入僵局。 The render thread is just running slowly. 渲染线程运行缓慢。 Or, perhaps, you have a loop that is failing to exit (though none is apparent from the posted code). 或者,也许,你有一个未能退出的循环(尽管从发布的代码中没有一个明显)。 On a slow device, with lots of other activity on the system, and a big mCards list, it could get starved for CPU and fail to respond quickly. 在一个速度较慢的设备上,系统上有许多其他活动,以及一个大的mCards列表,它可能会因CPU而缺乏响应并且无法快速响应。 Assuming you're following the common pattern and locking the SurfaceHolder when you grab the Canvas, your onTouchEvent() is going to lock up the UI thread for the full duration of the draw. 假设您在抓取Canvas时遵循常见模式并锁定SurfaceHolder,则onTouchEvent()将在绘制的整个持续时间内锁定UI线程。 The ANR summary in logcat usually lists recent thread activity levels; logcat中的ANR摘要通常列出最近的线程活动级别; if you have access to that, the information could tell you how busy the render thread has been. 如果您有权访问该信息,则该信息可以告诉您渲染线程的繁忙程度。

Not all ANRs are fatal. 并非所有的ANR都是致命的。 If the app becomes permanently unresponsive, that's very different from a temporary ANR that clears up when the user hits "wait". 如果应用程序永久无响应,则与用户点击“等待”时清除的临时ANR非常不同。 Do you know which kind this is? 你知道这是哪种吗?

You need to: 你需要:

  1. Re-evaluate your data synchronization. 重新评估您的数据同步。 Use shorter windows and maybe a read-write lock to pass data around. 使用较短的窗口和可能的读写锁来传递数据。 Poke around in java.util.concurrent. 在java.util.concurrent中查找。 Stalling the UI thread for an extended period is bad. 长时间停止UI线程是不好的。
  2. Determine why your rendering seems to be taking a long time, and whether it's just running slowly or spinning forever. 确定你的渲染似乎花了很长时间,以及它是慢速运行还是永远旋转。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM