一個ViewGroup#dispatchDraw()中的NP分析
0x0 背景
經常在Crash平臺上看到一個Crash,通過崩潰日誌中的CurActivity欄位可以知道崩潰頁面是在搜尋結果頁,然而因為崩潰堆疊中不涉及任何業務程式碼,所以也很難定位原因。
04-06 10:37:43.610: ERROR/AndroidRuntime(23203): java.lang.NullPointerException
04-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)
0x1 線索
在stackoverflow上有人提到在Animation的onAnimationEnd回撥中,刪除view會引起這個問題,但是具體原因沒有講。
一個偶然機會,在搜尋結果頁連續快速點選PK時,重現了該問題。檢視該出程式碼,果然存在Animation的onAnimationEnd()回撥中刪除view的情況。
0x2 原因
一句話,Animation的onAnimationEnd()是在draw()函式中同步呼叫的,在draw的時候刪除view,相當於在for迴圈遍歷所有子view的過程中將其中一個元素置空,導致遍歷到時產生NP。
Android具體的動畫執行流程如下:
1. 呼叫View#startAnimation()開始動畫執行,此時只是將Animation物件設定進去,並呼叫invalidate()觸發繪製更新
/**
* 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);
}
2. 繪製流程從root view的draw()方法呼叫到ViewGroup#dispatchView(),在這個函式中又會遍歷它的子view,分別呼叫他們的draw()函式
@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);
}
}
...
}
...
}
3. 正在執行動畫的view,在其View#draw()中獲取Animation資訊,並影響本次繪製
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;
}
4. 動畫結束時Animation#getTransformation()函式內部會直接同步回撥onAnimationEnd()
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()操作,那是會有引發空指標的風險的。
0x3 後記
就這個問題而言把removeView的操作自己放到一個Handler中非同步化,問題就能解決了。在Animation設定listener,並監聽其開始和結束的資訊,很容易讓人有一種這是非同步回撥的錯覺,殊不知這是一個同步回撥,如果在這裡同步的執行類似刪除view之類的操作就會有問題,後續這裡進行類似操作需要足夠慎重。
相關文章
- numpy 中np.array 與 np.ndarry的區別
- numpy中np.array()與np.asarray的區別以及.tolist
- python中numpy模組下的np.clip()的用法Python
- 破解NP的建議
- np.newaxis 為 numpy.ndarray(多維陣列)增加一個軸陣列
- Elasticsearch中關於transform的一個問題分析ElasticsearchORM
- 一個備庫中ORA錯誤資訊的分析
- Python:range、np.arange和np.linspacePython
- np.random.choicerandom
- Python基礎——min/max與np.argmin/np.argmaxPython
- np.random.multivariate_normal()randomORM
- np.stack函式函式
- python np.hstackPython
- python---np.shufflePython
- NP-完全問題
- 一個section加密的apk的分析加密APK
- [np-ml] Ridge Regression
- 從專案的一個 panic 說起:Go 中 Sync 包的分析應用Go
- vue的第一個commit分析VueMIT
- 一個JDO的成功案例分析 (轉)
- 把一個一中的欄位更新另一個表中的t-sqlSQL
- python--之np.deletePythondelete
- 怕不怕(P/NP)?技術帖
- 將一個Activity中的資料傳到另一個Activity的Fragment中的方法Fragment
- 一個需求分析做的不夠的案例
- python 對矩陣進行復制操作 np.repeat 與 np.tile區別Python矩陣
- 一個CRM OData的效能問題分析
- 一個 Chrome XSS Filter Bypass 的分析ChromeFilter
- 一個RESOURCE MANAGER引起的問題分析
- 從一個例子看Go的逃逸分析Go
- 一個ORA-604錯誤的分析
- 一個Linux病毒的原型分析(轉)Linux原型
- KANO模型,一個能解決你工作中90%煩惱的需求分析神器模型
- 由一個go中出現的異常引出對php與go中操作sql的一些分析GoPHPSQL
- MySQL:一個死鎖分析 (未分析出來的死鎖)MySql
- oracle日誌分析從列表中移去一個日誌檔案Oracle
- scipy.stats 庫的使用,np求均值和方差
- MYSQL如何識別一個binlog中的一個事物MySql