Android 開發(fā)藝術探索 - 讀書筆記之第八章 理解 Window 和 WindowManager

8.1 Window 和 WindowManager

示例代碼:簡單地添加一個 Window

    Button btn = new Button(this);
    btn.setText("Button");
    LayoutParams params = new WindowManager.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
    params.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | 
        LayoutParams.FLAG_NOT_FOCUSABLE |
        LayoutParams.FLAG_SHOW_WHEN_LOCKED;
    params.gravity = Gravity.LEFT | Gravity.TOP;
    params.x = 100;
    params.y = 300;
    windowManager.addView(btn, params);

params 兩個重要參數(shù) flags 和 type
1、幾個常用的 FLAG:

FLAG_NOT_FOUCSABLE:直接把事件傳遞給下層具有焦點的 Window
FLAG_NOT_TOUCH_MODAL:將當前 Window 區(qū)域以外的事件傳遞給底層 Window
FLAG_SHOW_WHEN_LOCKED:可以顯示在鎖屏的界面上

2、window 的三種 type

  • 應用 Window:層級范圍 1-99,優(yōu)先級最低;一個應用 Window 對應一個 Activity
  • 子 Window:1000-1999,優(yōu)先級中等;例如對應一個 Dialog 。
  • 系統(tǒng) Window:2000-2999,一般為 TYPE_SYSTEM__OVERLAY 或 TYPE_SYSTEM_ERROR ,需要聲明 SYSTEM_ALERT_WINDOW 權(quán)限

最終調(diào)用 WindowManager 的方法將控件呈現(xiàn)到屏幕上;WindowManager 繼承 ViewManager,提供了對 View 的增刪改三個方法:

addView(View view, ViewGroup.LayoutParams params);
updateViewLayout(View view, ViewGroup.LayoutParams params);
removeView(View view);

8.2 Window 的添加、刪除、更新過程

  • 每一個 Window 都對應著一個 View 和一個 ViewRootImpl
  • Window 和 View 通過 ViewRootImpl 建立聯(lián)系
  • 無法直接操作 Window,只能通過 WidowManager

8.2.1 Window 的添加過程

WindowManagerImpl 將所有對 View 的操作全部委托 WindowManagerGlobal 處理(橋接模式?);WindowManagerGlobal 添加 View 的過程:

  • 檢查參數(shù):view、display 是否為空,params 是否為 WindowManager.LayoutParams
if (view == null) throw ....
if (display == null) throw ...
if (!(params instanceof WindowManager.LayoutParams)) throw ...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
if (parentWindow != null) parentWindow.adjustLayoutParamsForSubWindwo(wparams);
  • 創(chuàng)建 ViewRootImpl ,使用 ArrayList 管理 mViews、mRoots、mParams 和 mDyingViews
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
  • 通過 ViewRootImpl 來更新界面并完成 Window 的添加過程
    • ViewRootImpl.setView
    • ViewRootImpl.requestLayout
    • ViewRootImpl.scheduleTraversals
    • 最終,通過 WindowSession 完成 Window 的添加過程
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

在 scheduleTraversals() 方法中將出現(xiàn):

mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel);

mWindowSession 的類型是 IWindowSession,他是一個 Binder 對象,真正的實現(xiàn)類是 Session,也就是 Window 的添加過程是一次 IPC 調(diào)用。

  • 在 Session 內(nèi)部會通過 WindowManagerService 來實現(xiàn) Window 的添加

mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outInputChannel);

8.2.2 Window 的刪除過程

代碼:WindowManagerGlobal.removeView()

public void removeView(View view, boolean immediate) {
    if (View == null)
        throw ....
    synchronized (mLock) {
        // 遍歷數(shù)組,查找索引
        int index = findViewLocked(view, true);
        View curView = mRoots.get(index).getView();
        removeViewLocked(index, immediate);
        if (curView == view) 
            return;
        throw ***
    }
}

代碼:removeViewLocked(int index, boolean immediate)

private void removeViewLocked(int index, boolean immediate) {
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();
    if (view != null) {
        InputMethodManager imm = InputMethodManager.getInstance();
        if (imm != null) {
            imm.windowDismissed(mViews.get(index).getWindowToken());
        }
    }
    boolean defered = root.die(immediage);
    if (view != null) {
        view.assignParent(null);
        if (defered) {
            // mDyingViews 待刪除列表
            mDyingViews.add(view);
        }
    }
}
  • WindowManagerGlobal 通過 ViewRootImpl 來完成刪除操作
  • removeView:異步刪除,通過 ViewRootImpl 的 die 方法
  • removeViewImmediate:同步刪除(不常用)

代碼:ViewRootImpl.die() 方法

boolean die (boolean immediate) {
    if (immediate && !mIsInTraversal) {
        doDia();
        return flase;
    }
    
    if (!mIsDrawing) {
        destroyHardwareRenderer();
    } else {
        Log.e(TAG, "嘗試摧毀正常繪制中的 Window");
    }
    // ViewRootImpl 的 mHandler 將處理此消息并調(diào)用 doDie
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
}

當 immediate 為 false,使用異步刪除,就發(fā)送一個 MSG_DIE 的消息,ViewRootImpl 的 mHandler 將處理此消息并調(diào)用 doDie 方法;如果是同步刪除,就是直接調(diào)用 doDie 方法;doDie 方法會調(diào)用 dispatchDetachedFromWindow 方法,內(nèi)部真正實現(xiàn)了 View 的刪除邏輯。
dispatchDetachedFromWindow 方法做了四步工作:

  • 垃圾回收,例如清除數(shù)據(jù)、消息、移除回調(diào)
  • 通過 Session 的 remove 方法刪除 Window

mWindow.remove(mWindow)
WindowManagerService.removeWindow()

  • 調(diào)用 View 的 dispatchDetachedFromWindow 方法,內(nèi)部調(diào)用 View 的 onDetachedFromWindow、onDetachedFromInternal()
  • 調(diào)用 WindowManagerGlobal 的 doRemoveView 方法刷新數(shù)據(jù),包括 mRoots、mParams、mDyingViews

8.2.3 Window 的更新過程

代碼:WindowManagerGlobal.updateViewLayout

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    if (View == null) throw ....
        
    if (!(params instanceof WindowManager.LayoutParams)) throw ...
    
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    
    view.setLayoutParams(wparams);
    
    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        root.setLayoutParams(wparams, false);
    }
}
  • 更新 View 的 LayoutParams,更新 ViewRootImpl 的 LayoutParams,內(nèi)部調(diào)用 scheduleTraversals 對 view 重繪,通過 WindowSession 更新 Window 的視圖,最終調(diào)用 WindowMService 的 relayoutWindow() 具體實現(xiàn)。

8.3 Window 的創(chuàng)建過程

  • View 是 Android 視圖的呈現(xiàn)方式
  • View 必須附著在 Window 上
  • 這一節(jié)將解釋 Activity、Dialog、Toast 中 Window 的創(chuàng)建過程

8.3.1 Activity 的 Window 創(chuàng)建過程

代碼:ActivityThread performLaunchActivity()

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
...
if (activity != null) {
    Context appContext = createBaseContextForActivity(r, activity);
    CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
    Configuration config = new Configuration(mCompatConfiguration);
    // 為 Activity 關聯(lián)運行過程中所依賴的一系列上下文環(huán)境變量
    activity.attch(appContext, this, getInstrumentation(), r.token, 
            r.ident, app, r.intent, r.activityInfo, title, r.parent, 
            r.embeddedID, r.lastNonConfigurationInstances, config, r.voiceInteractor);
}

在 Activity 的 attach 方法內(nèi)部,系統(tǒng)為 Activity 創(chuàng)建所屬 Window 對象并設置回調(diào),注意此時并未與 WindowManager 關聯(lián),最終 onResume 時才會完成關聯(lián):

mWindow = PolicyManager.makeNewWindow(this);
// 當 Window 接收到外界狀態(tài)改變時回調(diào) Activity 的接口實現(xiàn)
// onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
    mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
    mWindow.setUiOptions(info.uiOptions);
}

PolicyManager 實現(xiàn)了 IPolicy 定義的四個方法:

public interface IPolicy {
    public Window makeNewWindow(Context context);
    public LayoutInflater makeNewLayoutInflater(Context context);
    public WindowManagerPolicy makewNewWindowManager();
    public FallbackEventHandler makeNewFallbackEventHandler(Context context);
}

代碼:PolicyManager 的實現(xiàn) Policy,makeNewWindow

public Window makeNewWindow(Context context) {
    return new PhoneWindow(context);
}

setContentView 過程:

  • 1、如果沒有 DecorView,那么就創(chuàng)建它,installDecor
protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

為了初始化 PhoneWindow 還要通過 generateLayout 加載具體的布局到 DecorView 中

    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT);
    mContentRoot = (ViewGroup) in;
    ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
  • 2、將 View 添加到 DecorView 的 mContentParent 中

mLayoutInflater.inflate(layoutResId, mContentParent);

  • 3、回調(diào) Activity 的 onContentChanged 方法通知 Activity 視圖已經(jīng)發(fā)生改變
  • 4、ActivityThread.handleResumeActivity 方法中,調(diào)用 Activity onResume 方法,調(diào)用 Activity makeVisible(),在 makeVisible 方法中真正完成了添加和顯示,才能被用戶所看到
void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

總結(jié)就是,Activity 最終由 ActivityThread 的 performLaunchActivity() 方法中完成啟動;其中會調(diào)用 Activity 的 attach 方法,將 Activity 實現(xiàn)的回調(diào)注入到剛創(chuàng)建的 PhoneWindow 對象中;在 Window 的 setContentView 方法中,首先檢查 DecorView 是否存在,不存在則調(diào)用 installDecor,內(nèi)部調(diào)用 generateDecor 創(chuàng)建 DecorView,調(diào)用 generateLayout 加載布局文件到 DecorView 的 contentParent 中(id:ID_ANDROID_CONTENT),最后回調(diào) Activity 的 onContentChanged() 標識著視圖初始化完畢;最后在 ActivityThread 的 handleResumeActivity 中,調(diào)用 Activity 的 onResume() -> makeVisible(),才真正完成了添加和顯示的過程

8.3.2 Dialog 的 Window 創(chuàng)建過程

  • 1、創(chuàng)建 Window
  • 2、初始化 DecorView 并將 Dialog 的視圖添加到 DecorView 中
  • 3、將 DecorView 添加到 Window 中并顯示
  • 4、關閉時調(diào)用 WindowManager 的 removeViewImmediate(mDecor) 移除

Q:普通的 Dialog 為什么只能使用 Activity 的 Context,而不能使用 Application 的 Context?
A:Exception: Unable to add window -- token null is not for an application,意思是沒有應用token,一般只有 Activity 才有;而如果是系統(tǒng)類型的 Dialog就可以正常彈出。

8.3.3 Toast 的 Window 創(chuàng)建過程

  • 由于 Toast 內(nèi)部有定時取消的功能,所以系統(tǒng)采用了 Handler
  • 兩個 IPC 過程
    • Toast 訪問 NotificationManagerService
    • NotificationManagerService 回調(diào) Toast 的 TN 接口
  • Toast 屬于系統(tǒng) Window,內(nèi)部視圖由兩種方式指定,系統(tǒng)默認和自定義 View,對應內(nèi)部成員 mNextView

代碼:show()

public void show() {
    if (mNextView != null) {
        throw ...
    }
    
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    
    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        ...
    }
}

public void cancel() {
    mTN.hide();
    
    try {
        getService().cancelToast(mContext.getPackgetName(), mTN);
    } catch (RemoteException e) {
        ...
    }
}

TN 是一個 Binder 類,在 Toast 與 NMS 進行 IPC 過程中,當 NMS 處理 Toast 的顯示或隱藏請求時會跨進程回調(diào) TN 中的方法,這個時候由于 TN 運行在 Binder 線程池中,所以需要通過 Handler 將其切換到當前線程中。這里的當前線程是指發(fā)送 Toast 請求所在的線程。所以意味著,Toast 無法在沒有 Looper 的線程中彈出。

INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
    service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
    ...
}

NMS 的 enqueueToast 方法首先將 Toast 請求封裝為 ToastRecord 對象并將其添加到一個名為 mToastQueue 的 ArrayList 隊列中。對于非系統(tǒng)應用則最多存在50個 ToastRecord,防止 DOS(Denial of Service)。否則通過大量的循環(huán)去連續(xù)彈出 Toast,會導致其他應用沒有機會彈出 Toast,即拒絕服務攻擊。

if (!isSystemToast) {
    int count = 0;
    final int N = mToastQueue.size();
    for (int i=0; i<N; i++) {
        final ToastRecord r = mToastQueue.get(i);
        if (r.pkg.equals(pkg)) {
            count++;
            if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                return;
            }
        }
    }
}

當一個 ToastRecord 添加到 mToastQueue 后,NMS 立即調(diào)用 showNextToastLocked 顯示當前 Toast。最終顯示是 ToastRecord 的 callback 完成的。這個 callback 實際上就是 Toast 中的 TN 對象的遠程 Binder,通過 callback 來訪問 TN 中的方法是需要跨進程來完成的,最終被調(diào)用的 TN 中的方法會運行在發(fā)起 Toast 請求的應用的 Binder 線程池中。

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        try {
            record.callback.show();
            scheduleTimeoutLocked(record);
            return;
        } catch (RemoteException e) {
            ... 
            // show 失敗就移除該 ToastRecord,繼續(xù)下一個
        }
    }
}

Toast 顯示,NMS 調(diào)用了 scheduleTimeoutLocked,發(fā)送一個延時消息,即 Toast 的延時時長。

private void scheduleTimeoutLocked(ToastRecord r) {
    mHandler.removeCallbacksAndMessages(r);
    Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
    long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
    mHandler.sendMessageDelayed(m, delay);
}

Toast 的隱藏也是通過 ToastRecord 的 callback 來完成的,同樣是一次 IPC 過程:

try {
    record.callback.hide();
} catch (RemoteException e) {
    // empty
}

TN 的 show 和 hide 兩個方法:

public void show() {
    mHandler.post(mShow);
}

public void hide() {
    mHandler.post(mHide);
}

mShow 和 mHide 是兩個 Runnable,內(nèi)部調(diào)用了 TN 的 handleShow 和 handleHide 方法:

if(mView.getParent() != null) {
    mWM.removeView(mView);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容