1. 程式人生 > >[Android]View.post(),android7.0(sdk24以上)不執行的問題(部分Click點選事件無效的原因)

[Android]View.post(),android7.0(sdk24以上)不執行的問題(部分Click點選事件無效的原因)

我們熟知View.post()和Handler.post(),雖然最後執行過程還會走到Handler的post()方法中,但是View.post()做了許多額外的工作,所以我認為如非迫不得己,建議直接使用Handler.post()方法,詳情見此文。

如果在android7.0(sdk 24及以上)開發過程中,如果你的view沒有通過addView新增到檢視的時候,就會導致對應view的點選事件無效,以及view.post不執行,可能就是本文原因了,

以下是view.post在sdk版本24及以上的post方法

public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }

        // Postpone the runnable until we know on which thread it needs to run.
        // Assume that the runnable will be successfully placed after attach.
        getRunQueue().post(action);
        return true;
    }

下面是sdk23及以下的post方法
public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().post(action);
        return true;
    }
我們很容易的發現,24以上的sdk使用的是        getRunQueue().post(action);而sdk23以及以下是用的ViewRootImpl.getRunQueue().post(action);

其中,getRunQueue()的方法是

   /** 
     *  Returns the queue of runnable for this view.
     *
     * @return the queue of runnables for this view
     */
    private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }

這個mRunQueue是View類中一個私有變數。

ViewRootImpl可以理解是一個activity的View樹的樹根,每個ViewRootImpl管理對應的DecoView和View樹

ViewRootImpl中的佇列是一個靜態變數,也就是隻有一個這個佇列存在於這個app的生命週期中

static HandlerActionQueue getRunQueue() {
        HandlerActionQueue rq = sRunQueues.get();
        if (rq != null) {
            return rq;
        }
        rq = new HandlerActionQueue();
        sRunQueues.set(rq);
        return rq;
    }

在view.post中,並不是post完畢後就會執行,無論高低版本的View.post,只是把Runnable新增到佇列,等待進行操作,這和Handler.post不同

其中,SDK24以上是HandlerActionQueue類中

public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }

            mActions = null;
            mCount = 0;
        }
    }

sdk 23以下是ViewRootImpl的靜態方法
void executeActions(Handler handler) {
            synchronized (mActions) {
                final ArrayList<HandlerAction> actions = mActions;
                final int count = actions.size();

                for (int i = 0; i < count; i++) {
                    final HandlerAction handlerAction = actions.get(i);
                    handler.postDelayed(handlerAction.action, handlerAction.delay);
                }

                actions.clear();
            }
        }

在SDk24及以上我們可以瞭解到只有在View的dispatchAttachedToWindow方法中執行,如果這個view不是通過addview等方法加入父檢視的話,就無法呼叫dispatchAttachedToWindow,從而無法執行View.post,而post方法影響著click方法,即為
case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

從而高版本sdk24及以上可能導致點選事件失效。

而在23及以下版本中,ViewRootImpl的executeActions會頻繁的呼叫,ViewRootImpl中的TraversalRunnable進行呼叫doTraversa來()進行呼叫。而TraversalRunnable是通過Choreographer的postCallBack迴圈呼叫,這個Choreographer通過doScheduleCallback進行一個MSG_DO_SCHEDULE_CALLBACK型別的迴圈操作(它每隔一段時間操作(ms級別))。詳情請檢視以後的Choreographer文章

說完了問題原因,解決方法如下:

關於點選事件,首先一定保證對應的view已經addview到父檢視中,這樣可能解決問題,當然不一定滿足業務需求,也不一定能完美解決,那麼可以通過重寫View的對應post方法進行處理,如下

 private Handler mHandler;
    @Override
    public boolean post(Runnable action) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && action!= null && !isAttachedToWindow()) {
            mHandler = new Handler();
            return mHandler.post(action);
        }
        return super.post(action);
    }

    @Override
    public boolean removeCallbacks(Runnable action) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && action != null && !isAttachedToWindow()&& mHandler != null) {
            mHandler.removeCallbacks(action);
            return true;
        }
        return super.removeCallbacks(action);
    }