Android壁纸机制(Android Q与老版本对比)

一、什么是Android壁纸?

Android中,壁纸分为动态壁纸和静态壁纸两种。静态壁纸是一张图片,动态壁纸是以动画为表现形式,有的可以对用户的操作作出反应。二者表现形式看似差异很大,但是二者的本质是统一的: 它们都以一个Service的形式运行在系统后台,并在一个类型为TYPE_WALLPAPER的窗口上绘制内容。 实质上,静态壁纸是一种特殊的动态壁纸。

Android壁纸管理的三个层次:

  1. WallpaperService和Engine(壁纸的实现原理层次):壁纸的绘制由WallpaperService控制,继承和定制WallpaperEngine是进行壁纸开发的第一步。Engine是WallpaperService的一个内部类,实现了壁纸窗口的创建以及Surface的维护工作。同时Engine还提供了可供子类重写的一系列回调,用于通知开发者壁纸的生命周期、Surface状态变化以及对用户的输入事件作出回应。
  2. WallpaperManagerService(对壁纸的管理方式层次):用于管理壁纸的运行与切换,并通过WallpaperManager类向外界提供操作壁纸的接口。切换壁纸时会取消当前壁纸的WallpaperService的绑定,然后启动新的WallpaperService。Engine类进行窗口创建的时候使用的窗口令牌也是由WallpaperManagerService提供。
  3. WindowManagerService(对壁纸窗口的管理形式):用于计算壁纸窗口的Z轴排序、可见性以及为壁纸应用动画。壁纸窗口的Z序会根据FLAG_SHOW_WALLPAPER标记在其他窗口的LayoutParams.flags中的存在情况而不断调整,相对于其他窗口来说较为不稳定。

二、动态壁纸

1.动态壁纸的启动流程

Android壁纸机制(Android Q与老版本对比)_第1张图片

启动动态壁纸可以通过调用WallpaperManagerService.setWallpaperComponent()方法来完成,这个方法的步骤如下:

1)首先调用mWallpaperMap.get(UserId)来获取壁纸的运行信息。

Q:为什么需要根据UserID来划分壁纸的运行信息?

A:WallpaperManagerService支持多用户机制,设备上的每个用户都可以设置自己的壁纸。mWallpaperMap为每个用户保存了一个WallpaperData实例,其中保存了和壁纸状态相关的运行信息:WallpaperService的ComponentName、ServiceConnection等。当发生用户切换的时候,获取到相应用户的WallpaperData,然后获取ComponentName,这样就可以重新启动用户的壁纸。

2)调用bindWallpaperComponentLocked方法,启动新壁纸的WallpaperService。

a、认证服务资格

这个过程首先会对服务进行一系列认证,确认是一个壁纸服务之后才会启动WallpaperService,检查的内容如下:

  1. 服务必须以android.permission.BIND_WALLPAPER作为其访问权限。这个访问权限是一个签名级的访问权限,以免该服务被意外的应用程序启动
  2. 服务必须被声明可以处理android.service.wallpaper.WallpaperService这个Action,因为WallpaperManagerService会使用这个Action对这个服务进行绑定
  3. 服务必须在AndroidManifest.xml中提供android.service.wallpaper的meta-data,用来提供动态壁纸的开发者、缩略图以及描述文字

服务满足条件之后,就会着手启动目标服务并绑定,步骤如下:

  1. 创建WallpaperConnection,其实现了ServiceConnection接口用于监听和WallpaperService之间的连接状态,同时还实现了IWallpaperConnection.stub,支持跨进程通信。服务绑定成功之后,会在onServiceConnected调用中被发送给WallpaperService,成为WallpaperService和WallpaperManagerService之间通信的桥梁。

  2. 调用mContext.bindServiceAsUser启动指定的服务。之后的流程会在WallpaperConnection.onServiceConnected回调中完成。

  3. 新的壁纸服务启动之后,销毁旧的壁纸服务

  4. 将新的壁纸服务的信息保存到WallpaperData中

    在WallpaperData中会有一个lastDiedTime属性,描述壁纸服务的存活时间,如果小于一定的数值就会认为这个壁纸服务不可靠从而选择默认壁纸。

  5. 向WindowManagerService申请注册一个WALLPAPER类型的窗口令牌,其会再onServiceConnected之后被传递给WallpaperService作为添加窗口的令牌

b、传递创建窗口所需信息

仅仅将指定的壁纸服务启动起来还不能让壁纸显示出来,因为还没有窗口令牌而无法添加窗口。所以这后半部流程会在WallpaperConnection的onServiceConnected方法回调中进行。

在WallpaperService的onBind方法中会返回一个IWallpaperServiceWrapper实例。这个类继承了IWallpaperService.stub。保存了Wallpaper的实例,同时也实现了唯一的一个接口attach()。

WallpaperManagerService会在WallpaperConnection.onServiceConnected方法中收到回调,然后会进行以下三步:

  1. 将WallpaperService传回的IWallpaperService接口保存为mService
  2. 绑定壁纸服务,调用attachServiceLocked方法,这个方法会调用IWallpaperService.attach方法来传递壁纸服务创建窗口的信息
  3. saveSettingLocked,保存壁纸运行状态到文件系统中

其中IWallpaperService.attach方法中的参数意义如下:

  1. conn:WallpaperConnection。继承自IWallpaperConnection,只提供了两个接口定义:attachEngine和engineShown。

    Q:为什么有了WallpaperManager这个对外界的标准接口还需要WallpaperConnection?

    A:attachEngine和engineShown只有WallpaperService才用得到,并且是与WallpaperManagerService之间底层且私密的交流,不适合放在通用接口之中。WallpaperService只是一个承载壁纸运行的容器,真正实现壁纸的核心为Engine类,当WallpaperService创建完Engine之后,就会通过attachEngine方法将Engine对象引用交给WallpaperManagerService。

  2. conn.Token:向WindowManagerService申请的令牌

  3. WindowManager.LayoutParams.TYPE_WALLPAPER:指明需要添加TYPE_WALLPAPER类型的窗口。另一种情况是壁纸预览的时候,会使用TYPE_APPLICATION_MEDIA_OVERLAY类型创建窗口,此时壁纸服务创建的窗口将会以子窗口的形式衬在LivePicker的窗口之下。

  4. isPreview:实际作为壁纸的时候是false,壁纸预览的时候是true

c、创建Engine

调用IWallpaperService.attach是WallpaperManagerService与WallpaperService的第一次接触。该方法会创建IWallpaperEngineWrapper,其继承自IWallpaperEngine.stub,支持跨进程调用。在其中会创建并封装Engine的实例。

IWallpaperEngineWrapper在attach方法中只创建了对象,但是没有将其赋给任何变量。这个实例对象的保持依靠的是其内部的HandlerCaller以及HandlerCaller中的Handler的外部引用持有来实现的。Handler是HandlerCaller的内部类,其中包含了HandlerCaller的隐式引用,而HandlerCaller又持有IWallpaperEngineWrapper的引用,所以在内部Handler处理DO_ATTACH消息之前,IWallpaperEngineWrapper不会被垃圾回收器回收。
Android壁纸机制(Android Q与老版本对比)_第2张图片
在IWallpaperEngineWrapper中创建的HandlerCaller是Handler的一个封装,比Handler额外提供一个executeOrSendMessage方法,在HandlerCaller所在线程执行该方法的时候会使处理函数马上执行,在其他线程中与Handler.sendMessage一样。

这个HandlerCaller是一个重要的线程调度器,所有与壁纸相关的操作都会以消息的形式发送给mCaller,然后在IWallpaperEngineWrapper的executeMessage方法中处理,这些操作也就转移到了mCaller所在线程。默认情况下mCaller运行在主线程中。

然后就是处理DO_ATTACH消息,会进行如下步骤:

  1. mConnection.attachEngine:把IWallpaperEngineWrapper实例传递给WallpaperConnnection,这之后就不用担心实例被回收了。

  2. 通过onCreateEngine创建一个Engine。这个方法由开发者自己实现

  3. 将新建的Engine添加到WallpaperService.mActiveEngine中。

    当使用壁纸预览的时候,WallpaperService仍然只有一个,但是其内部会变成两个Engine。这也说明了WallpaperService仅仅是提供壁纸运行的场所,真正壁纸的实现是Engine。

Engine创建完成之后会通过Engine.attach来初始化Engine,步骤如下:

  1. 设置必要的信息,包括:
    • mSurfaceHolder:BaseSurfaceHolder类型的内部类实例,可以通过它来定制需要的Surface类型
    • 获取WindowSession,用于与WMS通信
    • mWindow.setSession(mSession):用于接受WMS的回调
    • 设置监听屏幕状态,保证屏幕关闭之后停止动画渲染节省电量
  2. 调用Engine.onCreate,重写的子类一般需要重写该方法来修改mSurfaceHolder的属性。此时还没有创建窗口,修改的属性会在窗口创建时生效。
  3. updateSurface:根据SurfaceHolder的属性创建窗口以及Surface,并进行壁纸的第一次绘制。

2、在动态壁纸创建中使用到的Binder通信对象和数据结构

使用到的Binder对象:

  1. IWallpaperService:实现在壁纸服务进程中,唯一提供的方法为attach,用于在壁纸服务启动之后接受窗口创建的信息,完成壁纸初始化工作。
  2. WallpaperConnection:实现在WallpaperManagerService中,通过IWallpaperService.attach传递给了IWallpaperEngineWrapper。WallpaperService通过attachEngine将IWallpaperEngineWrapper实例传递给WallpaperManagerService,通过engineShown将壁纸显示完成情况传递给WallpaperManangerService
  3. IWallpaperEngineWrapper:实现在壁纸进程中,是WallpaperManagerService对Engine进行请求或者设置的唯一接口。

Q:WallpaperService和WallpaperManagerService之间除了IWallpaperService之外为什么要多加一个IWallpaperEngineWrapper?

A:首先,从WallpaperManagerService的角度来看,IWallpaperService代表的是WallpaperService,是壁纸实现的容器。而IWallpaperEngineWrapper代表是Engine,是壁纸的真正实现。

其次,WallpaperService中可以运行多个Engine实例,但是WallpaperManagerService或者LivePicker关注的只是一个确定的Engine实例,而不是WallpaperService中的所有Engine,从这个角度来说,也是简化实现的一种方式。

使用到的数据结构:

  1. WallpaperInfo:存储壁纸的开发者、缩略图以及描述信息
  2. WallpaperConnection:WallpaperService和WallpaperManagerService进行通信的渠道、保存壁纸服务的重要运行时信息。
  3. WallpaperData:一个壁纸在WallpaperManagerService中可能用到的所有信息:ComponentName、WallpaperConnection、壁纸的启动时间。

3、理解Engine.updateSurface

该方法中更新surface的条件有:

  • 窗口尚未创建
  • Surface尚未创建
  • mSurfaceHolder中的像素格式发生了变化
  • 尺寸发生了变化
  • Surface的类型发生了变化
  • 窗口的flag发生了变化

这些条件只要有一个发生了变化就要更新Surface,SurfaceHolder允许修改的属性有:

  • 尺寸
    1. setFixedSize(int w,int h):只允许ImageWallpaper使用
    2. setSizeFromayout()——使用默认的宽高,恢复到MATCH_PARENT
  • 像素格式(创建之后就不再修改)
  • 内存类型(创建之后就不再修改)
  • 是否防止屏幕休眠(对壁纸不适用,修改会报异常)

完成Surface更新的原理:

  • 倘若窗口尚未创建,则通过WMS.addWondow完成窗口的创建
  • 通过WMS.relayoutWindow对窗口进行重新布局。如果窗口目前还没有一块可用的Surface(刚刚完成创建工作),Engine将会拥有一块Surface。而存储在LayoutParams中与Surface或窗口相关的参数都会被WMS接纳并修改Surface的属性(尺寸、像素格式、内存类型以及窗口的flags)。但是最终的决定权还是在WMS,updateSurface会将WMS的布局结果强制设置给SurfaceHolder。

完成Surface更新以后,updateSurface会触发SurfaceHolder的回调通知所有的SurfaceHolder的使用者(开发者自定义的Engine)。这些回调包括:

  • onSurfaceCreated
  • onSurfaceChanged
  • onSurfaceRedrawNeeded

另外还有一个在壁纸销毁时触发的回调:onSurfaceDestroyed。这些回调给开发者提供了Surface的生命周期。

对Engine的回调做个总结:

  1. onCreate:发生在Engine.attach方法中,表示Engine生命周期的开始
  2. onDestroy:发生在Engine.detach方法中,表示Engine生命周期的结束
  3. onSurfaceCreated / onSurfaceChanged / onSurfaceDestroyed / onSurfaceRedraw-Needed:发生在updateSurface和Engine.detach中,向开发者描述了Surface的生命周期
  4. onVisibilityChanged:通知当前壁纸的可见性发生了变化。这里的可见性指的是是否以壁纸窗口作为当前窗口的背景。因为使窗口从不可见到可见的花销巨大,会经历窗口的重新布局、Surface的创建、等待窗口完成第一次绘制等等,所以Android索性使窗口始终可见,只是在不需要显示壁纸的时候将它放在所有窗口的底部,使用户看不到(调用Surface.hide将壁纸的Surface隐藏,需要的时候调用Surface.show,这样Surface的内容并没有被清除,不需要重绘Engine对象),但是如果壁纸动画还在运行的话,仍然会消耗资源,所以当onVisibilityChanged报告壁纸可见性为false的时候,Engine必须停止一切消耗资源的操作。
  5. onTouchEvent:用于处理用户的触摸事件。开发者可以通过Engine.setTouchEventEnabled方法在壁纸窗口的flags中增加或者删除FLAG_NOT_TOUCHABLE标记,从而设置壁纸窗口是否可以接受用户的输入事件。InputDispatcher查找触摸事件的派发目标是根据窗口的Z序从上向下遍历查找触摸事件落在其上的第一个窗口。但是InputDispatcher为壁纸窗口做了特殊化处理,InputDispatcher::findTouchedWindowTargetLocked找到触摸目标之后会判断该窗口是否要显示壁纸,如果要的话就把触摸事件同时发送给两个窗口。
  6. onOffsetChanged:用于通知Engine对象需要对壁纸所绘制的内容进行偏移,发生于WallpaperManager.setWallpaperOffsets()。当要求显示壁纸的窗口内容发生滚动的时候,可能希望壁纸的内容也随之偏移。这个偏移量的表现形式没有限定,可以自由发挥。
  7. onCommand:用于处理WallpaperManager.sendWallpaperCommand发送的命令,同样可以自定义命令,提供了一种通信方式。
  8. onDesiredSizeChanged:显示壁纸的窗口调用WallpaperManager.suggestDediredDimensions指明它所期望壁纸的尺寸大小,可以配合setWallpaperOffsets来实现壁纸与内容的同步滚动。

4、壁纸的销毁

壁纸销毁的工作就是把壁纸创建的过程反过来进行一遍:

  1. 执行IWallpaperEngine.destroy,会触发onSurfaceDestroy回调通知开发者销毁壁纸运行期间所使用的资源,并移除壁纸窗口
  2. unbindService:终止WallpaperManagerService对壁纸服务的绑定
  3. 注销用来添加壁纸窗口的令牌

在WallpaperService中则是:

  1. 从mActiveEngines中将当前Engine删除
  2. 调用Engine.detach方法
    1. reportSurfaceDestroyed:触发onSurfaceDestroyed回调
    2. onDestroy:触发onDestroy回调

三、静态壁纸——ImageWallpaper

所谓静态壁纸,就是SystemUI中的一个名为ImageWallpaper的特殊动态壁纸而已,实现的架构就是动态壁纸的架构,只不过其内容是一张静态的图片而已。之所以与普通的动态壁纸区分,是因为Android为ImageWallpaper提供了很多方便的API,使得可以方便地将静态图片设置为壁纸。

静态壁纸的设置

旧版本API中(29以前)):

静态壁纸的服务位于SystemUI中,ImageWallpaper继承自WallpaperService,在其onCreateEngine方法中创建了一个继承自Engine的WallpaperEngine。

WallpaperEngine中有一个Bitmap类型的成员变量mBackground,这就是作为壁纸的位图。对静态壁纸进行重绘的时候,就会调用drawFrameLocked方法将mBackground绘制到壁纸窗口的Surface上。
Android壁纸机制(Android Q与老版本对比)_第3张图片

静态壁纸的设置不像动态壁纸那样需要签名级系统权限,只需要有android.permission.SET_WALLPAPER权限就可以通过WallpaperManager的相关接口设置静态壁纸。WallpaperManager提供了setBitmap、setResource以及setStream三个方法进行静态壁纸的设置。以setBitmap为例,会进行如下步骤:

  1. 通过WallpaperManagerService.setWallpaper方法获取一个文件描述符,这个文件的位置为/data/(secure)/system//wallpaper文件夹。
  2. 将Bitmap写入这个文件描述符所指向的文件中。

通过WallpaperManager设置完壁纸的文件位置之后,ImageWallpaper怎么知道壁纸文件路径发生了变化呢?Android提供了一个WallpaperObserver类(WallpaperManagerService.WallpaperObserver),该类继承自FileObserver,这个类是Android提供的一个工具类,用于监听文件系统中发生的文件创建、删除与修改事件。FileObserver可以监听一个文件夹或者是一个文件,当它发生变化的时候,onEvent回调就会根据参数获取到发生这些动作的文件路径。在壁纸监听中主要是CLOSE_WRITE事件,除此之外还有DELETE、DELETE_SELF事件,这两个事件的原因是因为位图被保存在文件系统中,保护非常弱,需要处理其被删除的情况。

在WallpaperObserver的onEvent方法中最终调用的是bindWallpaperComponentLocked方法,这也就意味着在壁纸文件位置重新设置之后,不会通知原有的DrawableEngine进行更新和重绘,而是销毁原有的DrawableEngine,创建一个新的进行壁纸绘制。

DrawableEngine设置壁纸的步骤如下:

  1. 调用forgetLoadedWallpaper,将mDefaultWallpaper和mWallpaper设置成null。这两个变量一个存储系统默认的壁纸,另一个存储现有的设置壁纸。
  2. 调用WallpaperManager.getBitmap获取作为壁纸的位图,在这个方法里又会调用Globals.peekWallpaperBitmap方法。在其中有两套保证静态壁纸稳定运行的机制:缓存以及备用方案。它会尽量返回已经加载的位图,如果失败就会返回默认的位图。
  3. 获取被设置为壁纸的位图使用的是Globals.getCurrentWallpaperLocked方法。这个方法会去前面WallpaperManager设置的文件位置加载位图并设置为壁纸。

API29中:

静态壁纸的服务仍然位于SytemUI中,ImageWallpaper继承自WallpaperService,在其onCreateEngine方法中创建的Engine换成了GLEngine:

class GLEngine extends Engine implements GLWallpaperRenderer.SurfaceProxy, StateListener 

这个类继承自Engine类,同时扩展了GLWallpaperRenderer类的SurfaceProxy接口以及StateListner接口。

GLWallpaperRenderer类是一个渲染器,负责发送OpenGl调用来渲染一个帧。而它的SurfaceProxy接口则是拥有SurfaceHolder的代理。通过这个接口能够将OpenGl的渲染帧发送到Surface上。

StateListener接口位于StatusBarStateController中,用于接受状态栏状态更新以及Dozing 的状态改变。

因为这个类的改变,现在静态壁纸的创建流程变成了:
Android壁纸机制(Android Q与老版本对比)_第4张图片
ImageWallpaperRenderer类继承了GLWallpaperRenderer类,在它的构造方法中调用loadBitmap方法进行了Bitmap的加载。

在这个方法中,首先就是调用了WallpaperManager的getBitmap方法去获取Bitmap。getBitmap中又调用了Wallpaper的静态内部类Globals的peekWallpaperBitmap方法,其同旧版本一样提供了两套保证尽态比值稳定运行的机制:缓存和备用方案。如果缓存为空的话就调用getCurrentWallpaperLocked方法来从文件中获取Bitmap。

从文件中获取Bitmap的流程相对于老版本没有改变太多,只是不再直接在WallpaperManagerService.getWallpaper方法中调用getWallpaperDir来获取文件路径以及句柄了,而是将这个获取过程移到了WallpaperData的初始化中。而Wallpaper对象又会在loadSetttingLocked方法中被创建。在这个方法中会做这么几件事:

  • 从mWallpaperMap中尝试获取Wallpaper,如果为空开始WallpaperData的初始化(一般刚开机这个肯定为空)
  • 调用migrateFromOld方法对WALLPAPER_CROP,WALLPAPER进行赋值,这两个参数用来指定WallpaperData初始化时的inputFileName,cropFileName。
  • 获取完wallpaper之后放入mWallpaperMap中,如果cropWallpaper不存在,就会调用generateCrop方法对原始的Wallpaper进行裁剪,如果连wallpaper文件都不存在就会采用调用默认壁纸的逻辑
  • 获取完Wallpaper,且确认存在,就会读取xml文件中对壁纸的设定,设置wallpaper中的各种参数

loadingSettingLocked方法会在很多地方被调用,但是这里只关注三个地方(我认为。。):

  • WalpaperManagerService的initialize方法,在WMS进行初始化的时候对壁纸的文件内容进行填充是有必要的
  • getWallpaperSafeLocked方法。有的时候可能WallpaperMap并不会拥有用户的数据——例如在用户切换的时候,在这个时候用户切换的obverser可能还没有收到用户切换的事件。当我们不在意这些顺序只想要更新数据的时候就可以调用这个安全的方法来更新。更新后的数据将会在用户切换观察者渐渐生效的时候被应用。
  • WallpaperManagerService.WallpaperObserver的onEvent方法。这个类和方法的作用和老版本中一样。

接收到返回的文件句柄之后,就会在Globals类中使用BitmapFactory.decodeFileDescriptor进行解码生成Bitmap文件然后一路返回到ImageWallpaperRender中。然后就会调用forgetLoadedWallpaper方法删除掉所有上一张加载的壁纸的内部引用。调用这个方法对于那些只想短暂拥有壁纸,想要减少内存消耗的应用是有用的。但是在调用这个方法之后想要调用这张壁纸又要从硬盘中重新读取。

都结束之后就将Surface的大小调整为Bitmap的宽高。至此Bitmap的获取就结束了。

获取完Bitmap,之后的壁纸渲染工作GLEngine都会交给GLWallpaperRenderer的各种方法,由其发送OpenGL调用来渲染,并通过SurfaceProxy来对SurfaceHolder进行处理,来将壁纸渲染到Surface上。

设置壁纸的过程和老版本基本一致。

四、WMS对壁纸窗口的特殊处理

WMS对于壁纸窗口的特殊处理体现在三个方面:

  • 壁纸窗口的Z序——衬在FALG_SHOW_WALLPAPER标记的窗口之下
  • 壁纸窗口的可见性——动态壁纸资源相关
  • 壁纸窗口的动画——跟随标记窗口一起动画

1、壁纸窗口的Z序

壁纸的存在意义就是给其他窗口提供背景,一个窗口希望壁纸作为其壁纸的时候,就可以将FLAG_SHOW_WALLPAPER加入到它的flag中。当WMS检测到这个标记,就会把壁纸窗口衬在这个窗口之下。所以壁纸窗口的Z序与声明FLAG_SHOW_WALLPAPER标记的窗口息息相关。

声明FLAG_SHOW_WALLPAPER的窗口在WMS中被称为壁纸目标,存储在名为mWallpaperTarget的WindowState类型成员中。确定壁纸窗口Z序的核心工作就是寻找这个目标窗口。

旧版本API中:

这个目标窗口存在三种情况:

  1. 当前系统中只有一个窗口声明FLAG_SHOW_WALLPAPER标记,且是用户可见的,那么这个窗口就是目标窗口
  2. 当前系统中有多个窗口声明标记,此时Z序最高的窗口为目标窗口
  3. 当前系统有多个窗口声明标记,同时它们还在进行窗口动画,此时要做相对复杂的选择策略
    Android壁纸机制(Android Q与老版本对比)_第5张图片

确定壁纸目标的流程的原则是找到窗口列表中第一个声明FLAG_SHOW_WALLPAPER、并且对用户可见的窗口。如果候选窗口是旧的壁纸目标,且在动画过程中,那么就继续向下查找可能成为壁纸目标的窗口。

在这个过程中,候选的壁纸目标将会被保存到foundW中,其在窗口列表中的索引将会被保存到foundI中,这两个变量用于确定壁纸窗口的插入位置。

在下一个阶段中,如果新旧目标都在动画过程中,那么就要设置mLowerWallpaperTarget(Z序较小)和mUpperWallpaperTarget(Z序较大)。这个阶段主要是为了处理新旧目标都在动画过程中这种情况的。这个时候foundW和foundI会被指向Lower的目标(因为壁纸窗口最终是被衬在目标窗口的下面,肯定插入位置要在新的目标的下面,最终壁纸窗口会在foundI指向的目标的下面)。而当新的窗口目标在旧的窗口目标上面的时候(图中棕色块),真正的壁纸目标mWallpaperTarget可能会和壁纸窗口的实际位置发生分离(mWallpaperTarget指向Upper,foundI指向Lower),最终壁纸窗口的插入位置和壁纸目标之间会隔着一层Lower。不过这种情况很少,一般Lower和Upper都是空的,mWallpaperTarget会被设置为foundW。

Lower和Upper不为空的时候表示此时同时有两个用户可见的壁纸目标。但是壁纸窗口只有一个,此时WMS的策略为:

  • 壁纸窗口的位置和可见性由Lower确定
  • 壁纸的offset由mWallpaperTarget确定(有可能是Lower【绿色块】,也有可能是Upper【棕色块】)
  • 壁纸的动画谁说了都不算,此时壁纸处于静止状态

然后就是确定壁纸窗口在窗口列表中的位置。这个过程有几条原则:

  • Activity在执行动画的时候会对自己的Layer进行调整,壁纸窗口作为目标窗口的附属,也要对自己的layer进行调整。但是这只在Lower为null的时候才会执行,因为当不为null的时候,有两个目标都在进行动画,壁纸窗口无法知道使用哪一个目标的layer进行调整,所以这个时候壁纸窗口的layer不会动
  • PhoneWindowManager设置了maxLayer,这是壁纸所能拥有的layer上限(不包含),这个上限与状态栏的layer相同,所以壁纸是不可能覆盖状态栏的
  • 确保壁纸位于layer上限、目标窗口、目标窗口的父窗口以及STARTING窗口之下(这些窗口都紧邻在一起)
  • 如果没有哪个窗口声明FLAG_SHOW_WALLPAPER标记,那么壁纸窗口就会被设置在壁纸列表的底部

确定完壁纸窗口的位置之后,就要将壁纸窗口移动到指定的位置。一般来说系统中只会存在一个壁纸窗口,但是在切换壁纸的时候,WallpaperManagerService.bindWallpaperComponentLocked的实现会首先启动新壁纸然后再销毁旧壁纸。这样,在一个较短的时间里,系统中可能存在两个不同的壁纸窗口。所以在调整壁纸窗口位置的时候会遍历WallpaperToken中的所有壁纸窗口,然后先将所有壁纸窗口从窗口列表中剥离,然后再按照顺序一个一个插入到foundI所指向的位置上(foundI的值随着插入递减),然后在销毁旧壁纸之后,新壁纸必然会在正确的位置上。

新版本API中寻找mWallpaperTarget的流程如下图:
Android壁纸机制(Android Q与老版本对比)_第6张图片
首先调用的是DisplayContent的applySurfaceTransaction方法,在这个方法中会调用WallpaperController的adjustWallpaperWindows方法。

在这个方法中首先会判断当前窗口是不是处于FREEFORM模式,如果是的话,就会将壁纸设置为TopWallpaper(也就是将壁纸窗口本身设置为自己的Target)

Freeform Windows:允许应用在可以改变大小的窗口中运行,在Android Q中可以在开发者模式中打开(之前需要ADB或者第三方应用来完成)

然后就会调用DisplayContent的forWindows函数对所有窗口执行mFindWallpaperTargetFunction回调。在这个回调中会执行以下八个条件来寻找目标窗口:

  • 当前的窗口类型为TYPE_WALLPAPER,并且当前没有设置topWallpaper或者设置了重置TopWallpaper选项,将当前窗口设置为TopWallpaper,然后返回false,继续寻找下一个合适窗口。
  • 当前窗口隐藏且不在动画过程中,返回false继续寻找。
  • 当前窗口将会被替换,而且当前的目标窗口属性为空,使用TopWallpaper作为目标窗口,保持这个窗口直到新窗口完全显示出来,这样能避免屏幕闪烁。
  • 执行锁屏时的壁纸逻辑
  • 执行最近任务动画设置壁纸的逻辑
  • 窗口声明了FLAG_SHOW_WINDOW,并且显示在屏幕上(当前处于Visible状态或者是在变为不可见之前执行动画)+是当前的Target+已经绘制完成——设置为Target(使用FindTargetResult类保存)。
  • 当前目标在执行动画,继续往后寻找可能的目标。

寻找到合适的WallpaperTarget之后(或者没有找到),就会调用updateWallpaperWindowTarget来更新WallpaperTarget,在这里存在两种情况:

  • 找到的Target等于现在的Target || mPrevTarget不为空,且找到的Target等于mPrevTarget
    • mPrev为空(从第一个条件进来):返回
    • mPrev不在动画中(从第二个条件进来):将mPrev置为空,并更新目标壁纸窗口为找到的Target
  • 新旧两个目标都在动画中
    • 新目标requestHide,而旧目标没有——使用旧目标作为Target
    • 都申请隐藏——使用新的
    • 都申请隐藏或者不隐藏,这种情况就和应用的打开/关闭列表有关:使变换逻辑更好地确定正在打开/关闭的应用的壁纸状态
      • 打开列表中有旧的没有新的——使用旧的
      • 关闭列表中有旧的——使用旧的

这其中使用到了两个WindowController的变量:

  1. mWallpaperTarget:如果不为空,这就是与壁纸相关的当前可见窗口
  2. mPrevWallpaperTarget:如果这变量不为空的话,我们当前就处于从一个壁纸目标动画转换到另一个壁纸目标的过程中,而这个变量保存的就是前一个壁纸目标。

2、壁纸的可见性

旧版本的API中:

首先,壁纸可见的第一个条件就是存在一个壁纸目标(不存在的话就会将壁纸窗口放置到窗口列表的底部)。然后就会调用WMS.isWallpaperVisible来确定壁纸窗口是否可见,这里有三个条件,满足其中一个壁纸窗口就可见:

  1. Lower或者Upper不为null,这表明目前存在新旧两个壁纸目标正在执行动画
  2. wallPaperTarget(不是mWallpaperTarget,是foundW)没有被其他全屏窗口遮挡,即窗口部分或者全部可见
  3. wallpaperTarget属于一个Activity,而这个Activity正在执行动画。Activity执行动画的时候会有layer的调整,所以即使这个时候wallpaperTarget被全屏窗口遮挡,随着layer的调整,壁纸窗口也有可能显示在其他窗口之上,所以此时也要显示壁纸

isWallpaperVisible的结果会保存在壁纸窗口的WindowState.mWallpaperVisible中。这个设置有如下作用:

在动画系统中,WindowStateAnimator的prepareSurfaceLocked方法会检查这个变量的值,如果为false,就会调用WindowStateAnimator.hide,进而调用Surface.hide将壁纸隐藏。相反,会调用WindowStateAnimator.showSurfaceRobustlyLocked,进而调用Surface.show来使壁纸窗口重新可见

然后通过IWindow.dispatchAppVisibility方法将可见性传递给WallpaperService下的Engine对象。其中的mWindow成员将其解释为onVisibilityChanged,Engine根据这一回调重启或者终止画面的绘制。

所以壁纸的可见性有两层意义:

  • WallpaperService中的Engine对象是否进行壁纸的绘制工作
  • WMS中壁纸窗口的Surface隐藏还是可见

不同于常规窗口可见性变化时的创建或者销毁,壁纸窗口的可见性只会使Surface显示或者隐藏。这样壁纸窗口的可见性变化效率会非常高,但是其Surface的内存占用会一直都在。

新版本API中:

首先,壁纸可见的第一个条件就是存在一个壁纸目标(不存在的话就会将壁纸窗口放置到窗口列表的底部)。然后就会调用WallpaperController.isWallpaperVisible来确定壁纸窗口是否可见,这里有两个条件,满足其中一个壁纸窗口就可见:

  • WallpaperTarget不为空且满足以下三个条件的任意一个:
    • 壁纸目标没有被遮挡
    • 与最近任务一起处于动画过程中
    • 壁纸窗口目标是一个App窗口并且这个窗口自己在执行动画
  • mPrevWallpaperTarget不为空

isWallpaperVisible的结果保存的位置和作用和旧版本API一样,见上方。

同时在adjustWallpaperWindows方法中关注的壁纸窗口的可见性的点在于:窗口目前对于SurfaceFlinger是可见的,但是它对于用户而言是否是可见的呢?所以围绕窗口可见性与否对于窗口的偏移和位置做了调整:
Android壁纸机制(Android Q与老版本对比)_第7张图片
首先在adjustWallpaperWindows里面设置留个参数:

  • mLastWallpaperX
  • mLastWallpaperXStep
  • mLastWallpaperY
  • mLastWallpaperYStep
  • mLastWallpaperDisplayOffsetX
  • mLastWallpaperDisplayOffsetY

这些参数会在之后的updateWallpaperOffset中计算出默认的壁纸偏移,然后调用WindowStateAniamtor的setWallpaperOffset设置。在这里x方向上的偏移会判断一次是否是RTL,使得这个行为适配大多数的桌面:

我们常用的习惯,称之为 LTR(Left-To-Right),其意为我们的阅读和书写习惯,是从左向右延伸的。而 RTL(Right-To-Left) 则正好相反,它的阅读和使用的习惯都是从右向左,常见使用 RTL 习惯的语言有阿拉伯语、希伯来语等。

设置完这些参数之后就会调用updateWallpaperTokens方法来更新每个壁纸窗口的WallpaperWindowToken。在这里首先调用的是WallpaperWindowToken中的updateWallpaperWindows方法。在这个方法里会做这些事情:

  • 首先对窗口的可见性进行一次判断,如果窗口可见就取消窗口的隐藏。然后设置该窗口的DisplayContent为LeyoutNeeded,这样就可以对窗口进行重新布局,保证壁纸窗口为正确的大小。
  • 然后从DisplayInfo中取出窗口的宽高信息,对于WindowContainer中的每一个窗口调用WallpaperController的updateWallpaperOffset方法来设置窗口的偏移。
  • 最后调用WindowState的dispatchWallpaperVisibility方法将窗口的当前可见性状态传递给客户端知道。随后的处理和旧版本一致。

处理完窗口的可见性之后就会调用DisplayContent的assignWindowLayrers方法。在这里传递进去的参数是false,所以在这里将会禁用窗口的布局功能。在这里主要是进行两项工作:

  1. 调用assignChildLayers为子窗口调整Z-Order。在这个函数中会传入getPendingTransaction作为参数,将Layer的变化都保存到mPendingTransation中。其中会调整四种类型窗口的Layers:

    1. mBelowAppWindowsContainer:NonAppWindowContainers类型,保存了所有应该显示到App类窗口的下面的非App类的窗口。layer设置为0
    2. mTackStackContainer:保存了所有与App(Activities)相关的Window。layer设置为1
    3. mAboveAppWindowContainer:NonAppWindowContainer类型,保存了所有应该显示到App类窗口的上面的非App类的窗口,layer设置为2
    4. mImeWindowContainers:NonAppWindowContainer类型,包含了所有IME window Containers。IME窗口的Z-Order依赖于IME目标,所以这个Containers的作用是跟踪所有的IME WindowContainers,以便在需要的时候可以一起移动他们。这里的类型是WindowContainer的子类,去除了屏幕放大的功能,因为IME从来都不会放大。对于IME窗口的处理分为四种情况:
      1. 不处于分屏模式的时候,处理IME非常容易:使IME超过所有目标,这样Containers中的所有子窗口都会在上方
      2. 分屏模式下需要将输入法放在分隔栏之上,而应用程序应该放在分割栏之下
      3. 在IME窗口目标处于动画过程的时候,它的Z-Order有可能与WindowContainer的Z-Order不相等,所以很难确保拥有正确的IME目标,所以在这里会将其放在应用程序Layer智商来保证IME会在所有Transition之上
      4. 在我们没有IME目标的时候,将会将其放到AboveAppWindowContaners中

    处理完这些子窗口的Layer之后就要处理他们的子窗口的layer。在这里是为上方的Containers调用assignChildLayers方法,传入的参数都有Transaction,但是mAboveAppWindowContainers中还要考虑是否要传入mImeWindowContainers:

    mAboveAppWindowContainers.assignChildLayers(t,needAssignIme == true ? mImeWindowsContainers : null)
    
  2. 调用scheduleAimation,这样就会开启动画系统,最终会调用到prepareSurfaces,其中会处理mPendingTransaction中积累的Layer变化的事件。这样就会将Z-Order的变化与Surface的显示与隐藏同步起来。

3、壁纸窗口的动画

API29以前:

默认情况下,目标窗口发生动画的时候,壁纸窗口也会随着目标窗口产生同步的动画。效果实现在WindowStateAnimator.computeShownFrameLocked中。 这个方法有如下几个条件:

  • LowerTarget为null,因为不为null的时候表示有两个壁纸目标在进行动画,这种情况下壁纸窗口无法确定使用哪一个目标的动画变化,所以壁纸窗口将保持不动
  • 设置attachTransformation为壁纸目标窗口的动画变换,其前提是目标窗口动画的getDetachedWallpaper返回false。这表示壁纸窗口在动画过程中扮演目标窗口子窗口的作用。
  • 设置appTransformation为目标窗口所属Activity的动画变换,前提是Activity的getDetachedWallpaper返回为false

默认情况下壁纸会随着目标窗口进行动画,除非目标窗口的Animation中设置了DetachWallpaper,或者新旧两个目标窗口都在进行动画。

之后就会将attachTransformation和appTransformation集成到tmpMatrix中,并由此计算出Surface最终的透明度、位置以及DsDx、DsDy、DtDx以及DtDy。

能够作为其他窗口背景的除了壁纸还有另外一种特殊的背景——Animation.setBackgroundColor设置一个背景色,目前Window Animator只会使用背景色的透明度分量,其他RGB会被忽略。但是如果指定了背景色的窗口是mWallpaperTarget、mLowerWallpaperTarget或者mUpperWallpaperTarget的话,这个Surface就会将壁纸窗口给遮挡起来。所以在WindowAnimator.updateWallpaperLocked中会进行检测,将Surface放在壁纸窗口的下面。

Android Q中Animation类的setDetachWallpaper方法已经被废弃,其注释说明:

@deprecated All Window animations are running with detached wallpaper

由此可以看到,现在所有的窗口动画都不会引起壁纸的动画,壁纸窗口不会随着目标窗口的动画而动画了!!

相对的,现在会为所有动画中的窗口设置AnimationBackground,这个过程如下图所示:
Android壁纸机制(Android Q与老版本对比)_第8张图片
和以前的版本相同,在setAnimationBackground中同样只采用了Alpha分量。在Transaction类的setLayer方法中将mAinmationBackgroundSurface的Layer设置为了Interger.MIN_VALUE。这样这个Surface就不会遮挡其他窗口了。

五、壁纸服务总结

Android壁纸使用了系统服务(管理者)+标准Android服务(实现者)的两层架构。当系统希望某些系统级的UI由第三方进行实现与拓展,同时又不希望给予第三方过多的操作窗口的权限时,这两层架构是最佳的选择。系统服务负责提供必要的系统级操作,而标准的Android服务可以在系统服务所规范的框架范围之内实现自由定制。

你可能感兴趣的:(Android,源码机制学习)