一、 摘要
本文通過源碼分析WindowManager的幾個重要的操作View的方法:
addView
,
removeView
,
updateViewLayout
等,以及它們隐含的一些風險項。
二、 WindowManager接口
WindowManager
接口繼承于
ViewManager
接口,
ViewManager
中僅有三個方法,也是我們熟知的那三個方法:
public interface ViewManager {
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
而移除View還有一個方法
removeViewImmediate
位于
WindowManager
中。
以上幾個接口方法的實作,均位于
WindowManagerImpl
實作類中:
public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
// ...
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
// ...
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
@Override
public void removeViewImmediate(View view) {
mGlobal.removeView(view, true);
}
}
是以,下面将從
WindowManagerGlobal
入手逐個分析。
三、 添加View
在
WindowManagerGlobal
中:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// ...
ViewRootImpl root;
// ...
root = new ViewRootImpl(view.getContext(), display);
// ...
root.setView(view, wparams, panelParentView);
// ...
}
将待添加的view傳給了
ViewRootImpl
,然後看
ViewRootImpl
中:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// ...
mView = view;
// ...
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
// ...
}
@Override
public void requestLayout() {
// ...
scheduleTraversals();
// ...
}
void scheduleTraversals() {
// ...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
// ...
}
Choreographer
這個類中:
/**
* Posts a callback to run on the next frame.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @param callbackType The callback type.
* @param action The callback action to run during the next frame.
* @param token The callback token, or null if none.
*
* @see #removeCallbacks
* @hide
*/
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
明确說明了回調将在繪制完下一幀之後執行,下一幀的繪制,由native層每隔16毫秒(60幀)發送一個
VSYNC
信号到這裡,收到信号後才會執行這裡的Runnable,即執行
mTraversalRunnable
。
mTraversalRunnable
最後執行的是
performTraversals
這個方法:
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
// ...
if (mFirst) {
// ...
host.dispatchAttachedToWindow(mAttachInfo, 0);
// ...
}
// ...
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// ...
performLayout(lp, mWidth, mHeight);
// ...
performDraw();
// ...
}
此處的host,即剛才
setView
中指派的view,也就是WM添加的view。在
View
中:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
// ...
onAttachedToWindow();
// ...
}
此時我們看到了
onAttachedToWindow
回調。
綜上分析,
onAttachedToWindow
回調發生的時機,是在添加的view繪制第一幀時,并且在
performMeasure
、
performLayout
、
performDraw
之前,是以該回調具有非常嚴重且明顯的延遲性,這也是為什麼我們在
onAttachedToWindow
中拿不到View的寬高。
四、 删除View
removeView
和
removeViewImmediate
最後都會走到
WindowManagerGlobal
中的同一個方法,隻是參數值不同:
public void removeView(View view, boolean immediate) {
// ...
removeViewLocked(index, immediate);
// ...
}
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
// ...
boolean deferred = root.die(immediate);
// ...
}
ViewRootImpl
中:
/**
* @param immediate True, do now if not in traversal. False, put on queue and do later.
* @return True, request has been queued. False, request has been completed.
*/
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
private void performDraw() {
// ...
mIsDrawing = true;
// ...
boolean canUseAsync = draw(fullRedrawNeeded);
// ...
mIsDrawing = false;
// ...
}
-
這個标志位在mIsDrawing
方法中,先指派為true,繪制結束後指派為falseperformDraw
-
最後也是執行的MSG_DIE
方法die
- 當immediate參數為true并且此時沒有處于周遊階段,則立即銷毀
- 否則會在下一次消息輪詢中執行銷毀。如果目前沒有處于繪制階段,還會銷毀硬體渲染器
我們檢視這個周遊階段的标志位:
/** Set to true while in performTraversals for detecting when die(true) is called from internal
* callbacks such as onMeasure, onPreDraw, onDraw and deferring doDie() until later. */
boolean mIsInTraversal;
從這裡可以得出,如果目前view處于
onMeasure
,
onPreDraw
,
onDraw
這幾個階段,這個标志位都會讓立即移除加入到消息隊列中,延後執行。
繼續追蹤
doDie
這個方法:
void doDie() {
// ...
dispatchDetachedFromWindow();
// ...
}
void dispatchDetachedFromWindow() {
// ...
mView.dispatchDetachedFromWindow();
// ...
}
此處的mView,即前面
setView
中傳入的view,也就是添加的那個view。在
View
中:
void dispatchDetachedFromWindow() {
// ...
onDetachedFromWindow();
// ...
}
見到了
onDetachedFromWindow
回調。
綜上分析,即便是調用立即移除,也可能會延遲到下一次消息輪詢中執行,是以無法保證回調的及時性。
五、 更新View
updateViewLayout
的流程相對于前兩個操作,簡單了很多:
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
// ...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
// ...
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
// ...
root.setLayoutParams(wparams, false);
}
主要做了兩件事:
- 更新view的布局參數
- 更新ViewRootImpl的布局參數
前者更新參數後,會執行一次
requestLayout
方法,而後者:
void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) {
// ...
scheduleTraversals();
}
調用
scheduleTraversals
準備繪制下一幀的内容,繪制時将應用更新後的參數值。
六、 總結
筆者曾經在工作中遇到這樣一個場景:
對View A調用addView,對View B調用removeView
觀察log發現,很快回調了B的onDetachedFromWindow,隔了很多log才回調了A的onAttachedToWindow
學習完今天的内容便可以解釋:
A在addView之後,進入了mChoreographer的回調隊列,等待下一次
vsync
信号,而B在removeView之後,即便處于下一次消息輪詢,但在消息隊列中的事件不足以多到丢幀的情況下,也會非常快輪詢到并執行,是以
onAttachedToWindow
回調遠遠慢于
onDetachedFromWindow
。
增加View和删除View,都具有延遲性,是以我們不能過于依賴
onAttachedToWindow
和
onDetachedFromWindow
回調,并且WM重複增加或删除同一個View會抛異常。對于高頻增删View的場景,我們可以通過設定可見性
setVisibility
來代替實作,這樣便可避免像add之後立馬remove這種場景導緻異常的問題。