本文共 8487 字,大约阅读时间需要 28 分钟。
经常在Crash平台上看到一个Crash,通过崩溃日志中的CurActivity字段可以知道崩溃页面是在搜索结果页,然而因为崩溃堆栈中不涉及任何业务代码,所以也很难定位原因。
04-06 10:37:43.610: ERROR/AndroidRuntime(23203): java.lang.NullPointerException04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2122)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.drawChild(ViewGroup.java:2506)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2123)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.drawChild(ViewGroup.java:2506)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2123)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.drawChild(ViewGroup.java:2506)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2123)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.drawChild(ViewGroup.java:2506)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2123)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.View.draw(View.java:9032)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.widget.FrameLayout.draw(FrameLayout.java:419)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at com.android.internal.policy.impl.PhoneWindow$DecorView.draw(PhoneWindow.java:1910)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewRoot.draw(ViewRoot.java:1608)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewRoot.performTraversals(ViewRoot.java:1329)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.view.ViewRoot.handleMessage(ViewRoot.java:1944)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.os.Handler.dispatchMessage(Handler.java:99)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.os.Looper.loop(Looper.java:126)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at android.app.ActivityThread.main(ActivityThread.java:3997)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at java.lang.reflect.Method.invokeNative(Native Method)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at java.lang.reflect.Method.invoke(Method.java:491)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599)04-06 10:37:43.610: ERROR/AndroidRuntime(23203): at dalvik.system.NativeStart.main(Native Method)
在stackoverflow上有人提到在Animation的onAnimationEnd回调中,删除view会引起这个问题,但是具体原因没有讲。
一个偶然机会,在搜索结果页连续快速点击PK时,重现了该问题。查看该出代码,果然存在Animation的onAnimationEnd()回调中删除view的情况。一句话,Animation的onAnimationEnd()是在draw()函数中同步调用的,在draw的时候删除view,相当于在for循环遍历所有子view的过程中将其中一个元素置空,导致遍历到时产生NP。
Android具体的动画执行流程如下:/** * Start the specified animation now. * * @param animation the animation to start now */ public void startAnimation(Animation animation) { animation.setStartTime(Animation.START_ON_FIRST_FRAME); //设置Animation对象 setAnimation(animation); invalidateParentCaches(); //触发绘制更新 invalidate(true); }
@Override protected void dispatchDraw(Canvas canvas) { boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); final int childrenCount = mChildrenCount; final View[] children = mChildren; int flags = mGroupFlags; if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { final boolean buildCache = !isHardwareAccelerated(); //遍历子view for (int i = 0; i < childrenCount; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { final LayoutParams params = child.getLayoutParams(); attachLayoutAnimationParameters(child, params, i, childrenCount); bindLayoutAnimation(child); } } ... } ... }
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { ... final Animation a = getAnimation(); if (a != null) { //更新动画信息 more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired); concatMatrix = a.willChangeTransformationMatrix(); if (concatMatrix) { mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM; } transformToApply = parent.getChildTransformation(); } else { if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) { // No longer animating: clear out old animation matrix mRenderNode.setAnimationMatrix(null); mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM; } if (!drawingWithRenderNode && (parentFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) { final Transformation t = parent.getChildTransformation(); final boolean hasTransform = parent.getChildStaticTransformation(this, t); if (hasTransform) { final int transformType = t.getTransformationType(); transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null; concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0; } } } ... } private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime, Animation a, boolean scalingRequired) { Transformation invalidationTransform; final int flags = parent.mGroupFlags; final boolean initialized = a.isInitialized(); if (!initialized) { a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight()); a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop); if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler); onAnimationStart(); } final Transformation t = parent.getChildTransformation(); //这里会获取Animation的Transformation信息 boolean more = a.getTransformation(drawingTime, t, 1f); ... return more; }
public boolean getTransformation(long currentTime, Transformation outTransformation) { if (mStartTime == -1) { mStartTime = currentTime; } final long startOffset = getStartOffset(); final long duration = mDuration; float normalizedTime; if (duration != 0) { normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) / (float) duration; } else { // time is a step-change with a zero duration normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f; } final boolean expired = normalizedTime >= 1.0f || isCanceled(); if (expired) { if (mRepeatCount == mRepeated || isCanceled()) { if (!mEnded) { mEnded = true; guard.close(); //发布AnimationEnd信息 fireAnimationEnd(); } } else { if (mRepeatCount > 0) { mRepeated++; } if (mRepeatMode == REVERSE) { mCycleFlip = !mCycleFlip; } mStartTime = -1; mMore = true; fireAnimationRepeat(); } } ... return mMore; }//这里是同步调用注入的Listener的onAnimationEnd()函数private void fireAnimationEnd() { if (mListener != null) { if (mListenerHandler == null) mListener.onAnimationEnd(this); else mListenerHandler.postAtFrontOfQueue(mOnEnd); } }
至此,如果在onAnimationEnd()同步执行removeView()操作,那是会有引发空指针的风险的。
就这个问题而言把removeView的操作自己放到一个Handler中异步化,问题就能解决了。在Animation设置listener,并监听其开始和结束的信息,很容易让人有一种这是异步回调的错觉,殊不知这是一个同步回调,如果在这里同步的执行类似删除view之类的操作就会有问题,后续这里进行类似操作需要足够慎重。
转载地址:http://grzdo.baihongyu.com/