前言
今天收到一份崩溃日志,经过一番分析之后,找到了原因,但对于这个崩溃,由于堆栈暴露的信息不足,项目代码又多又乱,基本无法定位崩溃代码,要定位代码,就是体力活,也没什么好方法。尽管如此,分析过程还是值得分享。
堆栈
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的dispatchTouchEvent
,onTouchEvent
会得到执行,如果子View中尝试removeView
,恰好移除next.child
指向的View B,此时ViewGroup会删除链表中第二个节点,调整链表。
A链向C,A与B的链被切断。B的recycle
被调用,B的child为null,因为在dispatchTransformedTouchEvent
执行之前,next就已经指向B了,相当于next.child
被置null。在第二次循环的时候,next被赋值给target,此时target.child
为null,导致resetCancelNextUpFlag
的参数为null。
由于在第二轮循环就能发生空指针,因此只需要两个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,因此我们应该这样操作来复现崩溃:
- 按住ButtonB,不松手
- 按住ButtonA,不松手
- 抬起按在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;
}
网友评论