天天看點

WindowManager操作View源碼分析

一、 摘要

本文通過源碼分析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

    這個标志位在

    performDraw

    方法中,先指派為true,繪制結束後指派為false
  • 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這種場景導緻異常的問題。