崩溃分析: resetCancelNextUpFlag 空指针

前言

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

堆栈

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会删除链表中第二个节点,调整链表。

崩溃分析: resetCancelNextUpFlag 空指针_第1张图片
第一轮循环:B被删除

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

崩溃分析: resetCancelNextUpFlag 空指针_第2张图片
两个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();

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


崩溃分析: resetCancelNextUpFlag 空指针_第3张图片
测试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 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 空指针)