Android Nougat多窗口简析

来源:互联网 发布:angular.js介绍 编辑:程序博客网 时间:2024/05/20 01:39
一、概述
    Android从7.0开始支持多窗口,官方终于支持这一功能了。其实很多ODM厂商早已实现该功能,实现方式各种各样,最通用的方案是多Stack方案,比较痛苦的是每次版本升级都要花很大力气进行适配和解各种乱七八糟的bug,现在终于一统江湖了,并且第三方应用开始对多窗口进行适配兼容。

    Android原生的多窗口功能比较强大,支持四种模式:全屏、分屏、画中画、FreeForm模式。目前都是可以直接或间接开启的,还有一些bug。多窗口主要涉及ActivityManagerService、WindowManagerService、Input三个模块,前两个模块本身已经很复杂了,现在支持四种模式多窗口,整个复杂度上升了几倍(ActivityManagerService简称AMS,WindowManagerService简称WMS)。

    本文所有分析均基于Android 7.1。

二、原理框架
        Android原生多窗口也是多Stack方案,即存在多个ActivityStack。ActivityStack是一个抽象的栈,每个栈都有自己的屏幕区域bound和id,Activity是以Task方式组织并放在某一个Stack中的。比如,桌面Launcher、任务栏Recents属于id=HOME_STACK的栈中。例如,上下分屏模式,此时就有上下两个栈,屏幕被分割成上下两部分bound。

    AMS和WMS中对Stack分别用ActivityStack和TaskTack描述,通过StackId来映射。对Task用分别用TaskRecord、Task描述,通过TaskId来映射。AMS与WMS中的Stack、Task堆栈顺序会在Activity启动之前进行同步。每个窗口都有一个Z-Order值,保存在WindowState.mLayer中,值越大窗口越高。窗口Z-Order值的计算是又是根据Stack、Task堆栈顺序来的,所以AMS中的Stack、Task堆栈最终决定窗口的Z-Order(浮窗例外)。

    AMS中Stack、Task栈保存的位置。Stack栈保存在ActivityStackSupervisor.ActivityDisplay的mStacks中。ActivityStack.mStacks变量是对ActivityStackSupervisor.ActivityDisplay的mStacks的引用,所以可以看到对Stack栈的调整直接在ActivityStack函数中完成,比如moveToFront()。Task栈保存在ActivityStack的mTaskHistory中。

    WMS中Stack、Task栈保存的位置。Stack栈保存在DisplayContent.mStacks中。Task栈保存在TaskStack.mTasks中。

    每个Activity显示在所属ActivityStack的bound区域内,多个Activity显示在各自ActivityStack的bound区域内,这样就可以实现多窗口。但是FreeForm模式下,Activity的bound由所属Task决定,而非Stack。多窗口不仅仅是控制Activity放入不同ActivityStack中,同时还要改变Activity的生命周期,即Focus Activity是resume状态,其他可见Activity是Pause状态,并不会进入Stop状态。

    整个系统中只会有一个Focus Stack,一个Focus Activity。用户在哪个Activity中操作,Focus Activity便指向该Activity,Focus Stack便指向Focus Activity所属的Stack。注意,画中画模式下,浮层Activity无法成为Focus Activity,故浮层属于的Stack并非Focus Stack。

    进入/退出多窗口按以下逻辑框架顺序进行处理:
        step1.调整ActivityStack栈
        step2.调整Task栈
        step3.调整Activity堆栈,调整WMS中APPWindowToken堆栈
        step4.调用Activity生命周期函数
        step5.添加Window到WMS
        step6.过度动画及应用窗口显示

三、关键函数
    FocusStack切换:ActivityStackSuperVisor.setFocusStackUnchecked()
    FocusActivity切换:ActivityManagerService.setFocusedActivityLocked()
    Activity生命周期:ActivityStack.resumeTopActivityInnerLocked()
    Resize Stack:ActivityStackSuperVisor.resizeStackUncheckedLocked()
    Activity启动:ActivityStarter.startActivityUnchecked()

四、源码分析
1、分屏模式
    进入分屏有两种入口:①在任务栏中拖动一个分屏应用到顶部;②在支持分屏应用界面长按底部任务按键。进入分屏后上半屏区域称为Docked区域,下半屏就是分屏后的任务栏。上面两种方式进入分屏调用的接口分别是AMS.startActivityFromRecents()和AMS.moveTaskToDockedStack()。进入分屏后,通过下半屏任务栏可以选择启动第二个分屏应用,调用接口是AMS.startActivityFromRecents()。



    简单分析下这两种分屏启动方式。在任务栏Recents中拖动一个分屏Phone应用到顶部,进入分屏模式,调用的接口为AMS.startActivityFromRecents()。进入分屏之前PhoneActivity属于id=1(FullScreenStack),任务栏Recents属于id=0(HomeStack),Focus Stack指向HomeStack。进入分屏相当于启动PhoneActivity,并将其放到上半区域Stack中,上半区域为DockedStack(id=3),即将PhoneActivity从id=1(FullScreenStack)移到id=3的DockedStack中,最后将HomeStack的bound resize,处在下半屏。注意分屏后Focus Stack指向上半区域Docked Stack。整个时序如下所示。

 
   在在支持分屏应用界面长按底部任务按键,进入分屏模式,调用的接口为AMS.moveTaskToDockedStack()。进入分屏相当于把PhoneActivity从id=1移到id=3的DockedStack中,同时resize HomeStack。注意这种方式进入分屏后,Focus Stack指向Home Stack。时序图如下,可以看出这种方式进入分屏调用了两个AMS接口。

 
   在分析源码过程中发现了一个有趣的Activity属性FLAG_RESUME_WHILE_PAUSING,表示不等前一个Activity pause完成就立即Resume,可以加快Activity启动。

    启动第二个分屏应用,Activity并不在Stack间移动,仍处于FullScreenStack(id=1),并且FullScreenStack和HomeStack的bound在启动第一个分屏应用时已经等于[0, 0, dw, dh] - DockedStack bound,所以只需将FullScreenStack移动到Stack栈顶即可。可能有人对FullScreenStack的bound有疑问,不是说FullScreen吗,怎么第二个分屏应用位于FullScreenStack中,其实FullScreenStack更准确的理解是:在非多窗口模式下即全屏模式下,第三方Activity均在FullScreenStack中启动;分屏模式下用作第二个分屏Stack;画中画和FreeForm模式下FullScreenStack放置全屏Activity。第二个分屏应用启动时序图如下。


2、画中画模式
    画中画即置顶Activity,Activity的窗口永远位于所有窗口之上,所属的Stack自然位于所有Stack的上面,这类特殊的Stack称为PinnedStack(id=4)。PinnedStack无法成为Focus Stack,Activity也无法成为Focus Activity,故无法接受key事件,也无法成为输入法焦点窗口(截止Android 7.1.1,点击画中画Activity内输入框可以弹出输入法窗口,但无法输入),但可以接收触摸事件(还存在不少bug,比如listview无法滑动等)。


  如何启动画中画,很简单,只需在AndroidManifest中声明Activity时添加android:supportsPictureInPicture="true"和android:resizeableActivity="true”属性,在Activity内部调用enterPictureInPictureMode()便可启动画中画模式。PinnedStack默认大小为[0,0,100, 100],需要修改/frameworks/base/core/res/res/values/config.xml文件


  前面已经说过PinnedStack为非Focusable stack,Activity为非focus activity,resumeFocusedStackTopActivityLocked()只会将stack堆栈中Focus Stack的focus activity进行resume,其他任何activity均为paused或stoped状态,所以画中画Activity是处于paused状态的。画中画模式启动时序图如下。


    PinnedStack置顶堆栈关键逻辑。Stack堆栈顺序调整只有两个地方:①新建一个ActivityStack时,需要add到Stack堆栈;②Activity启动需要Stack移到前台即调用ActivityStack.moveToFront()。那置顶的关键逻辑就在ActivityStack.moveToFront()中addIndex--,即move其他Stack到顶部时,如果顶部是PinnedStack,那么就放在PinnedStack下面一个位置(PinnedStack是always on top)。


3、FreeForm模式
    FreeForm模式效果如下,PC窗口既视感,从任务栏Recents中启动。下图中三个Activity均处在FreeForm模式下,此时并不存在与之对应的三个Stack,而是只有一个FreeForm Stack(stackId=2)存放了三个Task。这种模式下决定Activity显示位置的不是Stack bound,而是Task bound,即每个Task都有自己的bound。


    该模式默认为关闭,需要手动启用,终端输入以下命令即可。


  上面说了FreeForm模式下Activity从Recents中启动,调用的接口为AMS.startActivityFromRecents(),其时序跟从任务栏拖动一个应用进入分屏差不多,只是此处Activity在FreeForm Stack中启动,其他基本流程基本一致,不再分析。


4、窗口resize/拖动
    在分屏模式下,拖动中间bar条改变分屏大小,中间bar条是SystemUI添加到系统一个名为“DockedStackDivider”的浮窗,接收触摸事件,根据MotionEvent的Y值动态调用ActivityManagerService.resizeDockedStack()来改变Docked Stack的bound大小,完成窗口大小调整,AMS.resizeDockedStack()在上面已经分析过了,在此不再分析。

    在FreeForm模式下的窗口缩放和拖拽跟分屏模式不一样,没有SystemUI添加的bar浮窗,但是在FreeForm模式下DecorView与子Content View间插入了一个DecorCaptionView,代码如下所示。


   窗口调整跟拖拽便是在DecorCaptionView中根据触摸事件检测开启的,调用的开启函数便是WindowManagerService.startMovingTask(IWindow window, float startX, float startY)。就拿拖拽来说,用户按住FreeForm Activity的窗口边缘然后移动,此拖动过程分为三步:
  step1.DecorCaptionView检测触摸事件,发现是拖拽或窗口调整,那么跨进程调用WindowManagerService.startMovingTask()开启Task移动;
  step2.WMS.startMovingTask()会创建一个TaskPositioner,TaskPositioner会注册一个InputChannel到InputManager中,此后的触摸事件便会发送到TaskPositioner中来;
  step3.TaskPositioner接收move事件,计算Window Rect然后调用AMS.resizeTask(int taskId, Rect bounds, int resizeMode)来更新Activity窗口bound。

    整个过程相对来说比较简单,感兴趣的自行分析。

5、FreeForm模式下触摸事件
    在触摸事件分发流程中,事件是由InputDispatcher通过socket直接发送给Focus应用进程窗口,这在全屏应用中没什么问题。但是在FreeForm模式下,如下图所示,Focus activity是Setting,点击事件落在Setting窗口之外的QQ空间窗口区域内,此时事件会不再发送给Focus Activity的Setting而是发送给QQ空间,同时会把QQ空间Activity翻上来置为Focus Activity。


    这个Focus Activity切换是怎样实现的呢?我们知道无论Activity如何切换总有一个Focus Activity,在WindowManagerService中对应有一个AppWindowToken类型的mFocusedApp变量,每次focus Activity更新后均会调用WindowManagerService.setFocusTaskRegionLocked()将Focus Activity所在Task的bound更新到TaskTapPointerEventListener.mTouchExcludeRegion中。


    WMS注册了一个PointerEventDispatcher到InputManager中,这是一个特殊的InputChannel,即所有的触摸事件除了会发送给应用进程窗口,同时也会发送到PointerEventDispatcher中来,PointerEventDispatcher中又继续分发给注册进来的TaskTapPointerEventListener,TaskTapPointerEventListener根据down事件是否落在mTouchExcludeRegion区域内来决定Task的切换,如果落在mTouchExcludeRegion之外(即点击Focus Activity窗口之外区域),那么继续调用WMS.handleTapOutsideTask()函数来做进一步处理。WMS.handleTapOutsideTask()函数会查找落点区域Task,然后调用AMS.setFocusedTask()将对应Task翻上来,达到点击切换Task的交互效果。

 
  分屏模式下跟FreeForm模式下触摸事件处理逻辑类似,只是此时切换的是Stack而非Task。

6、多窗口下软键盘弹出
    软键盘的弹出可能会导致Activity窗口往上偏移,收起软键盘后Activity窗口恢复,本质是改变窗口的Frame值。细节太多,在此不再分析,感兴趣的可自行分析。

五、总结
    Android Nugget为了支持多窗口,做了很多改动,细节逻辑异常复杂,到目前为止还存在不少bug,相信在Android 7.2上会进一步完善。如果想了解学习这一块,最好是自己编出image,然后真机debug针对流程做分析。

0 0
原创粉丝点击