Android Window源碼分析

在Android中所有的視圖都是通過Window來呈現的,Window是View的直接管理者,每一個Activity都對應著一個Window,Activity的視圖DecorView會被添加到其Window中;另外,如果我們想要實現懸浮窗的效果,那么也離不開Window的開發。Android為我們提供了WindowManager類可以用來管理Window,WindowManager可以通過Activity的getWindowManager()獲得,WindowManager實現了ViewManager接口,這個接口有三個方法:addView()、updateViewLayout()、removeView(),通過這三個方法就可以完成Window的添加、更新和刪除操作,接下來我們分別看看這三個方法的執行流程。

Window的添加過程

我們通過Activity對Window的處理來分析下Window的添加流程。在啟動一個Activity時,會調用到ActivityThread的performLaunchActivity()創建Activity實例并調用它的attach()進行初始化,如下所示:

final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
    ...
    mWindow = new PhoneWindow(this); 
    ...
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    ...
    mWindowManager = mWindow.getWindowManager();
    ...
}

該方法首先會創建一個Window實例并賦值給mWindow,Window 是個抽象的概念, 具體實現類是 PhoneWindow,接著會獲取一個WindowManager對象,WindowManager是一個接口類型,具體實現是WindowManagerImpl,最后調用Window.setWindowManager()給Window設置WindowManager對象,如下所示:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    ...
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

方法內通過調用WindowManager的createLocalWindowManager()創建了一個新的WindowManager對象并賦值給mWindowManager,該方法內部直接new了一個WindowManagerImpl對象,因此可以知道每個Window都會對應一個WindowManagerImpl對象,方法如下所示:

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    return new WindowManagerImpl(mContext, parentWindow);
}

在Activity初始化完成后會調用Activity的onCreate(),而調用onCreate()時會調用setContentView()設置布局,其內部會調用剛剛創建的Window的setContentView(),如下所示:

public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ...
}

可以看到內部會調用installDecor(),該方法會根據不同的Theme創建不同的DecorView,DecorView 是一個 FrameLayout,setContentView()最終會把布局設置到DecorView中id為content的View上。到這里創建了 PhoneWindow和DecorView,但目前二者也沒有任何關系。當Activity的狀態變成resume時,最終會調用到ActivityThread的handleResumeActivity(),如下所示:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
        final Activity a = r.activity;
        boolean willBeVisible = !a.mStartedActivity;
        ...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            }
        }
        ...
}

該方法首先會調用performResumeActivity()執行Activity的onResume(),接著獲取到前面創建的WindowManagerImpl對象并調用其addView()創建DecorView對應的Window,接下來看下WindowManagerImpl的內部實現:

public final class WindowManagerImpl implements WindowManager {
    public void addView(View view, ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }

    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }
    ...
}

WindowManagerImpl實現了WindowManager的方法,但并沒有具體實現具體的操作,而是調用WindowManagerGlobal對應的方法來完成,接下來看一下WindowManagerGlobal的addView():

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ...
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    ...
    root.setView(view, wparams, panelParentView);
    ...
}

這個過程創建一個ViewRootImpl,并將View、ViewRootImpl以及LayoutParams等參數保存到一個列表中。最后會調用ViewRootImpl的setView(),如下所示:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ...
    mView = view;
    ...
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
          getHostVisibility(), mDisplay.getDisplayId(),
          mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
          mAttachInfo.mOutsets, mInputChannel);
    ...
    view.assignParent(this);
    ...
}

該方法首先將傳入的View賦值給ViewRootImpl的成員變量mView,這里傳入的View是DecorView,這樣DecorView就交由ViewRootImpl進行管理了;接著調用mWindowSession.addToDisplay(),mWindowSession是個Binder對象,會與WMS通信由WMS完成Window的創建;最后會調用傳入的View的assignParent(),該方法會將View的mParent設為當前的ViewRootImpl,mParent是ViewParent類型,ViewRootImpl和ViewGroup都實現了ViewParent接口,當ViewGroup(如DecorView)調用addView()添加子View時,會將子View的mParent置為該ViewGroup,這樣就形成了ViewRootImpl -> DecorView -> ViewGroup -> ... -> View這樣的樹形結構。ViewParent接口最常見的一個方法是requestLayout(),當調用View或ViewGroup的requestLayout()時,會找到View樹的根節點ViewRootImpl,執行ViewRootImpl的requestLayout(),執行該方法時會依次執行performMeasure()、performLayout()、performDraw(),它們的內部會分別調用mView的measure()、layout()、draw(),上面已經說到這里的mView就是DecorView,DecorView會遍歷其子View向下傳遞事件,這樣一個View樹的工作流程就啟動了。
ViewRootImpl可以理解成是WindowManager和DecorView的紐帶,它們的關系如下如所示:


28782908-7672-4693-a47e-ee83f717532c.png

接下來看一下WMS部分的處理邏輯,在調用IWindowSession.addToDisplay()后,會調用到Session的addToDisplay(),其內部又調用了WindowManagerService.addWindow(),該方法主要做了以下幾件事:

  • 對所要添加的窗口進行權限及類型的檢查,如果窗口不滿足一些條件,就不會再執行下面的代碼邏輯。
  • WindowToken相關的處理,比如有的窗口類型需要提供WindowToken,沒有提供的話就不會執行下面的代碼邏輯,有的窗口類型則需要由WMS隱式創建默認windowToken。
  • WindowState的創建和相關處理,將WindowToken和WindowState相關聯。WindowState存有窗口的所有的狀態信息,在WMS中它表示一個窗口。
  • 創建和配置DisplayContent,完成窗口添加到系統前的準備工作。

在創建WindowState時會傳入一個IWindow類型的對象作為參數,這是一個Binder對象,IWindow實現是ViewRootImpl的內部類W,IWindow會將WMS中窗口管理的操作回調給ViewRootlmpl,這樣一個Window就創建完成了,創建Window過程中涉及的各個對象也都建立起了聯系。

根據上面的分析可以知道創建一個Window的核心在于調用WindowManager.addView()并將Window的視圖傳遞進去;接著會為Window創建一個ViewRootImpl,后續Window視圖的事件都交由ViewRootImpl進行管理;最后與WMS通信完成Window創建。在Activity的Window創建中視圖正是DecorView。而如果我們需要創建一個子窗口,則需要先創建一個View,再調用WindowManager.addView()即可。但是我們看過源碼后發現,調用WindowManager的addView()并不會創建一個PhoneWindow類型的對象,那么為什么窗口會創建了?或者說PhoneWindow的職責究竟是什么?
我理解是表示一個窗口的并不是應用內的PhoneWindow對象,而是WMS內的WindowState對象,前文說到了WindowState有窗口的全部狀態信息,而且無論是Activity、Dialog的窗口或是我們創建的子窗口,都會調用WindowManager.addView(),其最終會調用到WMS.addWindow()創建WindowState對象用于表示窗口。既然Window并不真正表示一個窗口,那么PhoneWindow的作用是什么呢?

  • PhoneWindow的一個作用是給view包裹上一層DecorView,而DecorView中的布局結構會根據Theme決定。
  • 我們的Activity和Dialog的布局都比較復雜,比如都可能有appbar(toolbar/actionbar)等,通過PhoneWindow來封裝下可以更好的解耦代碼。

Dialog和Window一樣都使用了PhoneWindow封裝了DecorView,因此Dialog的樣式也會根據Theme決定,但是PopupWindow以及Toast同樣是Window,其內部并沒有使用PhoneWindow,而是直接通過WindowManager.addWindow()創建的Window,主要原因是PopupWindow和Toast樣式相對較簡單,無需通過PhoneWindow進行一層封裝。

Window的刪除過程

在了解了Window的添加過程后,我們依舊是通過Activity對Window的處理來看下Window的刪除過程。在Activity銷毀時會調用ActivityThread.handleDestroyActivity(),如下所示:

private void handleDestroyActivity(IBinder token, boolean finishing,
        int configChanges, boolean getNonConfigInstance) {
    ActivityClientRecord r = performDestroyActivity(token, finishing,
            configChanges, getNonConfigInstance);
    if (r != null) {
        cleanUpPendingRemoveWindows(r, finishing);
        WindowManager wm = r.activity.getWindowManager();
        View v = r.activity.mDecor;
        if (v != null) {
            ...
            wm.removeViewImmediate(v);
            ...
        }
        ...
}

首先獲得了WindowManager以及Activity的DecorView,接著調用WindowManager的removeViewImmediate()并將DecorView作為參數傳入,removeViewImmediate()的實現位于WindowManagerImpl中,如下所示:

public void removeView(View view) {
    mGlobal.removeView(view, false);
}

public void removeViewImmediate(View view) {
    mGlobal.removeView(view, true);
}

removeViewImmediate()同樣沒有具體實現操作,也是直接調用WindowManagerGlobal.removeView(),另外除了removeViewImmediate(),還有前面說到的ViewManager中的removeView(),這兩個接口都是用于刪除Window并都會調用WindowManagerGlobal.removeView(),但區別在于前者調用時immediate為true,這個字段標識是否要立即銷毀Window,我們后面會講到。接下來看一下WindowManagerGlobal的removeView(),其內部會找到傳入的View在列表中的索引并調用removeViewLocked(),如下所示:

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 deferred = root.die(immediate);
    if (view != null) {
        view.assignParent(null);
        if (deferred) {
            mDyingViews.add(view);
        }
    }
}

首先會獲取InputMethodManager并調用windowDismissed()來結束Window輸入法相關邏輯,接著會調用ViewRootImpl.die()并傳入了前文說到的immediate參數,方法如下所示:

boolean die(boolean immediate) {
    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;
}

首先若immediate為true且mIsInTraversal為false則會執行doDie()并返回,immediate是前文傳遞下來的,若我們執行的是WindowManager.removeViewImmediate()則該值為true,mIsInTraversal會在ViewRootImpl執行performTraversals()時置為true,執行結束后置為false,因此這里的邏輯就是如果要立即執行(immediate為true)且ViewRootImpl不再執行performTraversals()時會執行doDie(),否則會通過Handler發送一個MSG_DIE消息,而Handler在處理這個消息時就會執行doDie(),因此我們看下doDie()的實現,如下所示:

void doDie() {
    ...
    if (mAdded) {
        dispatchDetachedFromWindow();
    }
    ...
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

上述代碼主要關注兩個地方,首先是要刪除的Window如果有子View會調用dispatchDetachedFromWindow()來銷毀View,接著調用WindowManagerGlobal的doRemoveView(),我們先看下WindowManagerGlobal的doRemoveView(),如下所示:

void doRemoveView(ViewRootImpl root) {
    synchronized (mLock) {
        final int index = mRoots.indexOf(root);
        if (index >= 0) {
            mRoots.remove(index);
            mParams.remove(index);
            final View view = mViews.remove(index);
            mDyingViews.remove(view);
        }
    }
    if (ThreadedRenderer.sTrimForeground && ThreadedRenderer.isAvailable()) {
        doTrimForeground();
    }
}

可以看到該方法主要是從列表中移除了與要刪除的Window對應的View、ViewRootImpl和LayoutParams,這幾個列表在前面的添加過程也看到過。接著我們看下dispatchDetachedFromWindow()的實現,代碼如下:

void dispatchDetachedFromWindow() {
    ...
    mWindowSession.remove(mWindow);
    ...
}

dispatchDetachedFromWindow()會進行一些資源的釋放,接著調用了IWindowSession類型的remove(),IWindowSession在添加過程中已經看到過,它用于與WMS通信,會調用Session.remove(),其內部又會調用WindowManagerService.removeWindow(),該方法會通過Session和IWindow獲取到對應的WindowState,并調用WindowState的removeIfPossible(),最終會調用到WindowState的removeImmediately(),該方法主要會對Window涉及的一些資源進行回收與清理,到這里Window的刪除過程就完成了。

Window的更新過程

我們要更新Window時,會調用WindowManager.updateViewLayout(),這個方法的實現在WindowManagerImpl中,代碼如下所示:

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.updateViewLayout(view, params);
}

該方法依舊是調用WindowManagerGlobal.updateViewLayout()處理,代碼如下所示:

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    ...
    WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    view.setLayoutParams(wparams);
    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        mParams.add(index, wparams);
        root.setLayoutParams(wparams, false);
    }
}

首先是根據View找到要更新的Window的索引,再根據索引更新列表中LayoutParams,接著調用與Window對應的ViewRootImpl的setLayoutParams(),其內部會調用scheduleTraversals(),如下所示:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //1
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

注釋1處的代碼添加了一個mTraversalRunnable,mTraversalRunnable是TraversalRunnable類型,它實現了Runnable接口,run()里的代碼在下一幀渲染時會被執行,我們看下mTraversalRunnable的代碼,如下所示:

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

因此在下一幀渲染時會執行doTraversal(),其內部又會調用performTraversals(),performTraversals()其實我們就很清楚了,它會執行View的measure、layout以及draw流程,因此Window里的視圖會執行上述的流程,但performTraversals()除了執行View的工作流程外,還會調用relayoutWindow(),代碼如下所示:

private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {
    ...
    int relayoutResult = mWindowSession.relayout(
            mWindow, mSeq, params,
            (int) (mView.getMeasuredWidth() * appScale + 0.5f),
            (int) (mView.getMeasuredHeight() * appScale + 0.5f),
            viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
            mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
            mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame,
            mPendingMergedConfiguration, mSurface);
    ...
}

這里的mWindowSession已經見過很多次了,它是個IWindowSession類型的Binder對象,用于與WMS通信,會調用到Session.relayout(),其內部會調用WindowManagerService的relayoutWindow(),這樣就完成了Window的更新操作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373