Android 4.4 雙顯示屏支援實現思路(雙屏異顯)
本文是多年前在Intel Baytrail 平臺上所做過的一個專案的思路總結。當時裝置上有同時支援VGA/HDMI顯示裝置(很Intel吧,跟PC的介面很像吧),需求是在Android 上支援VGA/HDMI兩個螢幕同時顯示,並且同時需要顯示執行兩個應用程式在不同的顯示螢幕下,簡單的就是需要你在HDMI顯示屏上看視訊的同時,在VGA顯示螢幕上操作一個應用。本文簡單介紹了實現方式,至於程式碼不方便放出來,再說Android N都出來了,對這個功能已經接近支援了。本文將按照以下的思路來組織.
在Android 的Framework框架當中,在多display顯示方面,已經有比較完整的框架支援,雖然目前可能還不太完善,本文件將簡單介紹下android的多display 框架,力爭能分析清楚當前框架,找出不足之處,最終能夠提出完善當前框架的方案。因此,本文件將分成以下三個部分:
- ◆現有多Display 框架分析。
- ◆現有多Display 框架的不足。
- ◆完善多Display 框架的方案。
現有多Display 框架分析
在android 4.4.4的程式碼當中,我們已經能夠看到android已經有了大量關於多Display支援的程式碼,並且當前android也已經支援通過WIFI Display將影象投影到一個WIFI裝置,也增加了一個Presentation的特殊Dialog允許將該Dialog繪製在另外一個Display裝置上。本文將從以下四個方面分析當前android版本對多Display框架的支援:
- ◆JAVA FW應用部分,需要提醒注意的是,這個應用部分並不是指應用程式,而是指android FW與應用程式相連線的那一部分。
- ◆JAVA GUI FW部分,主要是DisplayManagerService 以及WindowManagerService對Display當中視窗的管理。
- ◆ Native GUI FW部分,主要是SurfaceFlinger當中對Display當中視窗的繪製,以及影象輸出
- ◆Android Input子系統對多Display的支援。
由於android多Display與android GUI系統緊密耦合,所以在討論android多Display之前,我們先大致瞭解下android GUI框架,在Android framework GUI系統當中,最核心最重要的是系統提供的三個系統service,分別是ActivityManagerService與WindowManagerService.這兩個Java層面的Service, 顧名思義,這兩個service將分別管理activity跟window。以及一個SurfaceFlinger這麼一個Native層的Service。這個service主要負責對所有介面的compose操作。下圖是android GUI框架的一個簡圖,簡單描述了應用程式,JAVA GUI FW,Native GUI FW 這三者之間的關係:
稍微解釋下上面這張圖,應用程式端的三個元件是由Framework當中的三個service來分別負責管理的,Activity由AMS負責管理,其在AMS中對應的是一個ActivityRecord物件,AMS將管理其生命週期。Window則虛擬對應WMS中的一個WindowState.說是“虛擬對應”是因為Window跟WMS並沒有直接的關係,只存在邏輯上的關係。WMS將管理視窗的Z軸以及每個視窗顯示的大小。mDoctorView與SF中的Layer也是虛擬對應的關係,歸納一點就是Layer為mDoctorView提供顯示buffer,從而讓mDoctorView這棵遞迴樹能將自己所有的子view都畫到這個buffer當中,最終SF將compose其管理的所有的Layer,最終顯示到螢幕之上。
WindowManagerService與SurfaceFlinger在android GUI框架當中是分工協作的,WindowManagerService管理邏輯關係,包括Z-Order, 顯示區域大小等等,SurfaceFlinger則根據WindowManagerService的輸入負責具體繪製以及compose。
◆應用部分
在應用程式部分,android最主要是在WindowImpl.java以及ContexImpl.java這兩個類
中增加了一個型別為Display的成員變數mDisplay。下面我們分別從Activity建立,以及應用往WindowManagerService新增window這兩個流程分析下應用部分針對多Display的改動。
Activity的建立
先補充一點Activity建立的一些背景知識,Activity是android當中應用程式的一個基本
單元,拋開其中的複雜流程而言,由上面所簡單介紹到的android GUI 框架,應用程式端與android 負責管理activity的系統服務ActivityManagerService之間大致關係如下圖:
簡單的描述下:每個應用程序都會存在一個ActivityThread物件作為與AMS通訊的介面(ActivityThread的作用不僅僅於此),應用程序當中的所有Activity都是通過它與遠端的AMS建立聯絡,比如Activity A需要建立Activity B, A會通過ActivityThread向AMS傳送Binder呼叫請求,AMS收到請求之後會進行一系列的執行過程,最終AMS會通知Activity B所在的ActivityThread來Create或者Resume Activity B.
有了以上簡單簡單的背景之後,再回過頭來看看應用程式端Activity的建立:
1, ActivityThread handleLauncheActivity 函式,這個函式是響應AMS端往應用程式端的binder呼叫。
2, performLaunchActivity函式,這個函式被handleLauncheActivity呼叫,這個函式當中會建立一個新的Activity物件,然後呼叫createBaseContextForActivity來為該Activity物件建立一個Context上下文物件。
3, createBaseContextForActivity函式需要我們重點關注下
private Context createBaseContextForActivity(ActivityClientRecord r,
final Activity activity) {
ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token);
appContext.setOuterContext(activity);
// For debugging purposes, if the activity's package name contains the value of
// the "debug.use-second-display" system property as a substring, then show
// its content on a secondary display if there is one.
Context baseContext = appContext;
String pkgName = SystemProperties.get("debug.second-display.pkg");
if (pkgName != null && !pkgName.isEmpty()
&& r.packageInfo.mPackageName.contains(pkgName)) {
DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
for (int displayId : dm.getDisplayIds()) {
if (displayId != Display.DEFAULT_DISPLAY) {
Display display = dm.getRealDisplay(displayId, r.token);
baseContext = appContext.createDisplayContext(display);
break;
}
}
}
return baseContext;
}
這個函式中首先呼叫ContextImpl.createActivityContext來建立一個ContextImpl物件,追進去看了之後發現ContextImpl中的mDisplay比賦值成null,這個函式下半部分看起來是一個測試多Display的功能,大致作用是將根據” debug.second-display.pkg”這個系統屬性定義的包名的activity放到其他的display當中,其呼叫的createDisplayContext(display)函式,會讓ContextImpl中的mDisplay被賦值,這為我們完善多Display提供參考。Activity的建立過程先到這裡,至此,我們所提到的WindowImpl.java 以及ContexImpl.java這兩個類當中ContexImpl類中的mDisplay在android預設情況下被賦值成NULL.接下來我們繼續看下應用往WindowManagerService新增window的過程。
新增Window
Activity建立完成之後,應用程式還只是有一個殼,具體需要顯示,繪製,還是要有Window的參與。上面已經有一張圖相信能夠簡單描述了應用程式,WindowManagerService, SurfaceFlinger之間的聯絡了。下圖將描述應用與WindowManagerService之間的聯絡介面:
1,Activity.javaattach 函式,在這個函式當中,會使用到我們上面所提到建立的ComtextImpl物件呼叫:
context.getSystemService(Context.WINDOW_SERVICE),mToken, mComponent.flattenToString(),(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
去建立一個一個WindowManagerImpl物件。
2, 在ContexImpl.java中我們看到:
registerService(WINDOW_SERVICE, new ServiceFetcher() {
Display mDefaultDisplay;
public Object getService(ContextImpl ctx) {
Display display = ctx.mDisplay;
if (display == null) {
if (mDefaultDisplay == null) {
DisplayManager dm = (DisplayManager)ctx.getOuterContext().
getSystemService(Context.DISPLAY_SERVICE);
mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
}
display = mDefaultDisplay;
}
return new WindowManagerImpl(display);
}});
如果ContextImpl物件中的mDisplay為null的話,將使用預設的display為WindowManagerImpl中的mDisplay成員賦值。
3, 新增視窗將使用WindowManagerImpl中的addView函式,該函式使用WindowManagerImpl本身物件的mDisplay成員作為引數,最終將window所在的Display資訊帶到WindowManagerService當中。
至此,應用部分就分析完成,應用部分最終的目的就在於為每個window指定了一個具體的Display資訊。這個資訊將為視窗在SurfaceFlinger中繪製提供幫助。
◆ JAVA GUI FW部分
在JAVA GUI FW部分,主要涉及就是JAVAFW當中對Display的管理,以及WindowManagerService對Display中window的管理兩個部分。我們先看看對Display的管理。
DisplayManagerService作為JAVA FW層中的一個系統服務,在系統的啟動階段由SystemServer啟動,其啟動之後的第一件事情,就嘗試從SurfaceFlinger當中獲取到系統預設的display 與HDMI display,可選擇的建立VirturlDisplay, WifiDisplay, OverlayDisplay.從目前看起來,這三個可選的Display的實現方式差不多,在SurfaceFlinger端都是對應一個VirturlDisplay,通過傳遞一個Surface到SurfaceFlinger端,從而讓SurfaceFlinger在這個surface上繪製一份整個系統的影象。
由於我們需要實現的雙屏是完整的雙屏,所以我們需要在這裡做一些改動,需要能從SurfaceFlinger當中拿到第二個屏的資訊。並且在DisplayManagerService儲存。但是如果第二個屏是HDMI Display的話,這個改動目前可以忽略掉。
WindowManagerService作為JAVA FW層中的一個系統服務,主要管理系統中所有的Window,在WMS中,有一個對應的WindowState物件。
1, addWindow,該函式在WindowManagerService當中,由上文提到的WindowManagerImpl中的addView函式呼叫到,並且將window所在的Display作為引數帶到WMS當中。
2, 在addWindow函式,WMS首先找到視窗所在的display,然後將視窗加到Display中的windowlist當中。
3, WindowState通過設定將自己所在Display的LayerStack值設定成自己的LayerStack值,告知SurfaceFlinger自己所在Display。
mSurfaceControl.setLayerStack(mLayerStack);
其他的視窗管理與非多Display系統差別不大,可能需要改動的是要讓多Display系統中,每個Display當中都需要有一個foucs狀態的視窗。需要有一個處於Resume狀態當中的Activity。
◆ Native GUI FW部分
SurfaceFlinger作為Android在native層比較重要的一個系統服務,主要作用是compose所有的layer,將其繪製輸出到顯示裝置當中,也就是物理Display當中。大致如下圖:
上圖很簡單,SurfaceFlinger需要利用HWC compose 屬於每個Display的layer,並且將其輸出到具體的DisplayDivice當中。WindowManagerService會在JAVA FW中指定每個Layer屬於哪一個Display.
在當前SurfaceFlinger當中,支援預設Display與HDMI Display兩個具體的物理Display.如果需要再加一個物理Display,可能再做一些改動。
◆ Android Input子系統對多Display的支援
從目前的程式碼中看起來,貌似input子系統還不支援多Display系統。
現有多Display 框架的不足
通過以上部分,我們簡單分析了現有的android多Display框架,現在我們簡單梳理下目前多Display系統稍顯不足的地方:
1, 應用端預設都是使用預設的Display來存放視窗,沒有介面可以讓應用程式自由選擇。
2, AMS與WMS當中,需要支援每個Display當中都要有一個Resume狀態的Activity,一個Focus狀態的Window.
3, 在Input子系統中需要增加對多Display的支援,這樣能讓使用者在每個Display上都能夠跟系統互動。
4, SurfaceFlinger當中目前只有預設的Display與HDMI Display兩種物理Display,如果再加一個I2C介面的顯示屏,可能再需要改動。從目前來看HDMI Display能滿足我們的需求,這部分可以暫時略過。
完善多Display 框架的方案
根據上文所提到的目前多Display框架的不足,我們需要針對性的做出一定的改進:
1, 在應用端,需要提供介面讓應用自己選擇自己需要放置視窗的Display 裝置或者我們根據一些policy強制將一些應用視窗放置到指定的Display當中。
2, 我們需要改動AMS與WMS,讓AMS中針對每一個Display都保留一個處於Resume狀態的Activity, 讓WMS為每一個Display保留處於Focus狀態的Window.
3, Input 子系統根據具體需求,可能需要的工作量會比較大,比如如果需要支援的第二個Display同樣具有觸控式螢幕這樣一個輸入裝置,這就需要做比較大的改動。
4, SurfaceFlinger當中需要支援任意多的物理Display,還需要繼續做一些調研。目前優先順序稍微低一點。
以上只是初步設想的方案,還沒有進行過可行性驗證,接下來我會進行可行性驗證,並且完善出更加詳細的方案。
◆ 顯示部分,確定顯示一個視窗介面在某一個Display中,大致如下圖所示:
解釋下上圖所提到的幾個變數:
1, JAVA FW應用程式端指定其應用ContextImpl的Display。
2, JAVA FW WindowManagerService則會獲取對應Display的layerstack將其放置在WindowStateAnimator當中,並且將值設定到SurfaceFlinger當中與之對應的Layer中。
3, SurfaceFlinger根據Layer當中的layerStack成員獲知需要將該Layer繪製到具體哪一個Display當中。
根據分析程式碼,1)部分需要改動。2)部分當中,由於HDMIDisplay預設是Mirror模式,所以也需要一點改動。3)部分當中,對HDMI顯示裝置以及預設支援,暫時不需要任何改動。
針對1)的改動:
private ContextcreateBaseContextForActivity(ActivityClientRecord r,
final Activity activity) {
ContextImpl appContext = ContextImpl.createActivityContext(this,r.packageInfo, r.token);
appContext.setOuterContext(activity);
// For debugging purposes, if theactivity's package name contains the value of
// the "debug.use-second-display" system property as asubstring, then show
// its content on a secondary display if there is one.
Context baseContext = appContext;
String pkgName =SystemProperties.get("debug.second-display.pkg");
++ pkgName = “com.android.gallery3d”
if (pkgName != null && !pkgName.isEmpty()
&&r.packageInfo.mPackageName.contains(pkgName)) {
DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
for (int displayId : dm.getDisplayIds()) {
if (displayId !=Display.DEFAULT_DISPLAY) {
Display display =dm.getRealDisplay(displayId, r.token);
baseContext =appContext.createDisplayContext(display);
break;
}
}
}
return baseContext;
}
該改動只是簡單的將指定的package 放置到非預設的顯示屏當中。
針對2)的改動:
由於DisplayManagerService當中預設將HDMI的顯示屏作為一個Mirror的顯示屏,所以其對應的LayerStack值與DefaultDisplay中的LayerStack值相等。
之前提到過,由於目前AMS/WMS分別只有一個處於focus狀態的Activity與Window.為了不影響主屏上正常顯示,我們必須讓主屏跟第二個屏都各自擁有一個處於focus狀態的Activity與Window。
1, 目前AMS管理兩個ActivityStack,一個為Home Stack,Launch app活在其中,另外一個為App Stack,所有的其他應用的activity活在其中。修改之後再增加一個ActivityStack,專門用來放置需要放置在第二屏上的activity。
1, 修改了AMS中當前預設流程:
當前AMS在啟動一個新的全屏的activity之後,會預設認為原來的activity已經處於不可見狀態,那麼會通知SurfaceFlinger下次繪製的時候不需要再繪製原來的Activity。這樣會造成如果啟動一個新的Activity到第二屏。那麼主屏上的所有介面都不會再被繪製。所以修改掉了預設流程:如果啟動的是到第二屏的應用,則原本針對Home Stack與App Stack中activity隱藏的流程不再繼續走下去。
◆ Input 子系統 Android Input子系統對多Display的支援
InputReader在派發input事件的時候,已經會帶上display的引數,只不過目前使用的是預設的default display。
1) 對touch屏的支援,目前程式碼中看起來,EventHub在上報event事件時會告知device ID,只需要將touch屏的device ID與Display關聯在一起就能夠完美支援。
2) 對滑鼠的支援,滑鼠輸入事件支援雙屏主要需要修改以下兩個方面:
a) 需要告知PointerController主屏與第二屏的尺寸大小。目前PointerController只知道主屏的大小。
b) 需要修改將Event Hub上報的滑鼠移動事件轉換成android 滑鼠事件的計算方式,具體就是原來系統當中,滑鼠事件被限制在主屏的大小範圍當中,修改之後需要根據相應的闕值將Event hub上報的滑鼠事件轉換成跟Display相關的android滑鼠事件。
c) 滑鼠圖示的繪製,根據上面所提到的,只需要修改滑鼠圖示的視窗在SurfaceFlinger當中Layer的layerStack變數就能讓其繪製在指定的Display當中。
最後的Framework改成的結構是這樣的,目前Android N上原生的實現框架已經搭得差不太多了。