美文网首页
崩溃分析: resetCancelNextUpFlag 空指针

崩溃分析: resetCancelNextUpFlag 空指针

作者: Shawon | 来源:发表于2018-01-14 05:16 被阅读1365次

    前言

    今天收到一份崩溃日志,经过一番分析之后,找到了原因,但对于这个崩溃,由于堆栈暴露的信息不足,项目代码又多又乱,基本无法定位崩溃代码,要定位代码,就是体力活,也没什么好方法。尽管如此,分析过程还是值得分享。

    堆栈

    java.lang.NullPointerException: Attempt to read from field 'int android.view.View.mPrivateFlags' on a null object reference
        at android.view.ViewGroup.resetCancelNextUpFlag(ViewGroup.java:2779)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2678)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3022)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2680)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3022)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2680)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3022)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2680)
        at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:536)
        at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1829)
        at android.app.Dialog.dispatchTouchEvent(Dialog.java:815)
        at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:422)
        at android.view.View.dispatchPointerEvent(View.java:12067)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4976)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4786)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4318)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4371)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4337)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4464)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4345)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4521)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4318)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4371)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4337)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4345)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4318)
        at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6912)
        at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6886)
        at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6843)
        at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7030)
        at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:186)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:325)
        at android.os.Looper.loop(Looper.java:142)
        at android.app.ActivityThread.main(ActivityThread.java:6523)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:857)
    

    本地不知道如何重现,只知道崩溃机器ROM版本为Android 8.0或8.1。

    分析

    遇到这种崩溃就会比较蛋疼,一方面是崩溃点在系统源码里,另一方面崩溃堆栈毫不涉及项目代码。这也就导致即便我们搞清楚了崩溃原因,也很难查清楚具体是项目里的哪行代码导致的崩溃。这就是我这次遇到的情况。

    直接看崩溃堆栈的前面三行:

    java.lang.NullPointerException: Attempt to read from field 'int android.view.View.mPrivateFlags' on a null object reference
        at android.view.ViewGroup.resetCancelNextUpFlag(ViewGroup.java:2779)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2678)
    

    这种问题肯定是要看源码的,我看的是8.0.0_r36版本的源码。
    幸运的是崩溃的原因很清晰,就是下面的view为null,也就是说参数传进来就是null。

        private static boolean resetCancelNextUpFlag(@NonNull View view) {
            if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
                view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
                return true;
            }
            return false;
        }
    

    dispatchTouchEvent中有三处对resetCancelNextUpFlag的调用,需要确定到底是哪个调用点崩了,由于崩溃点传参是null,因此只需要着重看那些参数可能为null的调用点。

    第一处

    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;
    

    直接传this,一定不为null,所以不是崩溃点。

    第二处

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }
    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }
    resetCancelNextUpFlag(child);
    

    child首先作为参数传给了canViewReceivePointerEvents,如果child为null,早就崩了,因此传给resetCancelNextUpFlag的时候一定不为null,这里也不是崩溃点。

    那剩下的那一处resetCancelNextUpFlag调用就是崩溃点了,这是好消息,缩小了调查范围。

    第三处(崩溃点)

    崩溃点附近的代码如下:

    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
    

    可以看出,当崩溃发生时,target.child是null,那么我们的重点就在target.child是如何变为null的,首先就需要知道target是干什么的。

    TouchTarget

    由于我们的手机屏幕都支持多点触控,那么当用户同时用多个手指触摸屏幕的时候,产生的TouchEvent中就包含了多个触摸点的信息,一般我们把单个的触摸点就叫pointer,每个pointer都有它的pointer id。
    TouchTarget用于记录一个被触摸的View,以及它所捕获的全部pointer。说白了,就是一个能处理TouchEvent的View,加上它处理的TouchEvent所属pointer的id。一个View能处理多个pointer产生的TouchEvent。
    同时,在有多个pointer的情况下,不同的pointer产生的TouchEvent可能需要给不同的View处理,因此需要多个TouchTarget来记录这些信息,这些TouchTarget以链表的形式组织,每个TouchTarget都有一个next变量,指向另一个TouchTarget,链表尾的指向null。而TouchTarget的child变量,就是处理TouchEvent的View。

    TouchTarget的添加

    dispatchTouchEvent方法中,会通过dispatchTransformedTouchEvent将调整后的TouchEvent派发给子View,如果子View感兴趣,会返回true,此时就会把该子View和它感兴趣的TouchEvent的pointer存储到TouchTarget中,加入链表作为表头存储,mFirstTouchTarget指向表头。这里有一点关键信息,后添加的TouchTarget是表头,也就是说,当我们先按住A按钮不松开,再按住B按钮不松开,此时表头的TouchTarget中child值指向B按钮。

    TouchTarget的删除

    当一个TouchTarget不捕获任何pointer的时候,如按在该View上的所有手指抬起时,该TouchTarget就会从链表中删除,并且执行recycle操作。
    当调用ViewGroup#removeView移除某个子View时,ViewGroup会调用下面的方法,该方法不仅从链表中删除了TouchTarget,调用其recycle方法,还给它保存的View发了一个ACTION_CANCEL事件,使得View能清理各类状态。

    private void cancelTouchTarget(View view) {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (target.child == view) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                final long now = SystemClock.uptimeMillis();
                MotionEvent event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                view.dispatchTouchEvent(event);
                event.recycle();
                return;
            }
            predecessor = target;
            target = next;
        }
    }
    

    child置空时机

    TouchTarget#recycle被调用时,child被置空。

    public void recycle() {
        if (child == null) {
            throw new IllegalStateException("already recycled once");
        }
        synchronized (sRecycleLock) {
            ......
            child = null;
        }
    }
    

    崩溃点反推

    经过上面的分析,TouchTarget链表的增减逻辑正确,且所有的节点的recycle是与删除一起做的,这些操作都是在主线程完成的,因此只要是从链表中拿到的节点,child一定不为null。现在回头看一下崩溃点,在崩溃点反推代码的执行逻辑。

    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
    

    现在假设resetCancelNextUpFlag处的参数target.child为null,反推代码执行情况。
    while循环实际上就是顺着链表在分发TouchEvent。


    第一轮循环

    我们知道TouchTarget链表维护正确,因此当我们获取表头的时候,它的child一定不为null,所以至少是在循环体第二次执行的时候target.child才为null。循环体第二次执行的时候,target实际上是第一次循环中的next指向的B。


    第二轮循环

    在第二轮循环的时候target.child为null,则需要第一轮循环时next指向的TouchTarget的child变为null,当B被removeView的时候,就会导致next指向的TouchTarget被recycle,导致next.child为null,继而使得第二轮循环时target.child为null
    在第一次循环的时候B有可能被removeView吗?
    显然ViewGroup自身是不会在这种时候更改View的结构,但是方法中执行了dispatchTransformedTouchEvent,这个方法会分发TouchEvent给子View,子View的dispatchTouchEventonTouchEvent会得到执行,如果子View中尝试removeView,恰好移除next.child指向的View B,此时ViewGroup会删除链表中第二个节点,调整链表。

    第一轮循环:B被删除

    A链向C,A与B的链被切断。B的recycle被调用,B的child为null,因为在dispatchTransformedTouchEvent执行之前,next就已经指向B了,相当于next.child被置null。在第二次循环的时候,next被赋值给target,此时target.child为null,导致resetCancelNextUpFlag的参数为null。
    由于在第二轮循环就能发生空指针,因此只需要两个TouchTarget就能复现崩溃:

    两个TouchTarget即可复现

    崩溃复现

    理论推导完了,就需要实际写代码验证一下,我们需要两个可以处理TouchEvent的View,直接用两个Button就行,一个叫ButtonA,一个叫ButtonB,将这两个按钮添加到同一个ViewGroup中,在ButtonA的onTouchEvent中(dispatchTouchEvent也可以),当发现ACTION_UP时尝试从ViewGroup中删除ButtonB,为了方便,直接用一个Dialog来展示这个ViewGroup。代码如下:

    Dialog dialog = new Dialog(context);
    final LinearLayout linearLayout = new LinearLayout(context);
    linearLayout.setOrientation(LinearLayout.VERTICAL);
    final Button btnB = new Button(context);
    btnB.setText("ButtonB");
    linearLayout.addView(btnB, getParam());
    Button btnA = new Button(context) {
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                linearLayout.removeView(btnB);
            }
            return super.onTouchEvent(event);
        }
    };
    btnA.setText("ButtonA");
    linearLayout.addView(btnA, getParam());
    dialog.setContentView(linearLayout);
    dialog.show();
    

    运行的话可以看到如下界面,有两个按钮:


    测试Dialog

    由于被删除的是ButtonB,因此TouchTarget的表头应当是ButtonA,这个和上面的理论推导是一致的。
    我们之前提到过后添加到链表里的TouchTarget是表头,也就是说我们需要ButtonA后添加到TouchTarget,因此我们应该这样操作来复现崩溃:

    1. 按住ButtonB,不松手
    2. 按住ButtonA,不松手
    3. 抬起按在ButtonA上的手指,触发removeView

    在我们抬手指的一瞬间,App就会崩溃,崩溃堆栈如下:

    java.lang.NullPointerException: Attempt to read from field 'int android.view.View.mPrivateFlags' on a null object reference
        at android.view.ViewGroup.resetCancelNextUpFlag(ViewGroup.java:2743)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2648)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2961)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2650)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2961)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2650)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2961)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2650)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2961)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2650)
        at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:445)
        at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1828)
        at android.app.Dialog.dispatchTouchEvent(Dialog.java:815)
        at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:407)
        at android.view.View.dispatchPointerEvent(View.java:11960)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4776)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4590)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4128)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4181)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4147)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4274)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4155)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4331)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4128)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4181)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4147)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4155)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4128)
        at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6642)
        at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6616)
        at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6577)
        at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6745)
        at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:325)
        at android.os.Looper.loop(Looper.java:142)
        at android.app.ActivityThread.main(ActivityThread.java:6541)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
    

    基本上崩溃堆栈是一模一样的。当然,最好用原生ROM测试,我用小米4,一直不崩,搞的我一度怀疑我是不是没看清楚源码。换了Nexus Android 8.0之后立马就崩了。

    当然也有其他方式可以干扰TouchTarget链表,removeView是开发者最容易利用的手段,因此基本可以确定崩溃的原因是在某些界面上,存在一个ViewA的dispatchTouchEvent或者onTouchEvent中尝试remove一个ViewB的情况,且ViewB所属TouchTarget刚好在TouchTarget链表中排在ViewA所属TouchTarget的后面。

    结论

    在事件分发时removeView导致该崩溃。
    不要在事件分发,绘制,动画这类调用中removeView,否则很容易遇到这种ViewGroup内部崩溃。实在要removeView,可以往主线程post一下,并做好判空。

    附录

    之前只是对事件分发有大概的印象,为了分析这个崩溃,仔细看了事件分发的代码,放在这里给自己参考。
    Android 8.0.0_r36 ViewGroup#dispatchTouchEvent源码阅读笔记:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {// 这个只是用来检查TouchEvent是否连贯,不影响事件分发流程分析。
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }
     
        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {// 看注释即可,事件的这个属性标志该事件是系统合成的,且要优先给accessibility view,如果当前view具有这种属性,就会清除标志。
            ev.setTargetAccessibilityFocus(false);
        }
     
        boolean handled = false;// handled变量用于记录事件是否被处理了,返回给方法调用者,指示事件是否被处理了。
        if (onFilterTouchEventForSecurity(ev)) {// 这里是一个较弱的检测,当View设置窗口有遮罩时滤除事件,且当前event带有窗口被遮罩的标识时,返回false,即该逻辑会导致所有分发到这里的事件被忽略掉。
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;// 本质上就是获取action,因为action里面有可能还有pointer_index信息,因此需要与ACTION_MASK做&运算
     
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {// 如果是ACTION_DOWN事件,意味着是最开始的事件,做一些清理工作,如注释所说,framework有可能没有分发up和cancel等事件过来。
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);// 如果mFirstTouchTarget不为null,就会将事件分发给所有的TouchTarget,TouchTarget采用链表连接。分发完之后将TouchTarget链表置空,将mFirstTouchTarget置空。
                // 将TouchTarget链表置空,将mFirstTouchTarget置空。重置PFLAG_CANCEL_NEXT_UP_EVENT标识,重置FLAG_DISALLOW_INTERCEPT,重置mNestedScrollAxes为SCROLL_AXIS_NONE
                resetTouchState();
            }
     
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 没有mFirstTouchTarget或者是ACTION_DOWN,且目前没有disallowIntercept,就会询问onInterceptTouchEvent是否拦截,onInterceptTouchEvent会在特定情况下返回true,比如是ACTION_DOWN的情况
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {// 如果已经禁止拦截,则置intercepted false
                    intercepted = false;
                }
            } else {// 其他情况,我们都要拦截
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
     
            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {// 看上面注释即可,依然是取消FLAG_TARGET_ACCESSIBILITY_FOCUS
                ev.setTargetAccessibilityFocus(false);
            }
     
            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)// 要么是ViewGroup自己要求下次事件过来要取消,要么事件本身就是ACTION_CANCEL,都会给canceled赋值true
                    || actionMasked == MotionEvent.ACTION_CANCEL;
     
            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;// split表示ViewGroup会在合适的时候把事件分给多个子view
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {// 既没有cancel掉,也不拦截的时候,执行以下分发逻辑
     
                // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()// 如果事件要求优先给accessibilityFocus View处理,我们就找一个出来。
                        ? findChildWithAccessibilityFocus() : null;
     
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)// idBitsToAssign用于在一个变量里存PointerId,变量中可以存许多PointerId,idBitsToAssgin相当于一个flag值。
                            : TouchTarget.ALL_POINTER_IDS;
     
                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    // 顺着mFirstTouchTarget链表往后,挨个对比,如果有对应的idBitsToAssign,移除对应的bits,如果发现target.pointerIdBits == 0,从链表删除它
                    removePointersFromTouchTargets(idBitsToAssign);
     
                    final int childrenCount = mChildrenCount;// 这里直接取了mChildrenCount存到childrenCount里
                    if (newTouchTarget == null && childrenCount != 0) {// dispatchTouchEvent第一次进来,newTouchTarget一定为null,ViewGroup一般而言也有子view,因此会执行if逻辑
                        final float x = ev.getX(actionIndex);// 获取事件的坐标
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();// 构建一个child分发列表,按照Z顺序,绘制顺序降序排序,如果子View都没有Z轴的概念,则返回null
                        final boolean customOrder = preorderedList == null// 如果为true,则是按绘制顺序,因为child没有Z轴。false则没有绘制顺序的概念了。
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;// 此处直接从mChildren获取子View们。
                        for (int i = childrenCount - 1; i >= 0; i--) {// 遍历使用我们保存的childrenCount遍历。
                            final int childIndex = getAndVerifyPreorderedIndex(// 获取第i个view的绘制顺序index
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(// 获取绘制顺序为index的view
                                    preorderedList, children, childIndex);
     
                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {// 如果存在childWithAccessibilityFocus,首先就找到这个child,然后执行下面的逻辑来处理
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;// 清空这个值
                                i = childrenCount - 1;// 这样保证再循环一次
                            }
     
                            if (!canViewReceivePointerEvents(child)// View只有在VISIBLE或者getAnimation不为null时,才能处理事件
                                    || !isTransformedTouchPointInView(x, y, child, null)) {// 把坐标转换成子view里的坐标,主要就是根据子view位置删减了一下坐标值
                                ev.setTargetAccessibilityFocus(false);// 处理不了事件,的情况,就continue,并且还将事件的FLAG_TARGET_ACCESSIBILITY_FOCUS清除
                                continue;
                            }
                            // 运行到此处,当前view能处理事件,且事件坐标也落在view里
                            newTouchTarget = getTouchTarget(child);// 在mFirstTouchTarget链表里面找child,找不到则返回null,找不到其实也表示它没有处理过这个pointerId的事件
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;// 找到的情况下,给它的pointerIdBits加上此次事件的标识,就可以直接break出去了
                                break;
                            }
     
                            resetCancelNextUpFlag(child);// 事件是初始化事件,所以可以reset
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 分析在下面
                                // Child wants to receive touch within its bounds.
                                // child#dispatchTouchEvent返回了true,下面继续处理逻辑
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;// 赋值上次TouchDown被处理的index,index是mChildren中的下标
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();// 赋值mLastTouchDownX,mLastTouchDownY
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);// 将child加上TouchTarget,这个方法同时也会改变mFirstTouchTarget,会指向表头child
                                alreadyDispatchedToNewTouchTarget = true;// 已经派发给了新的TouchTarget
                                break;
                            }
     
                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);// 下次循环不会再管childWithAccessibilityFocus的事情了
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
     
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;// 不明白为什么会把idBitsToAssign给TouchTarget的最后一个
                    }
                }
            }
     
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                // 相当于直接分派给自己去处理了
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)// reset这个TouchTarget的PFLAG_CANCEL_NEXT_UP_EVENT
                                || intercepted;// 拦截的情况下也要cancel
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {// 对于要cancel的child,从TouchTarget中移除
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
     
            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {// 如果是cancel,up之类的,要恢复touch状态
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);// 移除所有TouchTarget中的idBitsToRemove
            }
        }
     
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
    
    /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
     
        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();// 这部分看注释就明白了
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
     
        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;// 确定我们要传的是哪个PointerId
     
        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {// 不正确的情况需要抛弃
            return false;
        }
     
        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);// 事件的坐标偏移到子view的坐标系内
     
                    handled = child.dispatchTouchEvent(event);
     
                    event.offsetLocation(-offsetX, -offsetY);// 分发完成后事件的坐标需要偏移回来
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);// 拆出一个pointerid作为转换后的event
        }
     
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);// 对于transformedEvent,偏移后不需要偏移回来,因为实例是我们自己的
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
     
            handled = child.dispatchTouchEvent(transformedEvent);
        }
     
        // Done.
        transformedEvent.recycle();// 回收TouchEvent
        return handled;
    }
    

    相关文章

      网友评论

          本文标题:崩溃分析: resetCancelNextUpFlag 空指针

          本文链接:https://www.haomeiwen.com/subject/ktahoxtx.html