Android 4.4 双显示屏支持实现思路(双屏异显)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ljp1205/article/details/53540739

      本文是多年前在Intel Baytrail 平台上所做过的一个项目的思路总结。当时设备上有同时支持VGA/HDMI显示设备(很Intel吧,跟PC的接口很像吧),需求是在Android 上支持VGA/HDMI两个屏幕同时显示,并且同时需要显示运行两个应用程序在不同的显示屏幕下,简单的就是需要你在HDMI显示屏上看视频的同时,在VGA显示屏幕上操作一个应用。本文简单介绍了实现方式,至于代码不方便放出来,再说Android N都出来了,对这个功能已经接近支持了。本文将按照以下的思路来组织.


  

在Android 的Framework框架当中,在多display显示方面,已经有比较完整的框架支持,虽然目前可能还不太完善,本文档将简单介绍下android的多display 框架,力争能分析清楚当前框架,找出不足之处,最终能够提出完善当前框架的方案。因此,本文档将分成以下三个部分:

  1. ◆现有多Display 框架分析。
  2. ◆现有多Display 框架的不足。
  3. ◆完善多Display 框架的方案。

现有多Display 框架分析

在android 4.4.4的代码当中,我们已经能够看到android已经有了大量关于多Display支持的代码,并且当前android也已经支持通过WIFI Display将图像投影到一个WIFI设备,也增加了一个Presentation的特殊Dialog允许将该Dialog绘制在另外一个Display设备上。本文将从以下四个方面分析当前android版本对多Display框架的支持:

  1. ◆JAVA FW应用部分,需要提醒注意的是,这个应用部分并不是指应用程序,而是指android FW与应用程序相连接的那一部分。
  2. ◆JAVA GUI FW部分,主要是DisplayManagerService 以及WindowManagerService对Display当中窗口的管理。
  3. ◆ Native GUI FW部分,主要是SurfaceFlinger当中对Display当中窗口的绘制,以及图像输出
  4. ◆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上原生的实现框架已经搭得差不太多了。



猜你喜欢

转载自blog.csdn.net/ljp1205/article/details/53540739
4.4