WMS(一)

Android知識總結(jié)

一、WMS簡介

由圖可見WindowManger是對Window進(jìn)行管理,然后交給WMS進(jìn)行處理。View是由抽象出來的Window進(jìn)行管理的,我們所看到的是View。

常見的Window有:Dialog、PopupWindow應(yīng)用程序窗口、輸入法窗口系統(tǒng)錯誤窗口,Toast。

1.1、Activity與Window相關(guān)概念

  • 1、Activity只負(fù)責(zé)生命周期和事件處理
  • 2、Window只控制視圖
  • 3、一個Activity包含一個Window,如果Activity沒有Window,那就相當(dāng)于Service
  • 4、AMS統(tǒng)一調(diào)度所有應(yīng)用程序的Activity
  • 5、WMS控制所有Window的顯示與隱藏以及要顯示的位置

1.2、Window

Window 是一個抽象類,具體實(shí)現(xiàn)類為 PhoneWindow ,它對 View 進(jìn)行管理。Window是View的容器,View是Window的具體表現(xiàn)內(nèi)容。

Window表明它是和窗口相關(guān)的,窗口是一個抽象的概念,從用戶的角度來講,它是一個界面;從SurfaceFlinger的角度來看,它是一個Layer,承載著和界面有關(guān)的數(shù)據(jù)和屬性;從WMS角度來看,它是一個WIndowState,用于管理和界面有關(guān)的狀態(tài)。

  • 1、表示一個窗口的概念,是所有View的直接管理者,任何視圖都通過 Window 呈現(xiàn)(點(diǎn)擊事件由Window->DecorView->View; Activity的setContentView底層通過Window完成)。
  • 2、Window是一個抽象類,具體實(shí)現(xiàn)是 PhoneWindow
  • 3、創(chuàng)建Window需要通過WindowManager創(chuàng)建
  • 4、WindowManager是外界訪問Window的入口
  • 5、Window具體實(shí)現(xiàn)位于WindowManagerService中
  • 6、WindowManager 和 WindowManagerService 的交互是通過IPC完成
  • 7、定義窗口樣式和行為的抽象基類,用于作為頂層的 view 加到WindowManager 中,其實(shí)現(xiàn)類是 PhoneWindow。
  • 8、每個Window都需要指定一個Type(應(yīng)用窗口、子窗口、系統(tǒng)窗口)。Activity對應(yīng)的窗口是應(yīng)用窗口;PopupWindow,ContextMenu,OptionMenu是常用的子窗口;像Toast和系統(tǒng)警告提示框(如ANR)就是系窗口,還有很多應(yīng)用的懸浮框也屬于系統(tǒng)窗口類型。

1.3、WindowManager

WindowManager:繼承 ViewManager,用來在應(yīng)用與window之間的管理接口,管理窗口順序,消息等,用于溝通WMS和Window的橋梁。它的實(shí)現(xiàn)類為 WindowManagerImpl。(相當(dāng)于一個代理)

1.4、WindowManagerService

簡稱Wms,WindowManagerService 管理窗口的創(chuàng)建、更新和刪除,顯示順序等,另外窗口的大小和層級也是由WMS進(jìn)行管理,是WindowManager這個管理接品的真正的實(shí)現(xiàn)類。WindowManagerService 運(yùn)行在System_server進(jìn)程,作為服務(wù)端,客戶端(應(yīng)用程序)通過IPC調(diào)用和它進(jìn)行交互。

1.5、Token

這里提到的Token主是指窗口令牌(Window Token),是一種特殊的Binder令牌,WMS 用它唯一標(biāo)識系統(tǒng)中的一個窗口。

1.6、Window的type

  • 1、應(yīng)用窗口:層級范圍是1~99,Activity 就是一個典型的應(yīng)用窗口;
  • 2、子窗口:層級范圍是1000~1999,不能獨(dú)立,需要依附在其他窗口才可以(和其他窗口公用一個Token),PopupWindow就屬于子窗口;
  • 3、系統(tǒng)窗口:層級范圍是2000~2999,輸入法窗口,Toast系統(tǒng)音量條窗口,系統(tǒng)錯誤窗口都屬于系統(tǒng)窗口。

各級別type值在WindowManager中的定義分別為

  • 應(yīng)用窗口(1~99)
//第一個應(yīng)用窗口
public static final int FIRST_APPLICATION_WINDOW = 1;
//所有程序窗口的base窗口,其他應(yīng)用程序窗口都顯示在它上面
public static final int TYPE_BASE_APPLICATION  = 1;
//所有Activity的窗口,只能配合Activity在當(dāng)前APP使用
public static final int TYPE_APPLICATION  = 2;
//目標(biāo)應(yīng)用窗口未啟動之前的那個窗口
public static final int TYPE_APPLICATION_STARTING = 3;
//最后一個應(yīng)用窗口
public static final int LAST_APPLICATION_WINDOW = 99;
  • 子窗口(1000~1999)
//第一個子窗口
public static final int FIRST_SUB_WINDOW  = 1000;
// 面板窗口,顯示于宿主窗口的上層,只能配合Activity在當(dāng)前APP使用
public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
// 媒體窗口(例如視頻),顯示于宿主窗口下層
public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
// 應(yīng)用程序窗口的子面板,只能配合Activity在當(dāng)前APP使用(PopupWindow默認(rèn)就是這個Type)
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
//對話框窗口,只能配合Activity在當(dāng)前APP使用
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
//
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW+4;
//最后一個子窗口
public static final int LAST_SUB_WINDOW = 1999;
  • 系統(tǒng)窗口(2000~2999)
//系統(tǒng)窗口,非應(yīng)用程序創(chuàng)建
public static final int FIRST_SYSTEM_WINDOW = 2000;
//狀態(tài)欄,只能有一個狀態(tài)欄,位于屏幕頂端,其他窗口都位于它下方
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
//搜索欄,只能有一個搜索欄,位于屏幕上方
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW + 1;
//電話窗口,它用于電話交互(特別是呼入),置于所有應(yīng)用程序之上,狀態(tài)欄之下,
public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW + 2;
//系統(tǒng)警告提示窗口,出現(xiàn)在應(yīng)用程序窗口之上,屬于懸浮窗, 但是會被禁止
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW + 3;
//信息窗口,用于顯示Toast, 不屬于懸浮窗, 但有懸浮窗的功能,缺點(diǎn)是在Android2.3上無法接收點(diǎn)擊事件
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW + 5;
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW + 4;
//鎖屏窗口
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW + 4;
//系統(tǒng)頂層窗口,顯示在其他一切內(nèi)容之上,此窗口不能獲得輸入焦點(diǎn),否則影響鎖屏
public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW + 6;
//電話優(yōu)先,當(dāng)鎖屏?xí)r顯示,此窗口不能獲得輸入焦點(diǎn),否則影響鎖屏
public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW + 7;
//系統(tǒng)對話框窗口
public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW + 8;
//鎖屏?xí)r顯示的對話框
public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW + 9;
//系統(tǒng)內(nèi)部錯誤提示,顯示在任何窗口之上
public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW + 10;
//內(nèi)部輸入法窗口,顯示于普通UI之上,應(yīng)用程序可重新布局以免被此窗口覆蓋
public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW + 11;
//內(nèi)部輸入法對話框,顯示于當(dāng)前輸入法窗口之上
public static final int TYPE_INPUT_METHOD_DIALOG = FIRST_SYSTEM_WINDOW + 12;
//墻紙窗口
public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW + 13;
//狀態(tài)欄的滑動面板
public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW + 14;
//安全系統(tǒng)覆蓋窗口,這些窗戶必須不帶輸入焦點(diǎn),否則會干擾鍵盤
public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW + 15;
//最后一個系統(tǒng)窗口
public static final int LAST_SYSTEM_WINDOW = 2999;  

窗口flags顯示屬性在WindowManager中也有定義:

//窗口特征標(biāo)記
public int flags;
//當(dāng)該window對用戶可見的時候,允許鎖屏
public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;
//窗口后面的所有內(nèi)容都變暗
public static final int FLAG_DIM_BEHIND = 0x00000002;
//Flag:窗口后面的所有內(nèi)容都變模糊
public static final int FLAG_BLUR_BEHIND = 0x00000004;
//窗口不能獲得焦點(diǎn)
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
//窗口不接受觸摸屏事件
public static final int FLAG_NOT_TOUCHABLE = 0x00000010;
//即使在該window在可獲得焦點(diǎn)情況下,允許該窗口之外的點(diǎn)擊事件傳遞到當(dāng)前窗口后面的的窗口去
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;
//當(dāng)手機(jī)處于睡眠狀態(tài)時,如果屏幕被按下,那么該window將第一個收到觸摸事件
public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
//當(dāng)該window對用戶可見時,屏幕出于常亮狀態(tài)
public static final int FLAG_KEEP_SCREEN_ON = 0x00000080;
//:讓window占滿整個手機(jī)屏幕,不留任何邊界
public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;
//允許窗口超出整個手機(jī)屏幕
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
//window全屏顯示
public static final int FLAG_FULLSCREEN = 0x00000400;
//恢復(fù)window非全屏顯示
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
//開啟窗口抖動
public static final int FLAG_DITHER = 0x00001000;
//安全內(nèi)容窗口,該窗口顯示時不允許截屏
public static final int FLAG_SECURE = 0x00002000;
//鎖屏?xí)r顯示該窗口
public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
//系統(tǒng)的墻紙顯示在該窗口之后
public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
//當(dāng)window被顯示的時候,系統(tǒng)將把它當(dāng)做一個用戶活動事件,以點(diǎn)亮手機(jī)屏幕
public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
//該窗口顯示,消失鍵盤
public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
//當(dāng)該window在可以接受觸摸屏情況下,讓因在該window之外,而發(fā)送到后面的window的觸摸屏可以支持split touch
public static final int FLAG_SPLIT_TOUCH = 0x00800000;
//對該window進(jìn)行硬件加速,該flag必須在Activity或Dialog的Content View之前進(jìn)行設(shè)置
public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
//讓window占滿整個手機(jī)屏幕,不留任何邊界
public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
//透明狀態(tài)欄
public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
//透明導(dǎo)航欄
public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;

二、Window分類

Window 的類型有很多種,比如應(yīng)用程序窗口、系統(tǒng)錯誤窗口、輸入法窗口、PopWindow、Toast、Dialog 等??偟膩碚f Window 分為三大類型,分別是 Application Window(應(yīng)用程序窗口)、Sub Window(子窗口)、System Window (系統(tǒng)窗口),每個大類型中又包含了很多種類型,它們都定義在 WindowManager 的靜態(tài)內(nèi)部類 LayoutParams 中,接下來分別對這三大類型進(jìn)行講解。

  • Application Window:Activity就是一個典型的應(yīng)用程序窗口。
  • Sub Window:子窗口,顧名思義,它不能獨(dú)立存在,需要附著在其他窗口才可以,PopupWindow就屬于子窗口。
  • System Window:輸入法窗口、系統(tǒng)音量條窗口、系統(tǒng)錯誤窗口都屬于系統(tǒng)窗口。
窗口的次序

三、WindowManager

public interface ViewManager{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

WindowManager 其實(shí)是一個接口,它繼承自 ViewManager , ViewManager 中定義了 3 個抽象方法,分別是用來添加、更新、刪除 View 的,WindowManager 繼承了父類接口的方法,說明也具備了父類的能力。WindowManagerImpl是WindowManager的實(shí)現(xiàn)類,但是具體的功能都會委托給WindowManagerGlobal來實(shí)現(xiàn)。

此從運(yùn)用橋接模式,繼承 ViewManager 接口的類有WindowManagerViewGroup。

3.1、窗口的標(biāo)志

Window 的標(biāo)志也就是 Flag, 用于控制 Window 的現(xiàn)實(shí),同樣被定義在 WindowManager 的內(nèi)部類 LayoutParams 中,一共有 20 多個,這里給出幾個比較常用的,如下:

Window Flag 說明
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 只要窗口可見,就允許在開啟狀態(tài)的屏幕上鎖屏
FLAG_NOT_FOCUSABLE 窗口不能獲得輸入焦點(diǎn),設(shè)置該標(biāo)志的同時,F(xiàn)LAG_NOT_TOUCH_MODAL 也會被設(shè)置
FLAG_NOT_TOUCHABLE 窗口不接收任何觸摸事件
FLAG_NOT_TOUCH_MODAL 將該窗口區(qū)域外的觸摸事件傳遞給其它的 Window,而自己只會處理窗口區(qū)域內(nèi)的觸摸事件
FLAG_KEEP_SCREEN_NO 只要窗口可見,屏幕就會一直常亮
FLAG_LAYOUT_NO_LIMITS 允許窗口超過屏幕之外
FLAG_FULISCREEN 隱藏所有的屏幕裝飾窗口,比如在游戲、播放器中的全屏顯示
FLAG_SHOW_WHEN_LOCKED 窗口可以在鎖屏的窗口之上顯示
FLAG_IGNORE_CHEEK_PRESSES 當(dāng)用戶的臉貼近屏幕時(比如打電話),不會去響應(yīng)事件
FLAG_TURN_SCREEN_NO 窗口顯示時將屏幕點(diǎn)亮

設(shè)置 Window 的 Flag 有 3 種方法

    1. 通過 Window 的 addFlag 方法
Window mWindow = getWindow();
mWindow.addFlag(WindowManager.LayoutParams.FLAG_FULLSCREEN);
    1. 通過 Window 的 setFlags 方法
Window mWindow = getWindow();
mWindow.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN)
    1. 給 LayoutParams 設(shè)置 Flag, 并通過 WindowManager 的 addView 方法進(jìn)行添加
WindowManager.LayoutParams mWindowLayoutParams = new WindowManager.LayoutParams();
mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN;
WindowManager mWindowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
Text mText = new Text(this);
mWindowManager.addView(mTextView,mWindowLayoutParams);

3.2、軟件盤相關(guān)模式

窗口與窗口的疊加是十分常見的場景,但是如果其中的窗口是軟件盤的窗口,可能就會出現(xiàn)一些問題,比如典型的用戶登錄頁面,默認(rèn)的情況彈出軟件盤窗口可能遮擋輸入框下方的按鈕,這樣用戶體驗非常糟糕。為了使得軟鍵盤窗口能夠按照期望來顯示, WindowManager 的靜態(tài)內(nèi)部類 LayoutParams 中定義了軟件盤相關(guān)模式,這里給出常用的幾個:

SoftInputMode 描述
SOFT_INPUT_STATE_UNSPECIFIED 沒有設(shè)定狀態(tài),系統(tǒng)會選擇一個合適的狀態(tài)或依賴于主題的設(shè)置
SOFT_INPUT_STATE_UNCHANGED 不會改變軟鍵盤狀態(tài)
SOFT_INPUT_STATE_ALWAYS_HIDDEN 當(dāng)窗口獲取焦點(diǎn)時,軟鍵盤總是被隱藏
SOFT_INPUT_ADJUST_RESIZE 當(dāng)軟鍵盤彈出時,窗口會調(diào)整大小
SOFT_INPUT_ADJUST_PAN 當(dāng)軟鍵盤彈出時,窗口不需要調(diào)整大小,要確保輸入焦點(diǎn)是可見的
SOFT_INPUT_STATE_HIDDEN 當(dāng)用戶進(jìn)入該窗口時,軟鍵盤默認(rèn)隱蔽

從上面給出的 SoftInputMode ,可以發(fā)現(xiàn),它們與 AndroidManifest.xml 中 Activity 的屬性 android:windowsoftInputMode 是對應(yīng)的。因此,除了在 AndroidManifest.xml 中為 Activity 配置還可以通過代碼動態(tài)配置,如下所示:

geWindow().setSoftInputMode(MindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);

四、添加Window

添加Vindow流程圖
  • Activity#attach()方法之內(nèi)PhoneWindow被創(chuàng)建,并同時創(chuàng)建一WindowManagerImpl負(fù)責(zé)維護(hù)PhoneWindow內(nèi)的內(nèi)容。
  • 在Activity#onCreate()中調(diào)用setContentView()方法,這個方法內(nèi)部創(chuàng)建一個DecorView實(shí)例作為PhoneWindow的實(shí)體內(nèi)容。
  • WindowManagerImpl決定管理DecorView,并創(chuàng)建一個ViewRootImpl實(shí)例,將ViewRootImpl與View樹進(jìn)行關(guān)聯(lián),這樣ViewRootImpl就可以指揮View樹的具體工作。

添加過程如下文章:
View 繪制流程(一)
View 繪制流程(二)

下面我們看下mWindowSession.addToDisplayAsUser(...)流程。
Session 實(shí)現(xiàn)了IWindowSession.Stub,我們看Session@addToDisplayAsUser 方法

public int addToDisplayAsUser(IWindow window, int seq, WindowManager.LayoutParams attrs,
    int viewVisibility, int displayId, int userId, Rect outFrame,
    Rect outContentInsets, Rect outStableInsets,
    DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
    InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
            outContentInsets, outStableInsets, outDisplayCutout, outInputChannel,
            outInsetsState, outActiveControls, userId);
}

然后回到WindowManagerService@addWindow(...) 中。

4.1、Session

ArraySet類型的變量,元素類型為Session。它主要用于進(jìn)程間通信,其他的應(yīng)用程序進(jìn)程想要和WMS進(jìn)程進(jìn)行通信就需要經(jīng)過Session,并且每個應(yīng)用程序進(jìn)程都會對應(yīng)一個Session,WMS保存這些Session用來記錄所有向WMS提出窗口管理服務(wù)的客戶端。

WindowManagerGlobal@addView()中會初始化 Session

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow, int userId) {
    root = new ViewRootImpl(view.getContext(), display);
}

ViewRootImpl.java

public ViewRootImpl(Context context, Display display) {
    this(context, display, WindowManagerGlobal.getWindowSession(),
            false /* useSfChoreographer */);
}

我們看WindowManagerGlobal.getWindowSession()方法

private static IWindowSession sWindowSession;

public static IWindowSession getWindowSession() {
    synchronized (WindowManagerGlobal.class) {
        if (sWindowSession == null) {
            try {
                InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
                IWindowManager windowManager = getWindowManagerService();
                //獲取 Session
                sWindowSession = windowManager.openSession(
                        new IWindowSessionCallback.Stub() {
                            @Override
                            public void onAnimatorScaleChanged(float scale) {
                                ValueAnimator.setDurationScale(scale);
                            }
                        });
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        return sWindowSession;
    }
}

會進(jìn)入WindowManagerService@openSession

public IWindowSession openSession(IWindowSessionCallback callback) {
      return new Session(this, callback);
}
與WMS通信

在ViewRootImpl中mSession是IWindowSession類型,它是Binder對象,用于進(jìn)行線程間通信,IWindowSession是Client端的代理,它的Server端的實(shí)現(xiàn)為Session。從上圖可以看出,本地進(jìn)程的ViewRootImpl是想和WMS進(jìn)行通信需要經(jīng)過Session。

4.2、WindowManagerImpl、WindowManagerGlobal、ViewRootImpl

WindowManagerGlobal是一個單例類,一個進(jìn)程只有一個實(shí)例。它管理者所有Window的ViewRootImpl、DecorView、LayoutParams。

WindowManagerGlobal
  • WindowManagerImpl:確定 View 屬于哪個屏幕,哪個父窗口
  • WindowManagerGlobal:管理整個進(jìn)程,所有的窗口信息
  • ViewRootImpl:WindowManagerGlobal 實(shí)際操作者,操作自己的窗口和WMS進(jìn)行交互
// Window的DecorView集合
private final ArrayList<View> mViews = new ArrayList<View>();
// DecorView的管理者ViewRootImpl的集合
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
// Window布局參數(shù)的集合
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();

4.3、activity與 PhoneWindow與DecorView關(guān)系

4.4、ViewRootImpl

ViewRootImpl是View樹的樹根,但它卻又不是View,實(shí)現(xiàn)了View與WindowManager之間的通信協(xié)議,在WindowManagerGloble中的addView中被建立,是頂層DecorView的ViewParent。

  • 作用
  • 1、View樹的樹根并管理View樹
  • 2、觸發(fā)View的測量、布局和繪制
  • 3、輸入響應(yīng)的中轉(zhuǎn)站
  • 4、負(fù)責(zé)與WMS進(jìn)行進(jìn)程間通信

4.5、DecorView

DecorView
  • 在一個Activity或者Dialog中,是包含一個Window窗口的對象,Window并不是真正的實(shí)體內(nèi)容,通過View來表達(dá)真正的頁面內(nèi)容,這個View就是DecorView。
  • DecorView是FrameLayout的子類,它可以被認(rèn)為是Android視圖樹的根節(jié)點(diǎn)視圖。
  • 在Activity的生命周期onCreate方法里面,是會設(shè)置好布局內(nèi)容通過setContentView(布局id)的方式,這里是通過xml解析器轉(zhuǎn)化為一個View,這個View會被添加到ContentView中去,成為唯一的子View。

五、事件分發(fā)

DecorView雖然是ViewGroup,但它并不會直接分發(fā)給它的child,也不會傳遞給它的父View,而是先分發(fā)給Activity,為什么要這么做呢?

六、刷新流程

6.1、更新Window

更新Window

我們在使用App應(yīng)用的時候,如果需要輸入內(nèi)容,會需要鍵盤窗口,鍵盤窗口的彈出會引起之前窗口大小變化,這里就會發(fā)生Activity對應(yīng)的窗口的更新。還有在橫豎屏切換的時候,附生在當(dāng)前Activity上面的PopupWindow、Dialog是會發(fā)生窗體變換的,這里面也是調(diào)用了窗口的更新。

窗口的更新是一個非常常見的事情,現(xiàn)在開始了解它的執(zhí)行流程,上圖中WMImpl指的是WindowManagerImpl,它是WindowManager的實(shí)現(xiàn)類,但是似乎沒有做過多的事情,直接委托給了WMGlobal(WindowManagerGlobal)。

WindowManagerImpl@updateViewLayout開始流程:

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

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

進(jìn)入WindowManagerGlobal@updateViewLayout方法

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    //參數(shù)校驗
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    //設(shè)置 lp
    view.setLayoutParams(wparams);

    synchronized (mLock) {
        int index = findViewLocked(view, true);  //通過View找到DecorView 的索引值
        //通過索引值找到 ViewRootImpl 數(shù)組中的對象
        ViewRootImpl root = mRoots.get(index); 
        mParams.remove(index); //移除舊的 params
        mParams.add(index, wparams); //重新設(shè)置 params
        root.setLayoutParams(wparams, false);
    }
}
private int findViewLocked(View view, boolean required) {
    final int index = mViews.indexOf(view); //獲取 View 在 DecorView 的位置
    if (required && index < 0) {
        throw new IllegalArgumentException("View=" + view + " not attached to window manager");
    }
    return index;
}

ViewRootImpl@setLayoutParams

void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) 
    ...
     scheduleTraversals();
    ...
}

執(zhí)行scheduleTraversals()流程見:
View 繪制流程(一)

6.2、UI刷新

以電影為例,動畫至少要達(dá)到24FPS,才能保證畫面的流暢性,低于這個值,肉眼會感覺到卡頓。在手機(jī)上,這個值被調(diào)整到60FPS,增加絲滑度,這也是為什么有個(1000/60)16ms的指標(biāo),一般而言目前的Android系統(tǒng)FPS也就是60,它是通過了一個VSYNC來保證每16ms最多繪制一幀。

簡而言之:UI必須至少等待16ms的間隔才會繪制下一幀,所以連續(xù)兩次setTextView只會觸發(fā)一次重繪。

UI刷新.png

setText最終調(diào)用invalidate申請重繪,最后走到ViewRootImpl的invalidate,請求VSYNC,在請求VSYNC的時候,會添加一個同步柵欄,防止UI線程中同步消息執(zhí)行,這樣做為了加快VSYNC的響應(yīng)速度,如果不設(shè)置,VSYNC到來的時候,正在執(zhí)行一個同步消息,那么UI更新的Task就會被延遲執(zhí)行,這是Android的Looper跟MessageQueue決定的。

6.3、刷新流程

申請Vsync流程
VSync回來流程

等到VSYNC到來后,會移除同步柵欄,并率先開始執(zhí)行當(dāng)前幀的處理,調(diào)用邏輯如下。

執(zhí)行流程
  • 代碼講解
    同TextView類似,View內(nèi)容改變一般都會調(diào)用invalidate觸發(fā)視圖重繪,這中間經(jīng)歷了什么呢?View會遞歸的調(diào)用父容器的invalidateChild,逐級回溯,最終走到ViewRootImpl的invalidate。
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }

ViewRootImpl.java

void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        scheduleTraversals();
    }
}

ViewRootImpl會調(diào)用scheduleTraversals準(zhǔn)備重繪,但是,重繪一般不會立即執(zhí)行,而是往Choreographer的Choreographer.CALLBACK_TRAVERSAL隊列中添加了一個mTraversalRunnable,同時申請VSYNC,這個mTraversalRunnable要一直等到申請的VSYNC到來后才會被執(zhí)行。

// 將UI繪制的mTraversalRunnable加入到下次垂直同步信號到來的等待callback中去
 // mTraversalScheduled用來保證本次Traversals未執(zhí)行前,不會要求遍歷兩邊,浪費(fèi)16ms內(nèi),不需要繪制兩次
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 防止同步柵欄,同步柵欄的意思就是攔截同步消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // postCallback的時候,順便請求vnsc垂直同步信號scheduleVsyncLocked
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         <!--添加一個處理觸摸事件的回調(diào),防止中間有Touch事件過來-->
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

Choreographer編舞者postCallBack中的mTraversalRunnable是TraversalRunnable對象, 內(nèi)容如下:

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

doTraversal是調(diào)用的ViewRootImpl的doTraversal,最后會調(diào)用到PerformTraversal,開始View的測量、布局、繪制的流程。

6.4、View 繪制流程

繪制流程

上圖中就是一個完成的繪制流程,對應(yīng)上了第一節(jié)中提到的三個步驟:

  • 1)、performMeasure():從根節(jié)點(diǎn)向下遍歷View樹,完成所有ViewGroup和View的測量工作,計算出所有ViewGroup和View顯示出來需要的高度和寬度;

  • 2)、performLayout():從根節(jié)點(diǎn)向下遍歷View樹,完成所有ViewGroup和View的布局計算工作,根據(jù)測量出來的寬高及自身屬性,計算出所有ViewGroup和View顯示在屏幕上的區(qū)域;

  • 3)、performDraw():從根節(jié)點(diǎn)向下遍歷View樹,完成所有ViewGroup和View的繪制工作,根據(jù)布局過程計算出的顯示區(qū)域,將所有View的當(dāng)前需顯示的內(nèi)容畫到屏幕上。

繪制過程如下文章:
View 繪制流程(一)
View 繪制流程(二)

6.5、UI局部重繪

某一個View重繪刷新,并不會導(dǎo)致所有View都進(jìn)行一次measure、layout、draw,只是這個待刷新View鏈路需要調(diào)整,剩余的View可能不需要浪費(fèi)精力再來一遍。

總結(jié)

  • 1、android一般60FPS,是VSYNC及決定的,每16ms最多一幀
  • 2、VSYNC要客戶端主動申請,才會有VSYNC到來才會刷新
  • 3、UI沒更改,不會請求VSYNC也就不會刷新
  • 3、UI局部重繪其實(shí)只會去重繪帶刷新的View

面試題

  • 我們都知道Android的刷新頻率是60幀/秒,這是不是意味著每隔16ms就會調(diào)用一次onDraw方法?
    這里60幀/秒是屏幕刷新頻率,但是是否會調(diào)用onDraw()方法要看應(yīng)用是否調(diào)用requestLayout()進(jìn)行注冊監(jiān)聽。

  • 如果界面不需要重繪,那么還16ms到后還會刷新屏幕嗎?
    如果不需要重繪,那么應(yīng)用就不會受到Vsync信號,但是還是會進(jìn)行刷新,只不過繪制的數(shù)據(jù)不變而已;

  • 我們調(diào)用invalidate()之后會馬上進(jìn)行屏幕刷新嗎?
    不會,到等到下一個Vsync信號到來

  • 我們說丟幀是因為主線程做了耗時操作,為什么主線程做了耗時操作就會引起丟幀?
    原因是,如果在主線程做了耗時操作,就會影響下一幀的繪制,導(dǎo)致界面無法在這個Vsync時間進(jìn)行刷新,導(dǎo)致丟幀了。

  • 如果在屏幕快要刷新的時候才去OnDraw()繪制,會丟幀嗎?
    這個沒有太大關(guān)系,因為Vsync信號是周期的,我們什么時候發(fā)起onDraw()不會影響界面刷新;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容