第8章 深入理解Android壁纸
本章主要内容:
· 讨论动态壁纸的实现。
· 在动态壁纸的基础上讨论静态壁纸的实现。
· 讨论WMS对壁纸窗口所做的特殊处理。
本章涉及的源代码文件名及位置:
· WallpaperManagerService.java
frameworks/base/services/java/com/android/server/WallpaperManagerService.java
· WallpaperService.java
frameworks/base/core/java/android/service/wallpaper/WallpaperService.java
· ImageWallpaper.java
frameworks/base/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java
· WallpaperManager.java
frameworks/base/core/java/android/app/WallpaperManager.java
· WindowManagerService.java
frameworks/base/services/java/com/android/server/wm/WindowManagerService.java
· WindowStateAnimator.java
frameworks/base/services/java/com/android/server/wm/WindowStateAnimator.java
· WindowAnimator.java
frameworks/base/services/java/com/android/server/wm/WindowAnimator.java
8.1 初识Android壁纸
本章将对壁纸的实现原理进行讨论。在Android中,壁纸分为静态与动态两种。静态壁纸是一张图片,而动态壁纸则以动画为表现形式,或者可以对用户的操作作出反应。这两种形式看似差异很大,其实二者的本质是统一的。它们都以一个Service的形式运行在系统后台,并在一个类型为TYPE_WALLPAPER的窗口上绘制内容。进一步讲,静态壁纸是一种特殊的动态壁纸,它仅在窗口上渲染一张图片,并且不会对用户的操作作出反应。因此本章将首先通过动态壁纸的实现讨论Android壁纸的实现与管理原理,然后在对静态壁纸的实现做介绍。
Android壁纸的实现与管理分为三个层次:
· WallpaperService与Engine。同SystemUI一样,壁纸运行在一个Android服务之中,这个服务的名字叫做WallpaperService。当用户选择了一个壁纸之后,此壁纸所对应的WallpaperService便会启动并开始进行壁纸的绘制工作,因此继承并定制WallpaperService是开发者进行壁纸开发的第一步。Engine是WallpaperService中的一个内部类,实现了壁纸窗口的创建以及Surface的维护工作。另外,Engine提供了可供子类重写的一系列回调,用于通知壁纸开发者关于壁纸的生命周期、Surface状态的变化以及对用户的输入事件进行响应。可以说,Engine类是壁纸实现的核心所在。壁纸开发者需要继承Engine类,并重写其提供的回调以完成壁纸的开发。这一层次的内容主要体现了壁纸的实现原理。
· WallpaperManagerService,这个系统服务用于管理壁纸的运行与切换,并通过WallpaperManager类向外界提供操作壁纸的接口。当通过WallpaperManagaer的接口进行壁纸的切换时,WallpaperManagerService会取消当前壁纸的WallpaperService的绑定,并启动新壁纸的WallpaperService。另外,Engine类进行窗口创建时所使用的窗口令牌也是由WallpaperManagerService提供的。这一层次主要体现了Android对壁纸的管理方式。
· WindowManagerService,用于计算壁纸窗口的Z序、可见性以及为壁纸应用窗口动画。壁纸窗口(TYPE_WALLPAPER)的Z序计算不同于其他类型的窗口。其他窗口依照其类型会有固定的mBaseLayer以及mSubLayer,并结合它们所属的Activity的顺序或创建顺序进行Z序的计算,因此这些窗口的Z序相对固定。而壁纸窗口则不然,它的Z序会根据FLAG_SHOW_WALLPAPER标记在其它窗口的LayoutParams.flags中的存在情况而不断地被调整。这一层次主要体现了Android对壁纸窗口的管理方式。
本章将通过对动态壁纸切换的过程进行分析揭示WallpaperService、Engine以及WallpaperManagerService三者的实现原理以及协作情况。静态壁纸作为动态壁纸的一种特殊情况,将会在完成动态壁纸的学习之后于8.3节进行讨论。而WindowManagerService对壁纸窗口的处理将在8.4节进行介绍。
8.2 深入理解动态壁纸
8.2.1 启动动态壁纸的方法
启动动态壁纸可以通过调用WallpaperManager.getIWallpaperManager().setWallpaperComponent()方法完成。它接受一个ComponentName类型的参数,用于将希望启动的壁纸的WallpaperService的ComponentName告知WallpaperManagerService。WallpaperManager.getIWallpaperManager()方法返回的是WallpaperManagerService的Bp端。因此setWallpaperComponent()方法的实现位于WallpaperManagerService之中。参考其实现:
[WallpaperManagerService.java-->WallpaperManagerService.setWallpaperComponent()]
public void setWallpaperComponent(ComponentNamename) {
// 设置动态壁纸需要调用者拥有一个签名级的系统权限
checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT);
synchronized (mLock) {
/* ① 首先从mWallpaperMap中获取壁纸的运行信息WallpaperData。
WallpaperManagerService支持多用户机制,因此设备上的每一个用户可以设置自己
的壁纸。mWallpaperMap中为每一个用户保存了一个WallpaperData实例,这个实例
中保存了和壁纸运行状态相关的信息。例如WallpaperService的ComponentName,
到WallpaperService的ServiceConnection等。于是当发生用户切换时,
WallpaperManagerService可以从mWallpaperMap中获取新用户的WallpaperData,
并通过保存在其中的ComponentName重新启动该用户所设置的壁纸。因此,
当通过setWallpaperComponent()设置新壁纸时,需要获取当前用户的WallpaperData,
并在随后更新其内容使之保存新壁纸的信息 */
intuserId = UserHandle.getCallingUserId();
WallpaperData wallpaper = mWallpaperMap.get(userId);
......
final long ident = Binder.clearCallingIdentity();
try{
......
// ② 启动新壁纸的WallpaperService
bindWallpaperComponentLocked(name, false, true, wallpaper, null);
}finally {
Binder.restoreCallingIdentity(ident);
}
}
}
注意 WallpaperManager.getIWallpaperManager()并没有作为SDK的一部分提供给开发者。因此第三方应用程序是无法进行动态壁纸的设置的。
8.2.2 壁纸服务的启动原理
(1)壁纸服务的验证与启动
bindWallpaperComponentLocked()方法将会启动由ComponentName所指定的WallpaperService,并向WMS申请用于添加壁纸窗口的窗口令牌。不过在此之前,bindWallpaperComponentLocked()会对ComponentName所描述的Service进行一系列的验证,以确保它是一个壁纸服务。而这一系列的验证过程体现了一个Android服务可以被当作壁纸必要的条件。
[WallpaperManagerService.java-->WallpaperManagerService.setWallpaperComponentLocked()]
boolean bindWallpaperComponentLocked(ComponentNamecomponentName, boolean force,
boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) {
......
try {
/* 当componentName为null时表示使用默认壁纸。
这里会将componentName参数改为默认壁纸的componentName */
if(componentName == null) {
/* 首先会尝试从com.android.internal.R.string.default_wallpaper_component
中获取默认壁纸的componentName。这个值的设置位于res/values/config.xml中,
开发者可以通过修改这个值设置默认壁纸*/
String defaultComponent = mContext.getString(
com.android.internal.R.string.default_wallpaper_component);
if (defaultComponent != null) {
componentName = ComponentName.unflattenFromString(defaultComponent);
}
/* 倘若在上述的资源文件中没有指定一个默认壁纸,即default_wallpaper_component的
值被设置为@null),则使用ImageWallpaper代替默认壁纸。ImageWallpaper就是前文
所述的静态壁纸 */
if (componentName == null) {
componentName = IMAGE_WALLPAPER;
}
}
/* 接下来WallpaperMangerService会尝试从PackageManager中尝试获取ComponentName所
指定的Service的描述信息,获取此信息的目的在于确认该Service是一个符合要求的壁纸服务 */
intserviceUserId = wallpaper.userId;
ServiceInfo si = mIPackageManager.getServiceInfo(componentName,
PackageManager.GET_META_DATA |
PackageManager.GET_PERMISSIONS,serviceUserId);
/* ① 第一个检查,要求这个Service必须声明其访问权限为BIND_WALLPAPER。这个签名级的系
统权限这是为了防止壁纸服务被第三方应用程序启动而产生混乱 */
if(!android.Manifest.permission.BIND_WALLPAPER.equals(si.permission)) {
if (fromUser) {
throw new SecurityException(msg);
}
return false;
}
WallpaperInfo wi = null;
/* ② 第二个检查,要求这个Service必须可以用来处理
android.service.wallpaper.WallpaperService这个Action。
WallpaperManagerService会使用这个Action对此服务进行绑定。
其检查方式是从PackageManager中查询所有可以处理
android.service.wallpaper.WallpaperService的服务,然后检查即将启动的服务
是否在PackageManager的查询结果之中 */
Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE);
if(componentName != null && !componentName.equals(IMAGE_WALLPAPER)) {
// 获取所有可以处理android.service.wallpaper.WallpaperService的服务信息
List<ResolveInfo> ris =
mIPackageManager.queryIntentServices(intent,
intent.resolveTypeIfNeeded(mContext.getContentResolver()),
PackageManager.GET_META_DATA, serviceUserId);
/* ③ 第三个检查,要求这个Service必须在其meta-data中提供关于壁纸的描述信息。如果
即将启动的服务位于查询结果之中,便可以确定这是一个壁纸服务。此时会创建一
个WallpaperInfo的实例以解析并存储此壁纸服务的描述信息。壁纸服务的描述信息包含
了壁纸的开发者、缩略图、简单的描述文字以及用于对此壁纸进行参数设置的Activity的
名字等。壁纸开发者可以在AndroidManifest.xml中将一个包含了上述信息的xml文件设
置在名为android.service.wallpaper的meta-data中以提供这些信息 */
for (int i=0; i<ris.size(); i++) {
ServiceInfo rsi = ris.get(i).serviceInfo;
if (rsi.name.equals(si.name) &&
rsi.packageName.equals(si.packageName)){
try {
wi = newWallpaperInfo(mContext, ris.get(i));
} catch (XmlPullParserException e) {......}
break;
}
}
if (wi == null) {
/* wi为null表示即将启动的服务没有位于查询结果之中,或者没有提供必须的meta-data。
此时返回false表示绑定失败 */
return false;
}
}
......
}
......
}
可见WallpaperManagerService要求被启动的目标Service必须满足以下三个条件:
· 该服务必须要以android.permission.BIND_WALLPAPER作为其访问权限。壁纸虽然是一个标准的Android服务,但是通过其他途径(如第三方应用程序)启动壁纸所在的服务是没有意义的。因此Android要求作为壁纸的Service必须使用这个签名级的系统权限进行访问限制,以免被意外的应用程序启动。
· 该服务必须被声明为可以处理android.service.wallpaper.WallpaperService这个Action。WallpaperManagerService会使用这个Action对此服务进行绑定。
· 该服务必须在其AndroidManifest.xml中提供一个名为android.service.wallpaper的meta-data,用于提供动态壁纸的开发者、缩略图与描述文字。
一旦目标服务满足了上述条件,WallpaperManagerService就会着手进行目标服务的启动与绑定。
参考setWallpaperComponentLocked()方法的后续代码:
[WallpaperManagerService.java-->WallpaperManagerService.setWallpaperComponentLocked()]
boolean bindWallpaperComponentLocked(ComponentNamecomponentName, boolean force,
boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) {
...... // 检查服务是否符合要求的代码
/* ① 创建一个WallpaperConnection。它不仅实现了ServiceConnection接口用于监
听和WallpaperService之间的连接状态,同时还实现了IWallpaperConnection.Stub,
也就是说它支持跨进程通信。
在服务绑定成功后的WallpaperConnection.onServiceConnected()方法调用中,
WallpaperConnection的实例会被发送给WallpaperService,使其作为WallpaperService
向WallpaperManagerService进行通信的桥梁 */
WallpaperConnection newConn = new WallpaperConnection(wi, wallpaper);
// 为启动壁纸服务准备Intent
intent.setComponent(componentName);
intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.wallpaper_binding_label);
intent.putExtra(Intent.EXTRA_CLIENT_INTENT,PendingIntent.getActivityAsUser(
mContext, 0,
Intent.createChooser(new Intent(Intent.ACTION_SET_WALLPAPER),
mContext.getText(com.android.internal.R.string.chooser_wallpaper)),
0, null, new UserHandle(serviceUserId)));
/* ② 启动制定的壁纸服务。当服务启动完成后,剩下的启动流程会在
WallpaperConnection.onServiceConnected()中继续 */
if(!mContext.bindService(intent,
newConn,Context.BIND_AUTO_CREATE, serviceUserId)) {
}
// ③ 新的的壁纸服务启动成功后,便通过detachWallpaperLocked()销毁旧有的壁纸服务
if(wallpaper.userId == mCurrentUserId && mLastWallpaper != null) {
detachWallpaperLocked(mLastWallpaper);
}
// ④ 将新的壁纸服务的运行信息保存到WallpaperData中
wallpaper.wallpaperComponent = componentName;
wallpaper.connection = newConn;
/* 设置wallpaper.lastDiedTime。这个成员变量与其说描述壁纸的死亡时间戳,不如说是
描述其启动的时间戳。它用来在壁纸服务意外断开时(即壁纸服务非正常停止)检查此壁纸服务
的存活时间。当存活时间小于一个特定的时长时将会认为这个壁纸的软件质量不可靠
从而选择使用默认壁纸,而不是重启这个壁纸服务 */
wallpaper.lastDiedTime = SystemClock.uptimeMillis();
newConn.mReply = reply;
/* ④ 最后向WMS注册一个WALLPAPER类型的窗口令牌。这个令牌会在onServiceConnected()
之后被传递给WallpaperService用于作为后者添加窗口的通行证 */
try{
if (wallpaper.userId == mCurrentUserId) {
mIWindowManager.addWindowToken(newConn.mToken,
WindowManager.LayoutParams.TYPE_WALLPAPER);
mLastWallpaper = wallpaper;
}
} catch (RemoteException e) {}
} catch(RemoteException e) {}
returntrue;
}
bindWallpaperComponentLocked()主要做了如下几件事情:
· 创建WallpaperConnection。由于实现了ServiceConnection接口,因此它将负责监听WallpaperManagerService与壁纸服务之间的连接状态。另外由于继承了IWallpaperConnection.Stub,因此它具有跨进程通信的能力。在壁纸服务绑定成功后,WallpaperConnection实例会被传递给壁纸服务作为壁纸服务与WallpaperManagerService进行通信的桥梁。
· 启动壁纸服务。通过Context.bindService()方法完成。可见启动壁纸服务与启动一个普通的服务没有什么区别。
· 终止旧有的壁纸服务。
· 将属于当前壁纸的WallpaperConnection实例、componentName机器启动时间戳保存到WallpaperData中。
· 向WMS注册WALLPAPER类型的窗口令牌。这个窗口令牌保存在WallpaperConnection.mToken中,并随着WallpaperConnection的创建而创建。
仅仅将指定的壁纸服务启动起来尚无法使得壁纸得以显示,因为新启动起来的壁纸服务由于没有可用的窗口令牌而导致其无法添加窗口。WallpaperManagerService必须通过某种方法将窗口令牌交给壁纸服务才行。所以壁纸显示的后半部分的流程将在WallpaperConnection.onServiceConnected()回调中继续。同其他服务一样,WallpaperManagerService会在这个回调之中获得一个Binder对象。因此在进行onServiceConnected()方法的讨论之前,必须了解WallpaperManagerService在这个回调中将会得到一个什么样的Binder对象。
现在把分析目标转移到WallpaperService中。和普通服务一样,WallpaperService的启动也会经历onCreate()、onBind()这样的生命周期回调。为了了解WallpaperManagerService可以从onServiceConnected()获取怎样的Binder对象,需要看下WallpaperService.onBind()的实现:
[WallpaperService.java-->WallpaperService.onBind()]
public final IBinder onBind(Intent intent) {
/*onBind()新建了一个IWallpaperServiceWrapper实例,并将
其返回给WallpaperManagerService */
return new IWallpaperServiceWrapper(this);
}
IWallpaperServiceWrapper类继承自IWallpaperService.Stub。它保存了WallpaperService的实例,同时也实现了唯一的一个接口attach()。很显然,当这个Binder对象返回给WallpaperManagerService之后,后者定会调用这个唯一的接口attach()以传递显示壁纸所必须的包括窗口令牌在内的一系列的参数。
(2)向壁纸服务传递创建窗口所需的信息
重新回到WallpaperManagerService,当WallpaperService创建了IWallpaperServiceWrapper实例并返回后,WallpaperManagerService将会在WallpaperConnection.onServiceConnected()中收到回调。参考其实现:
[WallpaperManagerService.java-->WallpaperConnection.onServiceConnected()]
public void onServiceConnected(ComponentName name,IBinder service) {
synchronized (mLock) {
if (mWallpaper.connection == this) {
// 更新壁纸的启动时间戳
mWallpaper.lastDiedTime = SystemClock.uptimeMillis();
// ① 将WallpaperService传回的IWallpaperService接口保存为mService
mService = IWallpaperService.Stub.asInterface(service);
/* ② 绑定壁纸服务。attachServiceLocked()会调用IWallpaperService.attach()
方法以将壁纸服务创建窗口所需的信息传递过去 */
attachServiceLocked(this, mWallpaper);
// ③ 保存当前壁纸的运行状态到文件系统中,以便在系统重启或发生用户切换时可以恢复
saveSettingsLocked(mWallpaper);
}
}
}
进一步地,attachServiceLocked()方法会调用IWallpaperService.attach()方法,将创建壁纸窗口所需的信息发送给壁纸服务。
[WallpaperManagerService.java-->WallpaperManagerService.attachServiceCLocked()]
void attachServiceLocked(WallpaperConnection conn,WallpaperData wallpaper) {
try {
/* 调用IWallpaperService的唯一接口attach(),将创建壁纸窗口所需要的参数传递
给WallpaperService */
conn.mService.attach(conn, conn.mToken,
WindowManager.LayoutParams.TYPE_WALLPAPER, false,
wallpaper.width, wallpaper.height);
} catch(RemoteException e) {......}
}
attach()方法的参数很多,它们的意义如下:
· conn即WallpaperConnection,WallpaperService将通过它向WallpaperManagerService进行通信。WallpaperConnection继承自IWallpaperConnection,只提供了两个接口的定义,即attachEngine()以及engineShown()。虽说WallpaperManager是WallpaperManagerService向外界提供的标准接口,但是这里仍然选择使用WallpaperConnection实现这两个接口的原因是由于attachEngine()以及engineShown()是只有WallpaperService才需要用到而且是它与 WallpaperManagerService之间比较底层且私密的交流,将它们的实现放在通用的接口WallpaperManager中显然并不合适。这两个接口中比较重要的当属attachEngine()了。如前文所述,Engine类是实现壁纸的核心所在,而WallpaperService只是一个用于承载壁纸的运行的容器而已。因此相对于WallpaperService,Engine是WallpaperManagerService更加关心的对象。所以当WallpaperService完成了Engine对象的创建之后,就会通过attachEngine()方法将Engine对象的引用交给WallpaperManagerService。
· conn.mToken就是在bindWallpaperComponent()方法中向WMS注册过的窗口令牌。是WallpaperService有权添加壁纸窗口的凭证。
· WindowManager.LayoutParams.TYPE_WALLPAPER指明了WallpaperService需要添加TYPE_WALLPAPER类型的窗口。读者可能会质疑这个参数的意义:壁纸除了是TYPE_WALLPAPER类型以外难道还有其他的可能么?的确在实际的壁纸显示中WallpaperService必然需要使用TYPE_WALLPAPER类型添加窗口。但是有一个例外,即壁纸预览。在LivePicker应用中选择一个动态壁纸时,首先会使得用户对选定的壁纸进行预览。这一预览并不是真的将壁纸设置给了WallpaperManagerService,而是LivePicker应用自行启动了对应的壁纸服务,并要求壁纸服务使用TYPE_APPLICATION_MEDIA_OVERLAY类型创建窗口。这样一来,壁纸服务所创建的窗口将会以子窗口的形式衬在LivePicker的窗口之下,从而实现了动态壁纸的预览。
· false的参数名是isPreview. 用以指示启动壁纸服务的意图。当被实际用作壁纸时取值为false,而作为预览时则为true。仅当LivePicker对壁纸进行预览时才会使用true作为isPreview的取值。壁纸服务可以根据这一参数的取值对自己的行为作出调整。
当WallpaperManagerService向WallpaperService提供了用于创建壁纸窗口的足够的信息之后,WallpaperService便可以开始着手进行Engine对象的创建了。
(3)Engine的创建
调用IWallpaperService.attach()是WallpaperManagerService在壁纸服务启动后第一次与壁纸服务进行联系。参考其实现:
[WallpaperService.java-->IWallpaperServiceWrapper.attach()]
public void attach(IWallpaperConnection conn,IBinder windowToken,
intwindowType, boolean isPreview, int reqWidth, int reqHeight) {
// 使用WallpaperManagerService提供的参数,构造一个IWallpaperEngineWarapper实例
new IWallpaperEngineWrapper(mTarget, conn, windowToken,
windowType, isPreview, reqWidth, reqHeight);
}
顾名思义,在attach()方法中所创建的IWallpaperEngineWrapper将会创建并封装Engine实例。IWallpaperEngineWrapper继承自IWallpaperEngine.Stub,因此它也支持跨Binder调用。在随后的代码分析中可知,它将会被传递给WallpaperManagerService,作为WallpaperManagerService与Engine进行通信的桥梁。
另外需要注意的是,attach()方法的实现非常奇怪,它直接创建一个实例但是并没有将这个实例赋值给某一个成员变量,在attach()方法结束时岂不是会被垃圾回收?不难想到,【在IWallpaperEngineWrapper的构造函数】中一定有些动作可以使得这个实例不被释放。参考其实现:
[WallpaperService.java-->IWallpaperEngineWrapper.IWallpaperEngineWrapper()]
IWallpaperEngineWrapper(WallpaperService context,
IWallpaperConnection conn, IBinder windowToken,
intwindowType, boolean isPreview, int reqWidth, int reqHeight) {
/* 创建一个HandlerCaller。
HandlerCaller是Handler的一个封装,而它与Handler的区别是额外提供了
一个executeOrSendMessage()方法。当开发者在HandlerCaller所在的线程
执行此方法时会使得消息的处理函数立刻得到执行,在其他线程中执行此方法的效果
则与Handler.sendMessage()别无二致。除非阅读代码时遇到这个方法,读者
只需要将其理解为Handler即可。
注意意通过其构造函数的参数可知HandlerCaller保存了IWallpaperEngineWrapper的实例 */
mCaller= new HandlerCaller(context,
mCallbackLooper != null
? mCallbackLooper : context.getMainLooper(),
this);
// 将WallpaperManagerService所提供的参数保存下来
mConnection = conn; // conn即是WallpaperManagerService中的WallpaperConnection
mWindowToken = windowToken;
mWindowType = windowType;
mIsPreview = isPreview;
mReqWidth = reqWidth;
mReqHeight = reqHeight;
// 发送DO_ATTACH消息。后续的流程转到DO_ATTACH消息的处理中进行
Messagemsg = mCaller.obtainMessage(DO_ATTACH);
mCaller.sendMessage(msg);
}
注意 在这里貌似并没有保存新建的IWallpaperEngineWrapper实例,它岂不是有可能在DO_ATTACH消息执行前就被Java的垃圾回收机制回收了?其实不是这样。HandlerCaller的构造函数以及最后的sendMessage()操作使得这个IWallpaperEngineWrapper的实例得以坚持到DO_ATTACH消息可以得到处理的时刻。sendMessage()方法的调用使得Message被目标线程的MessageQueue引用,并且对应的Handler被Message引用,而这个Handler是HandlerCaller的内部类,因此在Handler中有一个隐式的指向HandlerCaller的引用,最后在HandlerCaller中又存在着IWallpaperEngineWrapper的引用。因此IWallpaperEngineWrapper间接地被HandlerCaller所在线程的MessageQueue所引用着,因此在完成DO_ATTACH消息的处理之前,IWallpaperEngineWrapper并不会被回收。虽然这是建立在对Java引用以及Handler工作原理的深刻理解之上所完成的精妙实现,但是它确实已经接近危险的边缘了。
在这里所创建的mCaller具有十分重要的地位。它是一个重要的线程调度器,所有壁纸相关的操作都会以消息的形式发送给mCaller,然后在IWallpaperEngineWrapper的executeMessage()方法中得到处理,从而这些操作转移到mCaller所在的线程上进行(如壁纸绘制、事件处理等)。可以说mCaller的线程就是壁纸的工作线程。默认情况下这个mCaller运行在壁纸服务的主线程上即context.getMainLooper()。不过当WallpaperService.mCallbackLooper不为null时会运行在mCallbackLooper所在的线程。mCaller运行在壁纸服务的主线程上听起来十分合理,然而提供手段以允许其运行在其他线程的做法却有些意外。其实这是为了满足一种特殊的需求,以ImageWallper壁纸服务为例,它是SystemUI的一部分而SystemUI的主线程主要用来作为状态栏、导航栏的管理与绘制的场所,换句话说其主线程的工作已经比较繁重了。因此ImageWallpaper可以通过这一手段将壁纸的工作转移到另外一个线程中进行。不过因为这一机制可能带来同步上的问题,因此在Android 4.4及后续版本中被废除了。
接下来分析DO_ATTACH消息的处理:
[WallpaperService.java-->IWallpaperEngineWrapper.executeMessage()]
public void executeMessage(Message message) {
switch(message.what) {
case DO_ATTACH: {
try {
/* ①###[[[ 把IWallpaperEngineWrapper实例(BInder对象)传递给WallpaperConnection进行保存。
保存在WallpaperConnection.mEngine成员之中
至此这个实例便名花有主,再也不用担心被回收了 ]]]###,而且WallpaperManagerService
还可以通过它与实际的Engine进行通信 */
mConnection.attachEngine(this);
} catch (RemoteException e) {}
/* ② 通过onCreateEngine()方法创建一个Engine。
onCreateEngine()是定义在WallpaperService中的一个抽象方法。
WallpaperService的实现者需要根据自己的需要返回一个自定义的Engine的子类 */
Engine engine = onCreateEngine();
mEngine = engine;
/* ③ 将新建的Engine添加到WallpaperService.mActiveEngines列表中。
读者可能会比较奇怪,为什么是列表?难道一个Wallpaper可能会有多个Engine么?
这个奇怪之处还是壁纸预览所引入的。当壁纸A已经被设置为当前壁纸之时,系统中会存
在一个它所对应的WallpaperService,以及在其内部会存在一个Engine。
此时当LivePicker或其他壁纸管理工具预览壁纸A时,它所对应的WallpaperService
仍然只有一个,但是在其内部会变成两个Engine。
这一现象更能说明,WallpaperService仅仅是提供壁纸运行的场所,而Engine才是真正
的壁纸的实现 */
mActiveEngines.add(engine);
// ④ 最后engine.attach()将会完成窗口的创建、第一帧的绘制等工作
engine.attach(this);
return;
}
}
}
正如前文所述,作为拥有跨Binder调用的IWallpaperEngineWrapper通过attachEngine()方法将自己传递给了WallpaperConnection,后者将其保存在WallpaperConnection.mEngine成员之中。从此之后,WallpaperManagerService便可以通过WallpaperConnection.mEngine与壁纸服务进程中的IWallpaperEngineWrapper进行通信,而IWallpaperEngineWrapper进一步将来自WallpaperManagerService中的请求或设置转发给Engine对象,从而实现了WallpaperManagerService对壁纸的控制。
到目前为止,WallpaperManagerService与壁纸服务之间已经出现了三个用于跨Binder通信的对象。它们分别是:
· IWallpaperService,实现在壁纸服务进程之中,它所提供的唯一的方法attach()用于在壁纸服务启动后接收窗口创建所需的信息(Token等等,WallpaperManagerService传给壁纸服务),或者说为了完成壁纸的初始化工作。除此之外IWallpaperService不负责任何功能,WallpaperManagerService对壁纸进行的请求与设置都交由在attach()的过程中所创建的IWallpaperEngineWrapper实例完成。
· WallpaperConnection,实现在WallpaperManagerService中,并通过IWallpaperService.attach()方法传递给了IWallpaperEngineWrapper。壁纸服务通过WallpaperConnection的attachEngine()方法将IWallpaperEngineWrapper实例传递给WallpaperManagerService进行保存。另外壁纸服务还通过它的engineShown()方法将壁纸显示完成的事件通知给WallpaperManagerService。
· IWallpaperEngineWrapper,实现在壁纸进程中。Engine实例是壁纸实现的核心所在。作为Engine实例的封装者,它是WallpaperManagerService对Engine进行请求或设置的唯一接口。
总体来说,IWallpaperService与WallpaperConnection主要服务于壁纸的创建阶段,而IWallpaperEngineWrapper则用于在壁纸的运行阶段对Engine进行操作与设置。
说明 按照常规的思想来推断,WallpaperManagerService与WallpaperService之间应该仅仅需要IWallpaperService提供接口对壁纸进行操作与设置。为什么要增加一个IWallpaperEngineWrapper呢?这得从WallpaperService与Engine之间的关系说起。IWallpaperService在WallpaperManagerService看来表示的是WallpaperService,而IWallpaperEngineWrapper则表示的是Engine。WallpaperService是Engine运行的容器,因此它所提供的唯一的方法attach()用来在WallpaperService中创建新的Engine实例(由创建一个IWallpaperEngineWrapper实例来完成)。Engine则是壁纸的具体实现,因此IWallpaperEngineWrapper所提供的方法用来对壁纸进行操作与设置。从这个意义上来讲IWallpaperService与IWallpaperEngineWrapper的同时存在是合理的。另外,将IWallpaperService与IWallpaperEngineWrapper分开还有着简化实现的意义。从DO_ATTACH消息的处理过程可知,WallpaperService中可以同时运行多个Engine实例。而WallpaperManagerService或LivePicker所关心的只是某一个Engine,而不是WallpaperService中的所有Engine,因此相对于使用IWallpaperService的接口时必须在参数中指明所需要操作的Engine,直接操作IWallpaperEngineWrapper更加简洁直接。
Engine创建完毕之后会通过Engine.attach()方法完成Engine的初始化工作。参考其代码:
[WallpaperService.java-->Engine.attach()]
void attach(IWallpaperEngineWrapper wrapper) {
......
// 保存必要的信息
mIWallpaperEngine = wrapper;
mCaller= wrapper.mCaller;
mConnection = wrapper.mConnection;
mWindowToken = wrapper.mWindowToken;
/* ① mSurfaceHolder是一个BaseSurfaceHolder类型的内部类的实例。
Engine对其进行了简单的定制。开发者可以通过mSurfaceHolder定制所需要的Surface类型 */
mSurfaceHolder.setSizeFromLayout();
mInitializing = true;
// 获取WindowSession,用于与WMS进行通信
mSession= WindowManagerGlobal.getWindowSession(getMainLooper());
//mWindow是IWindow的实现,窗口创建之后它将用于接收来自WMS的回调
mWindow.setSession(mSession);
//Engine需要监听屏幕状态。这是为了保证在屏幕关闭之后,动态壁纸可以停止动画的渲染以节省电量
mScreenOn =
((PowerManager)getSystemService(Context.POWER_SERVICE)).isScreenOn();
IntentFilter filter = new IntentFilte r();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(mReceiver, filter);
/* ② 调用Engine.onCreate()。
Engine的子类往往需要重写此方法以修改mSurfaceHolder的属性,如像素格式,尺寸等。
注意此时 尚未创建窗口,在这里所设置的SurfaceHolder的属性将会在创建窗口时生效 */
onCreate(mSurfaceHolder);
mInitializing = false;
mReportedVisible = false;
/* ③ 最后updateSurface将会根据SurfaceHolder的属性创建窗口以及Surface,并进行
壁纸的第一次绘制 */
updateSurface(false, false, false);
}
Engine.attach()方法执行的结束标志着壁纸启动工作的完成,至此在最后的updateSurface()方法结束之后新的壁纸便显示出来了。
(4)壁纸的创建流程
可见,壁纸的创建过程比较复杂。在这个过程中存在着多个Binder对象之间的互相调用。因此有必要对此过程进行一个简单的整理:
· 首先,壁纸管理程序(如LivePicker)调用IWallpaperManager.setWallpaperComponent()要求WallpaperManagerService设置指定的壁纸
· WallpaperManagerService通过调用bindWallpaperComponentLocked()将给定的壁纸服务启动起来。同时旧有的壁纸服务会被终止。
· WallpaperManagerService成功连接壁纸服务后,调用壁纸服务的attach()方法将窗口令牌等参数交给壁纸服务。
· 壁纸服务响应attach()的调用,创建一个Engine。
· Engine的updateSurface()方法将会创建壁纸窗口及Surface,并进行壁纸的绘制。
而在这个过程中,WallpaperManagerService中存在如下重要的数据结构:
· WallpaperInfo,存储了动态壁纸的开发者、缩略图与描述信息。这个数据结构创建于WallpaperManagerService.bindWallpaperComponentLocked()方法,其内容来自于壁纸所在应用程序的AndroidManifest.xml中名为android.service.wallpaper的meta-data。
· WallpaperConnection,它不仅仅是壁纸服务与WallpaperManagerService进行通信的渠道,它同时也保存了与壁纸服务相关的重要的运行时信息,如IWallpaperService、IWallpaperEngineWrapper、WallpaperInfo以及用于创建窗口所需的窗口令牌。WallpaperConnection创建于WallpaperManagerService.bindWallpaperComponentLocked()方法。
· WallpaperData,它保存了一个壁纸在WallpaperManagerService中可能用到的所有信息,包括壁纸服务的ComponentName,WallpaperConnection,壁纸服务的启动时间等。WallpaperData被保存在一个名为mWallpaperMap的SparseArray中,而且设备中每一个用户都会拥有一个固定的WallpaperData实例。当前用户进行壁纸切换时会更新WallpaperData的内容,而不是新建一个WallpaperData实例。另外,WallpaperData中还保存了与静态壁纸相关的一些信息,关于静态壁纸的内容将在8.3节进行介绍。
壁纸的创建过程同时体现了壁纸服务与WallpaperManagerService之间的关系,如图8-1所示。
图 8 - 1 壁纸服务与WallpaperManagerService之间的关系
8.2.3理解 UpdateSurface()方法
Engine.attach()最后所调用的Engine.updateSurface)方法是Engine所提供的壁纸框架的核
心所在。此方法的意义对壁纸来说,就好比performLayoutAndPlaceSurfacesLocked()对WMS
以及performTraversals()对控件树一样。updateSurface()的主要目的是将SurfaceHolder中的
设置以及窗口属性设置(如窗口的flag)过WMS的relayoutWindow()同步到Surface与窗口。
在这个过程中Surface的状态发生变化时会触发SurfaceHolder相关的回调。另外,当壁纸窗
口尚未创建或Surface尚未创建时,updateSurface()会通过WMS的接口进行窗口或Surface的
创建。
updateSurface()方法比较大,本节按照其工作内容将其划分为几个部分进行讨论。
updateSurface()中既然包含了许多的工作,那么必须根据某些条件确定这些工作是否真的有必
要做,以避免浪费CPU资源。参考如下代码:
[WallpaperService.java->Engine.updateSurface()]
void updateSurface(boolean forceRelayout,boolean forceReport,boolean redrawNeeded){
.......
/*获取mSurfaceHolder中新保存的尺寸。倘若这一尺寸为默认情况下的(-1,-1),
则updateSurface()会认为其表示的尺寸为MATCH_PARENT*/
int myWidth = mSurfacelolder.getRequestedwidth();
if (myWidth <= 0)mywidth=ViewGroup.LayoutParams.MATCH_PARENT;
int myHeight = mSurfaceHolder.getRequestedHeight();
if(myHeight <=0 )myieight=ViewGroup.LayoutParams.MATCH_PARENT;
//下面的一组变量是更新surface的条件。
//①窗口尚未创建
final boolean creating = !mCreated;
//②Surface尚未创建
final boolean surfaceCreating = !mSurfacecreated;
//③mSurfaceHolder中的像素格式发生了变化
final boolean formatChanged = mFormat != mSurfaceHolder.getRequestedFormat();
//④尺寸发生变化
boolean sizechanged = mwidth != myWidth || mHeight != myHeight;
//⑤Surface的类型发生了变化
final boolean typeChanged = mtype != mSurfaceHolder.getRequestedrype();
//⑥窗口的Flag发生了交换
final boolean flagachanged = mcurwindowFlags != mwindowFlags ||
mCurWindowPrivateFlags != mWindowPrivateFlags;
//只要以上条件满足其一便有必要更新Surface
if(forceRelayout || creating || surfacecreating || formatChanged || sizechanged
|| typechanged || flagschanged || redrawNeeded
|| lmIWallpaperEngine.mShownReported){
...... //壁纸窗口的创建、重新布局以及在必要时触发SurfaceHolder的回调
}
}
从这些条件中可以看到updateSurface()方法可能进行的工作如下:
口 创建壁纸窗口,由mCreated成员指定。
口 从WMS申请Surface,由mSurfaceCreated 成员指定。
口 修改Surface的像素格式,由SurfaceHolder.getRequestedFormat()的返回值指定。
口 修改Surface的尺寸,由SurfaceHolder.getRequestedWidth()/Height()的返回值指定。
口 修改Surface内存的类型,即NORMAL、GPU、HARDWARE以及PUSHBUFFERS。
由SurfaceHolder.getRequestedType()的返回值指定。
口 修改窗口的flags,由mwindowPlags成员指定。对壁纸窗口来说,窗口flags的
变化主要是由于Engine.setTouchEventsEnabled()方法增加或删除了FLAG_NOT_
TOUCHABLE标记。
在updateSurface()后续的代码中将会看到这些变量如何对Surface产生影响。
[WallpaperService.java-->Engine.updateSurface()]
void updateSurface (boolean forceRelayout,boolean forceReport,boolean redrawNeeded){
....... //surface更新条件的计算
if(forceRelayout ll creating ll surfaceCreating ll formatChanged || sizeChanged
|| typechanged ll flagsChanged ll redrawNeeded
|| !mIWallpaperEngine.mshownReported){
try{
/*将SurfaceHolder中的设置转储到Engine的成员变量中。
用于在下次updatesurface()的调用中检查它们是否发生变化*/
mwidth = mywidth;
mHeight = myHeight;
mFormat = mSurfaceHolder.getRequestedFormat();
mType = mSurfaceHolder.getRequestedType();
/*①更新窗口的LayoutParam。上述的像素格式、尺寸、内存类型以及窗口flags会使用
LayoutParams经过窗口的重新布局以完成设置*/
mLayout.x=0;
mLayout.y=0;
mLayout.width=mywidth;
mLayout.height=myHeight;
......
mLayout.flags=mWindowFlags
| WindowManager.J,ayout Params.FLAG LAYOUT_ NO_LIMITS
| windowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
......
//mwindowToken来自WallpaperManagerService的wallpaperConnection
mLayout.token = mWindowToken;
//②若壁纸窗口尚未创建,则进行窗口创建
if (!mCreated){
//窗口的类型来自IWallpaperEngineWrapper.attach()方法
mLayout.type=mIWallpaperEngine.mMindowType;
......
mInputChannel=new InputChannel();
//创建窗口,注意壁纸窗口会被添加到DEFAULT_DISPLAY中
if (mSession.addToDisplay (mWindow,mWindow.mSeg,mlayout,View.VISIBLE,
Display.DEFAULT_DISPLAY,mContentInsets,mInputchannel)<0){
return;//创建窗口失败就直接返回
}
mCreated =true;//标记窗口已经创建完成
//创建InputEventReceiver实例用于接收触摸事件
mInputEventReceiver=new WallpaperInputEventReceiver(mInputChannel,Looper.myLooper());
}
/*接下未的操作会修改Surface,因此必须将SurfaceHolder锁住,以免其他线程在这个过程
中尝试通过SurfaceHolder.lockCanvas()修改surface的内容*/
mSurfaceHolder.mSurfaceLock.lock();
mDrawingAllowed = true;
/*③重新布局窗口。它将SurfaceHolder中的设置以及窗口属性同步到Surface及其窗口值中。
倘若壁纸窗口刚刚完成创建,则经过重新布局后其Surface也会变得有效*/
final int relayoutResult=msession.relayout(
mWindow,mNindow.mSeq,mLayout,mWidth,mHeight,
View.VISIBLE,0,mWinFrame,mContentInsets,
mVisibleInsets,mconfiguration,msurfacellolder.msurface);
/*尽管SurfaceHolder的设置给出期望的尺寸,但是WMS拥有决定窗口最终尺寸的权利。因此
同ViewRoot Impl.performTraversals()中的行为一样,updateSurface()将WMS的布
局结果设置给SurfaceHolder,强迫其接受这个结果*/
int w = mwinFrame.width();
if(mCurNidth != w){
sizeChanged = true;
mCurWidth = w;
}
int h = mwinFrame.height();
if(mCurHeight != h){
sizeChanged = true;
mCurHeight = h;
}
mSurfaceHolder.setSurfaceFrameSize(w,h);
//Surface更新成功,解除对SurfacelHolder的锁定
mSurfaceBolder.mSurfaceLock.unlock();
........//触发SurfaceHolder的回调
}catch(RemoteException ex){.......}
}
}
这部分updateSurface()的代码完成Surface更新。其原理十分简单:
口 倘若窗口尚未创建,则通过WMS.addWindow()完成窗口的创建。
口 通过WMS.relayoutWindow()对窗口进行重新布局。重新布局的结果是倘若窗口尚
没有一块可用的Surface(如在这个方法中刚刚完成创建工作),Engine将会拥有一
块可用的Surface。另外,存储在LayoutParams中与Surface或窗口有关的参数都会
被WMS接纳并据此修改Surface的属性(尺寸、像素格式、内存类型以及窗口的flags)。
因此updateSurface()会使得 Surface发生创建或者改变,而这些变化正是SurfaceHolder
的使用者所关心的。在完成Surface的更新工作后,updateSurface()会触发SurfaceHolder
的回调以通知所有SurfaceHolder的使用者(即由开发者所实现的Engine的子类)。参考
updateSurface()的后续代码:
[WallpaperService.java->Engine.updateSurface()]
void updatesurface(boolean forceRelayout,boolean forceReport,boolean redrauleeded){
......... //Surface更新条件的计算
if(forceRelayout || creating || surfacecreating || formatChanged || sizeChanged
|| typechanged || flagsChanged || redrawNeeded
||!mIWallpaperEngine.mShownReported){
try{
......//进行壁纸窗口的创建以及重新布局的工作
try{
mSurfaceHolder.ungetcallbacks();
if (surfacecreating){
/*①发起onSurfaceCreated()回调。Engine将会收到这一回调,并对即将到来的绘
制工作做好初始化工作*/
onSurfaceCreated(mSurfaceHolder);
...... //将创建Surface的消息通知给surfaceHolder的同名回调
}
redrawNeeded |= creating ||(relayoutResult
& WindowlanagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0;
if (forceReport || creating || surfaceCreating
|| formatChanged || sizeChanged){
/*②发起onSurfacechanged()回调。Engine将会收到这一回调,并根据新的尺寸或
像未格式等信息对即将到来的绘制工作做好必要的准备工作*/
onSurfaceChanged(mSurfaceHolder,mFormat,mCurwidth,mcurHeight);
......//将Surface属性发生改变的消息通知给SurfaceHolder的同名回调
}
if(redrawleeded){
/*③发起onSurfaceRedrawNeeded()回调。
Engine将会收到这一回调,并绘制壁纸*/
onSurfaceRedrawNeeded(mSurfacelfolder);
...... //将Surface需要重绘的消息通知给SurfaceHolder的同名回调
}
......
}finally{
mIsCreating=false;mSurfaceCreated = true;
/*将绘制完毕的消息通知WMS。相信读者经过第4章以及第6章的学习之后对这个方法的作
用已经非常熟悉。它是显示新建窗口的必要条件*/
if(redrawNeeded){
mSession.finishDrawing(mWindow);
}
//将绘制完毕的消息通知WallpaperManagerService
mIWallpaperEngine.reportShown();
}
}catch (RemoteException ex){........}
}
}
总体来说,Engine.updateSurface()方法的工作主要有两个部分:
口 根据 SurfaceHolder的设置创建或更新Surface。SurfaceHolder中的设置被开发者修改
时,WallpaperService会接受来自SurfaceHolder的回调,并通过调用updateSurface()
方法使新的设置生效。SurfaceHolder允许修改的设置有尺寸、像素格式、内存类
型,以及是否防止屏幕休眠4种。然而,除非有特殊需求,像素格式以及内存类
型两种设置一般在Surface创建后便不会发生变化。至于防止屏幕休眠,很显然壁
纸的Surface并不适用,因此当对壁纸的SurfaceHolder尝试修改这一设置时会引发
一个异常。尺寸的设置比较特殊,SurfaceHolder提供了两个接口可以对Surface的
尺寸进行修改,它们是setFixedSize(int w,int h)和setSizeFromLayout()。setFixedSize(int w,int h)可以
接受指定的宽高作为参数并保存,然后在Engine.updateSurface()中使用这一宽高
布局窗口进而修改Surface的尺寸。而setSizeFromLayout()不接受任何参数,而是将SurfaceHolder
中保存的宽高复位为-1,然后在Engine.updateSurface()中使用默认的宽高——如
代码中所示的MATCH_PARENT——进行窗口的布局。简而言之,在壁纸这一用
例中,SurfaceHolder.setFixedSize()用于直接设置壁纸的尺寸,而SurfaceHolder.
setSizeFromLayout()则是将壁纸的尺寸恢复到默认的MATCH_PARENT,即充满屏
幕。遗憾的是,并不是所有的壁纸应用都可以使用setFixedSize0方法设置Surface的
尺寸,因为这种方式实在太过自由。因此Android 只允许内置ImageWallpaper使用这
一方法。另外,Engine.setTouchEventsEnabled()会在LayoutParams.flags 中添加或删除
FLAG_NOT_TOUCHABLE标记,以允许或禁止壁纸窗口接受触摸事件。这一设置同
样需要updateSurface()方法得以生效。
口 引发与Surface有关的各种回调。这些回调包括onSurfaceCreated()、onSurfaceChanged()
和onSurfaceRedrawNeeded()。Engine还有另外一个会在壁纸被销毁时触发回调
onSurfaceDestroyed()。这些回调从Surface的角度向开发者提供了关于壁纸的生命周
期相关的信息。开发者可以通过对这些回调做出响应从而保证正确显示壁纸。
8.2.4 壁纸的销毁
在8.2.2节介绍WallpaperManagerService.bindWallpaperComponentLocked()方法时提到过,
当WalpaperManagerService启动新的壁纸时,会通过detach WallpaperLocked()方法销毁之前
的壁纸。壁纸的销毁工作要比壁纸的启动简单得多。启动壁纸时的工作主要有注册窗口令牌、
启动对应的壁纸服务以及IWallpaperService.attach()三个步骤。那么销毁工作自然是将这三个
动作反过来执行。参考其代码实现:
[WallpaperManagerService.java->WalpaperManagerService.detachWallpaperLocked()]
void detachwallpaperLocked(WallpaperData wallpaper){
if(wallpaper.connection!=null){
/*①执行IWallpaperEngine.deetroy()方法。这个方法会在壁纸服务的进程中触发
onSurfaceDestroyed()回调以通知开发者销毁壁纸运行期间所使用的资源,并移除壁纸窗口*/
if (wallpaper.connection.mEngine!=null){
try{
wallpaper.connection.mEngine.destroy();
}catch(RemoteException e){}
}
//②unbindservice()将终止WallpaperManagerService对壁纸服务的绑定
mContext.unbindservice(wallpaper.connection);
/*③注销用来添加壁纸窗口的令牌。即便被销毁的壁纸服务所在的进程用某种方法(比如说反射)偷偷
保存了一份窗口令牌,以期能够在壁纸销毁之后强行进行壁纸窗口的创建是徒劳的。因为在壁纸被
销毁之后,它所保有的窗口令牌也就变得无效了*/
try{
mIWindowManager.removeWindowToken(wallpaper.connection.mroken);
}catch (RemoteException e){}
......
}
}
而在壁纸服务的进程收到Engine.destroy()的调用之后会从mActiveEngines列表中将当前
Engine 删除,然后调用Engine.detach()方法:
[WallpaperService.java-->Engine.detach()]
void detach(){
//① reportSurfaceDeetroyed()会触发onSurfacepeatroyed()回调
reportSurfaceDestroyed();
//②触发onDeetroy()回调
onDestroy();
if(mCreated){
try{
//③移除壁纸窗口
mSesaion.remove(mWindow);
}catch(RemoteException e){.......}
......
}
}
从壁纸开发者的实现而言,壁纸的销毁过程会触发onSurfaceDestroyed0以及onDe-
stroyed0两个回调以通知开发者进行相关资源的销毁。
8.2.5理解Engine的回调
在分析壁纸的启动、销毁以及updateSurface()的过程中遇到以下几个Engine类的回调:
口 onCreate(),此回调发生在Engine.attach()方法中,表示Engine生命周期的开始。
口 onDestroy(),此回调发生在Engine.detach()方法中,表示Engine生命周期的结束。
口 onSurfaceCreated()/onSurfaceChanged()/onSurfaceDestroyed()/onSurfaceRedrawNeeded()
4个回调发生在updateSurface()以及Engine.detach()方法中。重写这些回调与在
SurfaceHolder中注册Callback是等效的。它们向壁纸开发者描述了Surface的生命周
期以及状态变化。
另外,Engine中还有一些上文并未提及的重要回调。
onVisibilityChanged(),通知Engine当前壁纸的可见性发生了变化。读者可能会很自
然地将这个可见性与壁纸窗口的可见性联系到一起。其实它们是两种不同的概念。窗口可
见性表示的是窗口是否拥有一块可供绘制的Surface。而从这个意义上讲,只要Wallpaper-
ManagerService启动一个壁纸并且创建窗口,那么在壁纸被销毁之前此窗口永远是可见的,这
一点从updateSurface()方法的实现中可以看到。那么onVisiblityChanged()的可见性所指为何
呢?这得从WMS对壁纸窗口的优化说起。壁纸窗口从用户眼中所看到的形式并不像是一个
窗口,它更像是另一个窗口的背景。是否以壁纸窗口作为其背景取决于窗口是否在其flags
中指定FLAG_SHOW_WALLPAPER标记。于是当指定此标记的窗口与未指定此标记的窗口
交替显示时(例如在HOME应用与短信应用之间切换时),壁纸窗口需要随着指定此标记的
窗口在可见与不可见状态之间切换。通过第4章的学习可以知道,使窗口从不可见变为可见
并不是一件简单的事情。它经历了窗口的重新布局、Surface的创建、等待窗口完成第一次
绘制等工作。其中窗口完成第一次绘制的开销对壁纸窗口来说尤为巨大,因为从存储设备中
载入并显示一个高分辨率的图片所导致的延迟是很大的。这会使得壁纸窗口的显示很容易
滞后于指定此标记的窗口的显示。在这个滞后的时间段内,用户透过窗口的内容所看到的
可能是一个黑乎乎的背景,或者是上一个应用程序的界面。为了解决这个问题,Android索
性让壁纸窗口永远处于可见状态,只是当WMS认为壁纸窗口不应被显示时,将其藏在所有
窗口的最后面使用户看不到。而当一个指定FLAG_SHOW_WALLPAPER标记的窗口处于前
台显示时,再将壁纸窗口移动到这一窗口的下方使得用户可以看到它。这样一来便避免了
壁纸窗口初次显示的时间开销。不过这样一来又会引入新的问题,动态壁纸往往是一个动
画,它会不断地刷新其显示内容从而不断占用CPU的资源。当WMS将壁纸窗口隐藏于所有
窗口底部时,这种资源占用既不必要又很浪费。于是,onVisibilityChanged()回调应运而生。
onVisibilityChanged()并不是表示WMS销毁或创建壁纸窗口的Surface,而是表示WMS将壁
纸窗口衬于某一个窗口下方作为其背景,或是将壁纸窗口藏在所有窗口之下使得用户看不到。
所以当onVisibilityChanged()报告壁纸的可见性为false时,Engine必须停止一切消耗资源的
操作,反之Engine应当全力工作以讨好用户的眼球。
当WMS将壁纸窗口置于其他窗口底部时,它会调用Surface.hide()方法将壁纸的
Surface隐藏。这种隐藏相对于通过relayoutWindow()使窗口变得不可见来说非常轻
量。它不会导致释放Surface或者清除Surface中已经被绘制的内容。当需要显示壁纸
时会调用Surface.show()方法,由于Surface中已绘制的内容并没有被清除,所以不需
要重绘Engine对象。
onTouchEvent(),用于处理用户的触摸事件。开发者可以通过调用Engine.setTouch-
EventsEnabled()方法在壁纸窗口的flags中增加或删除FLAG_NOT_TOUCHABLE标记,
从而设置壁纸窗口是否可以接收用户的输入事件。壁纸窗口可以接受触摸事件这个事实有
一些匪夷所思,因为InputDispatcher查找触摸事件的派发目标的基本算法是这样的:它会
沿着窗口的Z序从上向下遍历查找触摸事件落在其上的第一个窗口,并将这个窗口作为派
发目标。很显然,要求显示壁纸的窗口A位于壁纸窗口之上,那么窗口A自然会阻挡壁纸
窗口作为触摸事件的派发目标。据此推断,壁纸窗口岂不是永远都无法接受触摸事件,进
而这个onTouchEvent()回调便成了无用的摆设?其实不然,这得益于InputDispatcher为壁
纸窗口做了一个小小的特殊化处理。在InputDispatcher::findTouchedWindowTargetsLocked()
函数通过上述策略找到触摸事件的派发目标窗口之后,会检查这个窗口是否要求显示
壁纸,如果是,则会在窗口列表中寻找类型为TYPE_WALLPAPER的壁纸窗口,并将其作
为一个额外的派发目标添加到派发目标窗口列表中,并在随后将触摸事件同时发送给这两
个窗口。
onOffsetChanged(),用于通知Engine对象需要对壁纸所绘制的内容进行偏移。这个回调
引发于WallpaperManager.setWallpaperOffsets()。当要求显示壁纸的窗口的内容发生滚动时,它
可能希望壁纸的内容也能随之偏移。一个典型的例子就是HOME应用,当用户在主界面中
拖动图标时,HOME应用会通过WallpaperManager.setWallpaperOffsets()告知壁纸服务中的
Engine对象需要进行偏移的量,而这个偏移量是一个0到1的浮点值。如何表现这个偏移量
在Android中并没有给出一个标准,最传统的做法是类似于静态壁纸那样对绘制内容进行滚
动。不过壁纸的开发者可以根据自己的创意自由发挥,例如将偏移量体现为颜色的变化、粒
子效果的变化等。
onCommand(),用于处理来自WallpaperManager.sendWallpaperCommand()的一个命令。
Android 并没有规定sendWallpaperCommand()将会发送什么样的命令给engine。开发者可以根
据需要自定义命令。换句话说,WallpaperManager.sendWallpaperCommand()与onCommand()
回调共同为开发者提供了一种通信方式。
onDesiredSizeChanged(),用于处理来自WalilpaperManager.suggestDesiredDimensions()方
法的请求。suggestDesiredDimensions()由显示壁纸的窗口调用,用于指明它所期望的壁纸的尺
寸。例如,当一个窗口中所显示的所有内容的宽度为2000、高度为3000时,它可以通过这个
方法尝试将壁纸的宽度与高度分别设置为2000与3000,从而当用户对内容进行拖动时,可以
在setWallpaperOffsets()方法的帮助下使得壁纸与内容同步滚动。
这些回调可以理解为Android特别为壁纸规定的一套协议。壁纸实现者可以在这些回调
的驱动下完成对壁纸的实现。
8.3 深入理解静态壁纸——ImageWallpaper
本节将会对静态壁纸进行介绍。所谓的静态壁纸是运行于SystemUI进程中的一个名为
ImageWallpaper的特殊的动态壁纸而已。首先它是一个动态壁纸,因为它是基于前文所述的
动态壁纸的架构实现的。而说它是静态的,是因为它的内容是一张静态的图片。之所以将它
区别于其他众多内置的动态壁纸,是因为Android为这个静态壁纸提供了特殊的API以及实
现机制,使得开发者可以方便地将一张静态图片设置为壁纸。
8.3.1 获取用作静态壁纸的位图
静态壁纸所对应的壁纸服务实现于SystemUI之中,它定义了一个继承自WallpaperService
的服务ImageWallpaper作为其运行容器,并在其onCreateEngine()方法中创建了一个
继承自Engine的DrawableEngine 对象作为其Engine实现。
在DrawableEngine中有一个Bitmap类型的成员变量mBackground。这个mBackground
即作为壁纸的位图。每当需要对静态壁纸进行重绘时,DrawableEngine会通过调用
drawFrameLocked()方法选择通过软件方式或硬件加速的方式将这个mBackground绘制到
壁纸窗口的Surface上。
这一切都很简单,读者可以自行对这个过程进行研究。然而需要讨论的问题是,mBack-
ground从哪里来呢?纵览ImageWallpaper的代码可以发现,mBackground的赋值位于
DrawableEngine.updateWallpaperLocked()方法之中。参考其代码:
[ImageWallpaperjava->DrawableEngine.updateWallpaperLocked()]
private void updatewallpaperlocked(){
Throwable exception = null;
try{
//①忘记已经加载的壁纸
mWallpaperManager.forgetLoadedNallpaper();
mBackground=null;
//②从WallpapaerManager.getBitmap()方法中获取作为壁纸的位图
mBackground=mWallpaperManager.getBitmap();
}catch (RuntimeException e){exception=e;}
......
}
此时理解WallpaperManager.forgetLoadedWallpaper()方法的意义可能比较困难,因此,首
先讨论WallpaperManager.getBitmap()的实现。
[WallpaperManagerjava->WallpaperManager.getBitmap()]
public Bitmap getBitmap(){
/*sGlobals是一个Globals类型的进程唯一的静态成员,它是WallpaperManager与
WallpaperManagerService通信的代理人。其中true表示当位图加载失败时是否返回默认的壁纸位图*/
return sGlobals.peekwallpaperBitmap (mcontext,true);
}
进一步分析Globals.peekWallpaperBitmap()的实现。
[WallpaperManager.java->Globals.peekWallpaperBitmap()]
public Bitmap peekwallpaperBitmap (Context context,boolean returnDefault){
synchronized {this){
//①倘若已加载了璧纸位图,则返回
if(mWallpaper!=null){
return mWallpaper;
}
//②倘若没有加载壁纸位图,则返回已加载的默认壁纸位图
if (mDefaultWallpaper!=null){
return mDefaultwallpaper;
}
mWallpaper = null;
try{
//③通过getcurrentWallpaperLocked()方法获取壁纸位图,并缓存在mWallpaper中
mWallpaper = getCurrentWallpaperLocked(context);
}catch (OutofMemoryError e){.......}
if(returnDefault){
if(mWallpaper == null){
//④若壁纸位图加载失败,则加载默认的壁纸位图并返回
mDefaultWallpaper = getDefaultwallpaperLocked(context);
return mDefaultlallpaper;
}else{
//倘若壁纸位图加载成功,则释放已加载的默认位图
mDefaultlallpaper = null;
}
}
return mWallpaper;
}
}
peekWallpaperBitmap()方法中出现了两个比较重要的成员:mWallpaper以及
mDefaultWalpaper。在二者都未被加载时,peekWallpaperBitmap()会首先尝试通过
getCurrentWallpaperLocked()方法获取用作壁纸的位图并保存在mWallpaper成员中。于是下次再调用
peekWallpaperBitmap()时,就会直接返回mWallpaper,从而避免再次加载位图的开销。然
而,倘若位图加载失败(比如说位图尺寸太大导致OOM),peekWallpaperBitmap()会通过
getDefaultWalpaperLocked()方法加载默认位图存储在mDefaultWallpaper中并返回。那么下次
再调用peekWallpaperBitmap()时,就会直接返回mDefaultWallpaper。简单来说,在这个只有
少许代码的peekWallaperBitmap()中竟提供了两套保证静态壁纸稳定运行的机制:缓存以及备
用方案。peekWallpaperBitmap()会尽可能地返回已经加载的位图,并且当指定的位图加载失败
时,将会返回系统默认的位图。
于是WallpaperManager.forgetLoadedWallpaper()方法的意义也就清楚了。它的实现
是将mDefaultWallpaper以及mWallpaper两个成员设置为null,于是随后进行Globals.
peekWallpaperBitmap()调用时便会重新加载位图,而不是返回已经加载过的位图。
getDefaultWallpaperLocked()尝试通过加载com.android.internal.R.drawable.default
wallpaper资源所描述的位图作为系统的默认壁纸。不过倘若连这个位图的加载也失败
了,那么只好返回null。ImageWallpaper在这种最糟糕的情况下会显示为一片黑色。
那么getCurrentWallpaperLocked)方法如何获取被设置为壁纸的位图呢?参考其实现:
[WallpaperManager.java-->Globals.getCurrentWallpaperLocked()]
private Bitmap getCurrentWallpaperlocked(Context context){
try{
Bundle params=new Bundle();
/*①通过WallpaperManagerService.getWallpaper()方法获取存储了被设置为壁纸的位图
文件的描述符fd。其中params将会携带着位图的尺寸信息返回给Globals*/
ParcelFileDescriptor fd=mService.getWallpaper(this,params);
if(fd!=null){
int width=params.getInt("width",0);
int height=params.getInt("height",0);
try {
BitmapFactory.Options options = new BitmapFactory.Options();
//②将文件解码为一个位图,此时位图的格式为位图文件中所定义的格式
Bitmap bm = BitmapFactory.decodeFileDescriptor(
fd.getFileDescriptor(),null,options);
//③对位图进行转码,使其density、像素格式符合Android系统的要求
return generateBitmap(context,bm,width,height);
}catch (OutofMemoryError e)(......}
}
}catch(RemoteException e){......}
return null;
}
原来,被设置为壁纸的位图来自WallpaperManagerService.getWallpaper()方法。奇特的是
getWallpaper()方法返回的并不是一个Bitmap,而是一个文件描述符。
那么这个文件描述符又是从何而来呢?再看WallpaperManagerServie.getWallpaper()方法
的实现:
[WallpaperManagerService.java-->WalpaperManagerService.getWallpaper()]
public ParcelFilepescriptor getWallpaper(TWallpaperManagercallback cb,Bundle out Params){
synchronized (mLock){
/*正如在动态壁纸的启动过程中所计论过的那样,Android壁纸支持多用户设置,动态壁纸如此,静态
堂纸也是如此,所以不同的用户会拥有不同的壁纸位图。因此第一步仍然是鉴定调用者的用户身份*/
int callingUid = Binder.getCallingUid();
int wallpaperuserId = 0;
if(callingUid==android.os.Process.SYSTEM_UID){
//调用者若为系统代码,则返回当前用户的壁纸位图
wallpaperUserId = mCurrentUserId;
}else{
//否则获取调用者所属的用户ID
wallpaperUserId = UserHandle.getUserId(callinguid);
}
//①从mWallpaperMap中获取对应用户的WallpaperData
WallpaperData wallpaper=mWallpaperMap.get(wallpaperUserId);
try{
//从wallpaperData中获取壁纸位图的尺寸信息并保存在参数outParams中
if(outParams!= null){
outParams.putInt("width",wallpaper.width);
outParams.putInt("height",wallpaper.height);
}
//②打开指定位置的一个位图文件
File f = new File(getWallpaperpir(wallpaperUserId),WALLPAPER);
if(!f.exists()){
return null;
}
//③将文件包装为一个文件描述符并返回给调用者
return ParcelFileDescriptor.open(f,MODE_READ_ONLY);
}catch(FileNotEoundException e){.....}
return null;
}
}
可见,壁纸位图存储在文件夹getWallpaperDir(wallpaperUserld)下的WALLPAPER文
件中。查看getWallpaperDir()的实现可知这个文件的完整路径是/data/system/users/<user_
id/wallpaper。所以,设置静态壁纸的本质工作就是将壁纸的位图写入上述路径之后,然后
ImageWallpaper从这个路径解码这幅位图,并将其绘制在壁纸窗口的Surface之上。
倘若设备启用了加密文件系统,那么存储壁纸位图的路径则变为/data/secure/system/
users/<user_id>/wallpaper。
8.3.2静态壁纸位图的设置
于是问题随之而来,是谁将壁纸位图写入这个路径的呢?当这个路径中所保存的位图被
更新时lmageWallpaper如何得到通知并重新载入然后重绘呢?这就要从设置静态壁纸的API
的实现说起。
不似动态壁纸的设置需要签名级系统权限,任何一个申请了普通权限android,permission.
SET_WALLPAPER的应用程序都可以通过WallpaperManager相关的接口进行静态壁纸的设
置。WallpaperManager 提供了setBitmap()、setResource()以及 setStream()这三个方法进行静
态壁纸的设置,其区别仅在于接受参数的类型不同,开发者可以根据实际情况自由选择。以
setBitmap()为例:
[WallpaperManager.java->WallpaperManager.setBitmap()]
public void setBitmap(Bitmap bitmap)throws IOException {
......
try{
//①通过WallpaperManagerService.setWallpaper()方法获取一个文件描述符
ParcelFileDescriptor fd = sGlobals.mservice.setWallpaper(null);
if(fd == null){
return;
}
FileoutputStream fos = null;
try{
//②将Bitmap写入这个文件描述符所指向的文件中
fos = new ParcelFileDescriptor.AutoCloseoutputStream(fd);
bitmap.compress(Bitmap.CompressFormat.PNG,90,fos);
]finally{
//③关闭文件
if(fos != null){
fos.close();
}
}
}catch (RemoteException e){......}
}
看来,把位图数据写入文件的操作是由进行壁纸设置的进程完成的。WallpaperManagerService
通过setWallpaper()方法为壁纸设置进程提供了一个文件描述符,然后壁纸设置进程将
位图数据写入这个描述符中。很显然,通过setWallpaper()与getWallpaper()两个方法所返回
的文件描述符必然指向同一个文件。所以可以断定WallpaperManagerService在静态壁纸的设
置过程是以一个中间人的身份存在的。参考WallpaperManagerService.setWallpaper()方法的实
现以印证这一想法:
[WallpaperManagerService.java->WallpaperManagerService.setWallpaper()]
public ParcelFileDescriptor setWallpaper(String name){
//要求调用者必须申请android.permission.SET_WALLPAPER权限
checkPermission (android.Manifest.permission.SET_WALLPAPER);
synchronized(mLock){
/*①与getWallpaper()方法一样,必须确定设置壁纸的用户,
并根据用户的ID获取对应的WallpaperData */
int userld = UserHandle.getCallinguserId();
WallpaperData wallpaper = mwallpaperMap.get(userId);
.......
try{
//②updatewallpaperBitmapiocked()将创建一个文件描述符
ParcelFileDeacriptor pfd = updateNallpaperBitmapLocked(name,wallpaper);
if (pfd! = null){
//③设置imagewallpaperpending为true
wallpaper.imageWallpaperPending = true;
}
return pfd;
}finally{.....}
}
}
再看updateWallpaperBitmapLocked()的实现:
[WallpaperManagerService.java->WallpaperManagerService.setWallpaper()]
ParcelFileDescriptor updatewallpaperBitmaplocked(String name,WallpaperData wallparer){
try{
//①文件路径的计算方法与getWallpaper()方法一模一样
File dir = getWallpaperDir(wallpaper.userId);
......
File file = new File(dir,WALLPAPER);
//②创建一个可供写入的文件描述符并返回
ParcelFileDescriptor fd=ParcelpileDescriptor.open(file,MODE_CREATE | MODE_READ_WRITE);
.......
return fd;
}catch(FileNotgoundException e){......}
}
8.3.3连接静态壁纸的设置与获取——WallpaperObserver
从这两个方法的实现来看,只能说明WallpaperManagerService为壁纸设置者与lmageWallpaper
提供了同一个文件的路径,使得位图数据可以从壁纸设置者传递到ImageWallpaper中。
但是上述设置静态壁纸的过程ImageWallpaper 并没有参与,那么ImageWallpaper如何知
道刚刚进行了静态壁纸的设置呢?原来在WallpaperManagerService中还存在着一个对静态壁
纸来说很重要的组件:WallpaperObserver。
WallpaperObserver 继承自FileObserver。这个基于inotify机制的FileObserver是Android
框架提供的一个工具类,用于监听文件系统中所发生的文件创建、删除与修改等事件。
FileObserver的使用者可以监听一个文件夹或者一个文件,并在上述增删修改动作发生
时,抽象方法onEvent()便会得到回调并且可以根据其参数得知发生这些动作的文件路径。
对WallpaperManagerService来说,这个功能显得尤为有用。WallpaperObserver监听文件夹/
data/system/users/<user_id>/下的CLOSE_WRITE事件,于是,当静态壁纸设置者完成位图
的写入并关闭文件之后,WallpaperObserver的onEvent()便会得到通知。不难想到,在这个
onEvent()方法中,WallpaperManagerService会尝试通知ImageWallpaper进行壁纸的重新载入
与绘制。
WallpaperObserver除了CLOSE_WRITE之外还监听DELETE以及DELETE_SELF两
个事件。其中DELETE事件是指被监听的文件夹下任何文件被删除,而DELETE_SELF
则表示被监听的文件夹被删除。因为作为静态壁纸的位图数据被保存在文件系
统之中,而在root权限下删除或修改这个文件易如反掌,因此它并没有受到足够有
效保护。监听DELETE以及DELETE_SELF两个事件的目的就是应对位图被删除的
情况。
参考onEvent()方法的实现:
[WallpaperManagerService.java->WallpaperObserver.onEvent()]
public void onEvent(int event,String path){
.......
synchronized (mLock){
//①获取FileObserver上报的发生CLOSE_WRITE事件的文件路径
File changedFile=new File(mWallpaperDir,path);
/*mWallpaperFile是Wallpaperobserver的一个成员交量,在Wallpaperobserver的
构造函数中它被赋值指向/data/system/users/<user_id>/wallpaper。
因为/data/system/users/<userid>/文件夹下还存在其他与当前用户有关的数据,因此
这个判断的目的是排除发生在无关文件上的事件*/
if(mWallpaperFile.equals(changedFile)){
......
/*②确定是否通知ImageWallpapex\r进行壁纸更新。其条件有三:
1.wallpaperCompoenent == null意味着目前系统没有任何壁纸
2.event != CLOSE_WRITE意味着位图文件被删除
3.imagewallpaperPending为true。在WallpaperManagerservice.setWallpaper()
会将这个值设置为true。满足这个条件表示这个CLOSE_WRITE事件由设置静态壁纸所
引发。所以,私自打开存储壁纸位图的文件并写入数据并不会引发静态壁纸的切换*/
if(mWallpaper.wallpaperComponent == null || event != CLOSE_WRITE
|| mwallpaper.imagewallpaperpending){
if(event==CLOSE_WRITE){
mWallpaper.imageWallpaperPending =false;
}
//③bindWallpaperComponentLocked()方法将会启动IMAGE_WALLPAPER
bindWallpaperComponentLocked(IMAGE_WALLPAPER,true,false,mwallpaper,null);
/*保存壁纸的状态。以便系统重启或发生用户切换时
可以恢复到目前的壁纸设置*/
saveSettingsLocked(mWallpaper);
}
}
}
}
这个方法的核心工作是前面所介绍的bindWallpaperComponentLocked()方法。
注意bindWallpaperComponentLocked()方法的工作是启动一个新的壁纸并终止旧有壁纸的运行,
所以它会在lmageWallpaper中创建一个新的DrawableEngine用以渲染新的壁纸位图。换句
话说,Android并没有以某种方式通知现有的DrawableEngine重新载入位图并重绘,而
是重新启动一个新的DrawableEngine对象,并将旧有的DrawableEngine对象销毁。新的
DrawableEngine对象通过调用它的updateWallpaperLocked()从位图文件中加载新的位图,并
通过drawFrameLocked()方法将其绘制在壁纸窗口之上。
总而言之,设置静态壁纸的完整过程如下:
口 静态壁纸的设置者通过WallpaperManagerService.setWallpaper()获取一个文件描述符。
口 静态壁纸的设置者将作为壁纸的位图写入这个描述符所指向的文件中。
口 WallpaperManagerService中的WallpaperObserver得到文件写入完毕的通知,然后通过
bindWallpaperComponentLocked()方法绑定壁纸服务ImageWallpaper。在这个过程中,
新的DrawableEngine在ImageWallpaper中被创建。
口 新建的DrawableEngine 通过WallpaperManager.getWallpaper()获取存储位图的文件描
述符,然后将其解码为一个Bitmap对象并保存在mBackground成员中。
口 DrawableEngine通过drawFrameLocked()方法将mBackground绘制在壁纸窗口的
Surface上。
相对于动态壁纸,静态壁纸的特殊性在于WallpaperManager 及WallpaperManagerService
为静态壁纸的设置提供了一套机制,使得静态壁纸设置者可以将位图传递给ImageWallpaper。
虽然静态壁纸与动态壁纸的设置方式与流程存在较大不同,但是它们也有着高度的
统一性。首先,二者的本质都是在WallpaperService之中运行一个Engine对象。其次,虽
然WallpaperManagerService为动态壁纸以及静态壁纸提供不同的接口进行设置,但是
它们的设置流程都将会合到bindWallpaperComponentLocked()方法。再者,无论是调用
setWallpaperComponent()还是setWallpaper(),都会在对应的壁纸服务内创建新的Engine对象。
至此,对WallpaperManagerService以及壁纸实现相关内容的讨论告一段落。很显然,壁
纸窗口的存在意义是为其他窗口提供背景,所以它注定会对其他窗口的Z序、可见性等状态
有很强的依赖性。在处理这些依赖关系上,WallpaperManagerService很显然是无能为力的。于
是这就需要窗口的管理者WMS出场了。
8.4 WMS对壁纸窗口的特殊处理
壁纸存在的意义是为其他窗口提供背景。当一个窗口希望壁纸作为其背景时,可以将
FLAG_SHOW_WALLPAPER标记加入其flags中。当WMS检测到处于显示状态的窗口声
明这一标记时,会将壁纸窗口衬于此窗口之下,于是用户便可以透过此窗口的透明区域看
到壁纸窗口的内容。所以,与其他窗口不同,壁纸窗口在窗口列表中的Z序与声明FLAG_
SHOW_WALLPAPER标记的窗口紧密联系。另外,动态壁纸往往十分消耗计算资源,WMS
必须确保当用户看不到壁纸的时候能够通过onVisiblityChanged()回调将这个状态通知动态壁
纸,使后者立即停止任何消耗资源的操作。再者,申请这个标记的窗口希望在进行窗口动画
时,壁纸窗口能够同步地进行动画,就好像壁纸是此窗口的一部分一样。于是在WMS的窗
口动画子系统中也存在着对壁纸窗口的特殊处理。
壁纸窗口在WMS中的特殊行为主要体现在三个方面:壁纸窗口的Z序、壁纸窗口的可
见性以及壁纸窗口的动画。
8.4.1 壁纸窗口Z序的确定
既然壁纸窗口要衬托在声明了FLAG_SHOW_WALLPAPER标记的窗口之下,因此壁纸
窗口的Z序取决于声明这个标记的窗口的Z序。在WMS中,声明这个标记的窗口称为壁纸目标,
并保存在WMS的一个名为mWallpaperTarget的WindowState类型的成员中。所以确定
壁纸窗口Z序的核心工作就是寻找这个壁纸目标。
寻找壁纸目标的工作可能很简单,也可能很复杂。最简单的情况是系统中仅有一个窗口
声明FLAG_SHOW_WALLPAPER标记,并且这个窗口目前是用户可见的,在这种情况下它
就是壁纸目标。稍微复杂一些的情况是系统中有多个窗口声明FLAG_SHOW_WALLPAPER
标记,此时拥有最高Z序并且用户可见的窗口会成为壁纸目标。而更加复杂的情况则是有多
个窗口声明FLAG_SHOW_WALLPAPER标记,同时它们还在进行窗口动画,此时便需要在
这些窗口之间进行相对复杂的选择策略。
选择壁纸目标并调整壁纸窗口Z序的策略位于WMS.adjustWallpaperWindowsLocked()方
法中。每当窗口列表发生变化,例如有窗口添加或移除都有可能使得壁纸窗口的Z序发生变
化,此时WMS便会调用这一方法重新计算壁纸目标并调整壁纸窗口在窗口列表中的位置,
并在此之后通过调用assignLayersLocked()方法刷新窗口列表中每个窗口的layer值从而调整
它们的Z序。
这个方法的内容较多,为了便于分析可以将它的工作内容分为两个方面。
第一个是查找壁纸目标窗口,而第二个则是将壁纸窗口移动到窗口目标下。
第一个工作又可以分为两子阶段:
口 首先通过在窗口列表中查找FLAG_SHOW_WALLPAPER标记初步确定一个壁纸目
标。这个初步确定的壁纸目标必须满足两个基本条件:声明上述标记,并且用户可见。
口 如果旧的壁纸目标与初步确定的新壁纸目标这两个窗口都处于动画过程中,WMS会比
较这两个目标窗口的Z序,并用mLowerWallpaperTarget 以及mUpperWallpaperTarget
两个成员记录这两个窗口。在这种情况下,虽然真正的壁纸目标mWallpaperTarget可
能是它们二者之一,但是壁纸窗口却被放置在mLowerWallpaperTarget之下。当二者
之一停止动画之后,再次调用adjustWallpaperWindowsLocked()时,会跳过这个阶段,
从而使得壁纸窗口被正确地放置在mWallpaperTarget下。
第二个工作同样也可以分为两个子阶段:
口 根据第一个阶段所确定的壁纸目标计算壁纸窗口在窗口列表中的位置。正常情
况下,壁纸窗口的位置应该位于壁纸目标mWallpaperTarget所指定的窗口下。不
过也有例外。一是当新旧两个壁纸目标都处于动画状态时,壁纸窗口的位置
以mLowerWallpaperTarget所指定的窗口为准。二是当目标窗口拥有父窗口或
STARTING窗口时,壁纸窗口会位于所有这些与目标窗口有关的窗口之下。
口 将壁纸窗口移动到这一位置上。
本节将分别对这4个阶段进行介绍。
1.初步确定壁纸目标
初步确定壁纸目标的原则是在窗口列表中寻找第一个声明FLAG_SHOW_WALLPAPER
标记且用户可见的窗口。不过这个策略会根据候选窗口的动画情况进行细微调整。
[WindowManagerService java-->WindowManagerService.adjustWallpaperWindowsLocked()]
int adjustwallpaperwindowsLocked(){
...... //局部变量的声明与初始化
//从上到下依次遍历所有窗口
while(i > 0){
i--;
w = windows.get(i);
.......
if ((w.mAttrs.flags & FLAG_SHOW_WALLPAPER) != 0 && w.isReadyForDisplay()
&&(mWallpaperTarget==w || w.isDrawnLw())){
// ①如果声明FLAG_SHOW_WALLPAPER标记,并且用户可见,那么它作为壁纸目标的候选
foundW = w;
foundI = i;
if (w == mwallpaperTarget && w.mMinAnimator.isAnimating()){
/*②如果候选窗口是旧的壁纸目标,并且处在动画过程中,
那么将继续向下查找可能成为壁纸目标的窗口*/
continue;
}else{
//③否则终止查找,确定此窗口为窗口目标的候选
break;
}
}
......
}
......
}
在这个阶段中,候选的壁纸目标会保存在foundW局部变量中,并且它在窗口列表中的
索引会被保存在foundl中。这两个变量将会是后续阶段的工作依据。后面将会以跟踪foundW
与foundl的变化作为主线,讨论adjustWallpaperWindowsLocked()的工作原理。
另外,w == mWallpaperTarget && w.mWinAnimator.isAnimating()这个条件判断可能令人费
解。adjustWallpaperWindowsLocked()下一个阶段中会检查旧有的壁纸目标与新壁纸目标的动
画情况,并在两个目标都进行动画时设置mLowerWallpaperTarget与mUpperWallpaperTarget。
于是,如果候选窗口并没有进行动画,则下一阶段的处理便不需要进行,那么候选目标就是
最终的壁纸目标,所以adjustWallpaperWindowsLocked()会终止对候选窗口的查找。否则,就
必须找到mWallpaperTarget之外的一个候选窗口作为新的壁纸目标foundW,并在下一阶段在
二者之间选择赋值给mLowerWallpaperTarget 以及mUpperWallpaperTarget。
2.设置mLowerWallpaperTarget以及mUpperWallpaperTarget
接下来看adjustWallpaperWindowsLocked()如何设置mWallpaperTarget、mLowerWallpaperTarget
以及mUpperWallpaperTarget。
[WindowManagerService,java->WindowManagerService.adjustWallpaperWindowsLocked0]
int adjustwallpaperWindowstocked(){
if(mWallpaperTarget!=foundW
&& (mLowerWallpaperTarget == null || mlowerWallpaperrarget != foundW)){
......
//重置mLowerWallpaperTarget以及mUpperWallpaperTarget
mLowerWallpaperTarget = null;
mUpperWallpaperTarget = null;
/*①设置mWallpaperTarget为上一阶段所找到的新的壁纸目标。
旧的壁纸目标会被保存在oldW中*/
windowstate oldW = mWallpaperTarget;
mWallpaperTarget = foundw;
if(foundw!=null && oldW != null){
//旧的壁纸目标是否在动画中?
boolean oldAnim = oldw.mMinAnimator.mAnimation != null
||(oldw.mappTokenl=null && oldw.mAppToken.mAppAnimator.animation != null);
//新的壁纸目标是否在动画中?
boolean foundAnim = .........;
/*若二者同时处在动画状态中,则设置mLowerWallpaperTarget为位置较低的窗口,
并设置mUpperWallpaperTarget位置较高的窗口*/
if(foundAnim && oldAnim){
int oldI = windows.indexof(oldW);
......
if(oldI >= 0){
......
if(foundI>oldI){
/*②新的壁纸目标窗口位于旧的壁纸目标窗口之上。所以设置
mUpperWallpaperTarget为新的壁纸目标,mLowerWallpaperTarget为旧的壁纸
目标。另外,用于确定壁纸窗口插入位置的foundW与foundI也会被设置为
mLowerWallpaperTarget*/
mOpperWallpaperTarget = foundM;
mLowerwallpaperrarget = oldw;
foundW = oldW;
foundI = oldI;
}else{
/*③新的壁纸目标窗口位于旧的壁纸目标窗口下。所以设置mUpperWallpaperTarget
为旧的壁纸目标,mLowerWallpaperTarget为新的壁纸目标*/
mUpperWallpaperrarget = oldx;
mLowerlallpaperTarget = founaM;
}
}
}
}
//其他情况下,保持foundw的状态不变
}else if (mLowerWallpaperTarget!=null){
/*如果mLowerWallpaperTarget或mUpperWallpaperTarget停止动画,
则将这两个变量重置为null*/
......
}
}
这个阶段主要是为了处理新旧两个壁纸目标窗口都处于动画状态这种特殊情况的。处
于这种情况下,用于指示壁纸窗口添加位置的foundW以及foundl会被设置为mLowerWallpaperTarget
所指定的窗口及其在窗口列表中的索引。于是真正的壁纸目标 mWallpaperTarget
有可能会与壁纸窗口的实际位置发生分离,即中间隔着mLowerWallpaperTarget。而这种情况
是很少见的,绝大多数情况下,mLowerWallpaperTarget 以及mUpperWallpaperTarget是null,
而mWalpaperTarget会被设置为foundW。
mLowerWallpaperTarget以及mUpperWallpaperTarget表示同时有两个用户可见的壁纸
目标。而壁纸窗口只有一个。那么谁说了算呢?WMS的策略是,壁纸窗口的位置与
可见性是mLowerWallpaperTarget说了算。而壁纸的Offset则
是mWallpaperTarget说了算。至于壁纸窗口的动画,则是谁说了都不算——这种情况
下壁纸窗口将会保持静止。
经过这一阶段,mWallpaperTarget被设置为foundW,而foundW与foundI又根据动画情
况被设置为mWallpaperTarget 或mLowerWallpaperTarget。
3.确定壁纸窗口在窗口列表中的位置
接下来便根据foundW确定壁纸窗口在窗口列表中的最终位置。在这个过程中会同时计
算壁纸窗口的可见性。关于可见性的详细讨论请参考8.4.2节。
[WindowManagerService java->WindowManagerService.adjustWallpaperWindowsLockcd()]
int adjustWallpaperwindowsLocked(){
//壁纸窗口可见的最基本要求是找到一个壁纸目标foundW
boolean visible = foundw != null;
if (visible){
//根据foundW的状态最终确定壁纸是否可见
visible = iswallpaperVisible{foundw);
/*①计算壁纸在动画过程中的layer调整。
在第4章与窗口Layer计算有关的内容介绍过,Activity执行动画时会通过Animation.
set2Adjustment()方法临时调整隶属于Activity的窗口的layer。壁纸窗口作为其目标窗口的一个
附属,当目标窗口存在layer调整时,自然需要跟着调整自己的layer。WMS将这一调整存储在
mWallpaperAnimLayerAdjustment成员中注意,仅当mLowerWallpaperTarget为null时
才会这样设置。因为此时有两个壁纸目标正在进行动画,所以壁纸窗口无法得知使用那个目标的
layer调整。于是索性不再对壁纸的layer进行调整*/
mNallpaperAnimLayerAdjustment =
(mLowerwallpaperrarget == null && foundw.mAppToken != null)
?foundw.mAppToken.mAppAnimator.animlayerAdjustment:0;
/*②maxLayer是PhoneWindowManager所允许壁纸拥有的layer上限(不含)。
这一上限与状态栏的layer相同。即壁纸无论如何不能覆盖状态栏与导航栏*/
final int maxlayer=mpolicy.getwaxwallpaperlayer()
*TYPE_LAYER_MULTTPLTER
+TYPE_LAYER_OFESET;
/*③确保壁纸位于 layer上限、目标窗口、目标窗口的父窗口以及STARTTNG窗口之下。离开这个循
环之后,foundW将指向这些窗口中位置最靠下的那个窗口,而foundI 则指向此窗口在列表中的位置*/
while(foundI>0){
WindowState wb=windows.get(foundI-1);
/*下面这个判断的合理性建立在如下事实之上:窗口、窗口的父留口以及
其所展Activity的STARTING窗口紧邻在一起*/
if (wb.mBasetayer < maxtayer &&
wb.mAttachedwindow != foundN &&
(foundl.mAttachedWindow==null ||
wb.mAttachedWindow!=foundW.mattachedwindow) &&
(wb.mAttrs.type != TYPE_APELICATION_STARTING 11
foundW.mToken == null || wb.mToken != foundw.mToken)){
break;
}
foundN = wb;
foundI --;
}
}
if(foundW==null && topCurW!=null){
/*倘若自始至终没能找到founaw,即没有哪个窗口声明ELAGSHOW_WALLPAPER标记则foundn与
foundr被设置为topCurw与topCurI+1。topCurN与topCurI在第一阶段中被赋值,用于指示壁
纸窗口在adjustwallpaperwindowslocked()执行之前的位置。事实上仅当登纸窗口位于窗口
列表底部时,topCurw才不为null。因此,这里的肽值的意思是保持壁纸窗口位于窗口列表的底
部。倘若foundN与topCurw都为null,foundI则保持为初始的0,所以即便无法进入这个分支
分支进行戴值,在foundw为nu11时也会因为foundI为0而使得壁纸窗口从当前位置被移动到窗口
列表的底部*/
foundw = topCurW;
foundI = topCurI+1;
}else{
//④foundw此时被设置为登纸窗口即将插入的位置上的那个窗口
foundn=foundI>0? windows.get(foundI-1):null;
}
/*从mWallpaperTarget中获取壁纸的Offaet并保存在WMS的成员变量之中。
随后这些Offset信息将会被发送给壁纸窗口*/
if (visible){
if(mwallpaperrarget.mwallpaperx >= 0){
mLastwallpaperx=mwallpaperTarget.mWallpaperx;
mLastWallpaperxstep=mWallpaperTarget.mWallpaperXStep;
}
if(mWallpaperTarget.mwallpaperY>=0){
mLastWallpapery=mwallpaperTarget.mwallpaperY;
mLastWallpaperystep-mwallpaperrarget.mwallpaperystep;
}
}
}
经过这一阶段的计算,foundl-1是最终壁纸窗口在窗口列表中的位置。
4.移动壁纸窗口到指定的位置
接下来便是移动壁纸窗口到foundl-1的位置。虽说在正常情况下系统中只有一个壁纸
窗口。但是也有例外。WallpaperManagerService.bindWallpaperComponentLocked()的实现是
首先启动新壁纸,然后终止旧壁纸。而且壁纸的启动与终止分别在对应壁纸服务的进程中
异步进行。于是在一个较短的时间里,系统中可能会存在两个壁纸窗口。所以
adjustWallpaperWindowsLocked()在进行壁纸窗口的移动时必须将这种情况考虑在内。
参考其实现:
[WindowManagerService.java->WindowManagerService.adjustWallpaperWindowsLocked()]
int adjustWallpapermindowsLocked(){
int curfokenIndex = mWallpaperTokens.size();
//遍历所有的WallpaperToken
while (curTokenIndex >0 ){
curTokenIndex--;
windowroken token = mlallpaperrokens.get(currokenIndex);
......
int curwallpaperIndex = token.windows.size();
//遍历WallpaperToken下所有的壁纸窗口
while(curWallpaperIndex > 0){
curWallpaperIndex--;
hindowstate wallpaper = token.windows.get(curWallpaperIndex);
......
//①首先将壁纸窗口从窗口列表中移除
int oldIndex = windows.indexof(wallpaper);
if(oldIndex>=0){
windows.remove(oldIndex);
mWindowsChanged = true;
if (oldIndex<foundI){
foundI --;
}
}
//②再将壁纸插入foundI所指定的位置上
windows.add(foundI,wallpaper);
mWindowsChanged = true;
changed |= ADJUST_WALLPAPER_ LAYERS _CHANGED;
}
}
}
至此,壁纸窗口的位置调整完毕。根据第4章的知识可知,由于窗口列表的窗口位置
发生变化,于是在adjustWallpaperWindowsLocked()方法结束之后,必须尽快调用
WMS.assignLayersLocked()为所有窗口重新计算layer值。
总体来说,在正常情况下,mWallpaperTarget是窗口列表中从上往下第一个用户可见并且
声明FLAG_SHOW_WALLPAPER标记的窗口,而壁纸窗口则会被移动到mWallpaperTarget窗
口或其父窗口、STARTING窗口之下。而在新旧两个壁纸目标都存在动画时,那么壁纸窗口
则会被移动到mLowerWallpaperTarget窗口或其父窗口、STARTING窗口之下。
8.4.2 壁纸窗口的可见性
壁纸窗口的可见性的计算处于WMS.adjustWallpaperWindowsLocked()的第三个阶段中。
从这个阶段的代码可知,壁纸窗口可见的第一个条件就是在系统之中存在一个壁纸目标。然
后WMS.isWallpaperVisible()方法会根据这个壁纸目标的状态做出壁纸是否可见的最终决定。
参考这一方法的实现:
[WindowManagerService.java->WindowManagerService.adjustWallpaperWindowsLocked()]
final boolean isWallpaperviaible (Windowstate wallpaperrarget){
return(wallpaperTarget!=null
&&(!wallpaperTarget.mObscured ||
(wallpaperTarget.mAppToken != null
&&wallpaperlarget.mAppToken.mAgpAnimator.animation!=null)))
|| mUpperWallpaperTarget!=null
|| mLowerlallpaperTarget!=null;
}
显而易见,只要满足三个条件之一壁纸即为可见的:
口 mUpperWallpaperTarget 或mLowerWallpaperTarget不为nul。这表示目前存在着新旧
两个壁纸目标同时进行动画的情况。
口 wallpaperTarget不被另一个全屏窗口遮挡。即这个窗口部分或者全部用户可见。此时
自然需要显示壁纸。
口 wallpaperTarget隶属于一个Activity 而这个Activity正在执行动画。Activity执行动画
时会附带layer的调整,于是即便布局结果显示wallpaperTarget被另一个全屏窗口所
遮挡,但是在实际显示的过程中由于layer 调整的存在可能使得 wallpaperTarget显示
在其他窗口之上,所以此时仍然需要显示壁纸。
isWallpaperVisible()的参数wallpaperTarget并不是mWallpaperTarget,而是foundW。
isWallpaperVisible()方法的返回值保存在adjustWallpaperWindowsLocked()方法中,并在
最后一个阶段进行壁纸窗口移动之前,通过调用WMS.dispatchWallpaperVisibility()方法将壁
纸的可见性通知给WallpaperService中的Engine对象。
[WindowManagerService.java->WindowManagerService.dispatchWallpaperVisibility()]
void dispatchwallpaperVisibility (final Windowstate wallpaper,final boolean visible){
if (wallpaper.mWallpaperVisible!=visible){
/*①将wallpaper的可见性保存在壁纸窗口的Windowstate.mWallpaperVisible中。
这个看似不起眼的设置其实非常重要。在动画系统中,WindowstateAnimator的
prepareSurfacelocked()方法中会检查这个成员的取值。倘若为false,则会调用
WindowStateAnimator.hide()方法,进而调用Surface.hide()将壁纸窗口隐藏。相反,会调用
WindowStateAnimator.showSurfaceRobustlyLocked()进而调用surface.show()使壁纸窗
口重新可见*/
wallpaper.mNallpaperVisible = visible;
try{
/*②通过IWindow.dispatchAppVisibility()方法
将可见性传递给Wallpaperservice下的Engine对象*/
wallpaper.mClient.dispatchAppVisibility(visible);
}catch (RemoteException e){}
}
}
dispatchWallpaperVisibility()合理地使用IWindow.dispatchAppVisibility()回调将壁纸的可
见性通知给 WallpaperService里的Engine对象。Engine对象的mWindow成员则在接收到这个
回调后将其解释为onVisibilityChanged()。Engine对象可以根据这一回调重启或终止画面的绘
制工作。
由第4章的知识可知,WMS中布局子系统的任何变化都会引发动画系统进行一帧的处理。
而在这一帧的处理之中,每个窗口的WindowState/Animator.prepareSurfaceLocked()方法都会被
调用以更新Surface的状态。壁纸窗口也不例外。在prepareSurfaceLocked()方法中可以看到如
下处理:
[WindowStateAnimatorjava-->WindowStateAnimator.prepareSurfaceLocked()]
public void preparesurfacelocked(final boolean recoveringMemory){
if (mIsWallpaper && !mwin.mwallpapervisible){
/*①本窗口是壁纸窗口,并且Windowstate.mWallpaperVisible为false,
于是通过Hide()方法将壁纸窗口的Surface隐藏*/
hide();
}else if (w.mattachedHidden ll!w.isReadyEorDisplay()){
.....
/*②本窗口不是壁纸窗口,但是本窗口有可能是壁纸目标,因此调用windowanimator的
hideWallpapersLocked()尝试隐藏璧纸*/
mAnimator.hidewallpapersLocked(w,true);
.....
}else if (.......){
//③否则设置Surface为可见
}
}
倘若经过WMS.dispatchWallpaperVisibility()的设置,壁纸窗口的WindowStateAnimator.
prepareSurfaceLocked()就会对其所设置的mWallpaperVisible做出反应,隐藏或者显示壁纸的
Surface。
倘若一个非壁纸窗口被隐藏时,则会通过WindowAnimator.hideWalpapersLocked()尝
试隐藏壁纸。hideWallpapersLocked()会检查此窗口是否是mWallpaperTarget,如果是,并
且mLowerWallpaperTarget为null即没有进行动画,则会遍历每一个壁纸窗口并为其调用
WindowStateAnimator.hide()将壁纸隐藏。
所以说,壁纸的可见性有两层含义,第一层是相对WallpaperService中的Engine对象而
言,壁纸的可见性决定了Engine对象是否进行壁纸内容的绘制工作。第二层则是对WMS中
的窗口管理而言,壁纸的可见性会使得壁纸窗口的Surface被隐藏或显示。
壁纸的可见性不同于常规意义上的窗口可见性。常规意义上的窗口可见性的变化伴随
着Surface的创建与销毁,而壁纸的可见性只会使得Surface显示或隐藏。这一区别使得显
示或隐藏壁纸的效率变得非常高,但是带来的负面影响则是Surface的内存永远处于占用
状态。
8.4.3 壁纸窗口的动画
在默认情况下,当壁纸目标窗口发生动画时,壁纸窗口也会随着目标窗口产生同步的动
画。这一效果的实现位于WindowStateAnimator.computShownFrameLocked()方法中。参考相
关代码:
void computeShownErameLocked(){
......
/*①壁纸窗口会额外地 应用 来自壁纸目标窗口的变换,前提是mLowerWallpaperTarget为null。这个
前提其实很简单,因为mLowerwallpaperTarget不为null时表示有两个壁纸目标在进行动画。这种情
况下些纸窗口无法决定使用哪一个目标的动画变换,于是壁纸窗口便保持了静止不动*/
if(mIsWallpaper && mAnimator.mLowerwallpaperTarget == null
&& mAnimator.mNallpaperTarget != null){
/*不要为wallpaperAnimator这个名字所迷惑。
这个WindowstateAnimator其实是壁纸目标窗口的Animator*/
final WindowStateAnimator wallpaperAnimator
=mAnimator.mWallpaperTarget.mwinAnimator;
/*②设置attachedrrangformation为壁纸目标窗口的动画变换,前提是壁纸目标窗口动画的
getDetachedwallpaper()返回为falae。从这个行为来看,壁纸窗口在动画过程中扮演了壁纸目
标窗口子窗口的角色*/
if(wallpaperAnimator.mHasLocalTransformation &&
wallpaperAnimator.mAnimation != null &&
!wallpaperAnimator.mAnimation.getDetachWallpaper()}{
attachedTransformation = wallpaperAnimator.mTransformation;
}
/*③设置appTransformation为壁纸目标窗口所属Activity的动画变换,
前提是Activity的动画的getDetachedWallpaper()返回为falae*/
final AppwindowAnimator wpAppAnimator=mAnimator.mWpAppAnimator;
if(wpAppAnimator != null && wpAppAnimator.hasTransformation
&& wpAppAnimator.animation != null
&&!wpAppAnimator.animation.getDetachWallpaper()){
appTransformation=wpAppAnimator.transformation;
}
}
/*剩下的代码会按照常规窗口的方式将attachedTransformation与appTrans formation集成到
tmpMatrix中,并由此计算出Surface最终的透明度、位置以及DaDx、DtDx、DaDy以及DtDy*/
......
}
这段代码体现了如下几个关键点:
口 一般情况下,壁纸窗口会随着其目标窗口进行动画变换。
口 如果不希望壁纸窗口随着目标窗口进行动画变换,可以通过调用窗口动画的
Animation.setDetachWallpaper()方法并传递参数false实现。
口 当mLowerWallpaperTarget存在时,壁纸窗口会保持静止。
其实能够作为其他窗口背景的除了本章所介绍的壁纸之外,还有一种特殊的背景。窗口
可以通过Animation.setBackgroundColor()方法设置一个动画背景色。WindowAnimator在进行
动画的过程中倘若发现存在一个窗口指定了动画背景色,就会在此窗口之下放置一个Surface
充当此窗口在动画过程中的背景。目前WindowAnimator只会使用背景色的透明度分量,其他
的RGB分量会被忽略。
这个动画背景和壁纸之间存在着一个小小的冲突,倘若指定了动画背景色的窗口是
mWallpaperTarget、mLowerWallpaper Target以及mUpperWallpaperTarget三者之一,这块黑色的
Surface 就会将壁纸窗口遮挡,在用户看来就仿佛壁纸消失了一样。于是会在添加这块Surface
之前检查指定动画背景色的窗口是否是上述壁纸目标窗口。如果是,则会将Surface放置在壁
纸窗口之下。参考代码如下:
[WindowAnimator java-->WindowAnimator.updateWallpaperLocked()]
private void updatewallpaperlocked(int displayId){
......
//遍历所有的WindowstateAnimator,从中查找指定动画背景的窗口
for(int i=winAnimatorlist.size()-1;i>=0;i--){
WindowStateAnimator winAnimator=winAnimatorlist.get(i);
//首先检查窗口动画是否设置了动画背景色
if(winAnimator.mAnimating){
if(winAnimator.mAnimation!=null){
final int backgroundcolor
= winAnimator.mAnimation.getBackgroundColor();
if(backgroundColor!=0){
/*①将指定动画背景的窗口的WindowStateAnimator保存在windowAnimationBackground中。
稍后将会把作为动画背景的Surface放置在这个WindowStateAnimator所属的窗口下。
另外,从这个判断可以看出,倘若有多个窗口指定动画背景色,那么将以layer值最小
的,也就是位置最靠下的那个窗口为准*/
if(windowAnimationBackground =null ||(winAnimator.mAnimtayer<
windowAnimationBackground.mhnimLayer)){
windowAnimationBackground = winAnimator;
windowAnimationBackgroundcolor = backgroundcolor;
}
}
}
}
//然后检查窗口所属的Activity的动画是否指定背景色。其判新方法与上述代码完全一致
......
}
/*完成上述循环之后,windowAnimationBackgroudnColor指定动画背景色。
而windowAnimationBackground指定需要添加背景Surface的窗口*/
if(windowAnimationBackgroundColor!=0){
//②animLayer正常情况下的取值是windowAnimationBackground窗口的layer
int animlayer = windowAnimationBackground.mAnimLayer;
WindowState win = windowAnimationBackground.mwin;
if(mwallpaperTarget==win
|| mLowerWallpaperrarget == win || mUpperwallpaperfarget == win){
/*③倘若windowAnimationBackgrroumd的窗口是一个壁纸目标,于是就需要将animLayer修正
为壁纸窗口的显示layer。于是遍历所有的窗口列表并在其中寻找壁纸窗口,然后将
animLayer修正为壁纸窗口的layer*/
final int N=winAnimatorList.size();
for (inti-0;i<N;i++){
WindowStateAnimator winAnimator=winAnimatorList.get(i);
if(winAnimator.mIsWallpaper){
animLayer=winAnimator.mAnimayer;
break;
}
}
}
//④最后将作为动画背景的Surface显示在animLayer-LAYER_OFFSET_DIM的位置上
if(windowanimationBackgroundsurface != null){
windowAnimationBackgroundSurface.show(mDw,mDh,
animlayer-WindowManagerService.LAYER_OFESET_DIM,
windowAnimationBackgroundcolor);
}
}else{
//若没有窗口指定动画背景色,则隐藏作为动面背景的Surface
if(windowAnimationBackgroundSurface!=null){
windowAnimationBackgroundsurface.hide();
}
}
}
updateWallpaperLocked()方法在WindowAnimator每一帧的处理中都会调用。它的主要工
作是遍历所有动画中的窗口,并为其显示或隐藏一块作为动画背景的Surface。倘若要求显示
动画背景的窗口是一个壁纸的目标,那么这块Surface会被放置在壁纸窗口之下。
8.4.4壁纸窗口总结
总的来说,WMS对壁纸窗口的特殊处理主要分为Z序的调整、可见性的计算与处理以及
壁纸动画三个方面。
壁纸窗口Z序的调整是其中最复杂且最重要的处理,其目的是让壁纸窗口能够正确地位
于希望壁纸作为背景的窗口的下方。其调整方法简单来说就是倘若有窗口声明 FLAG_SHOW_
WALLPAPER,则壁纸会位于这一窗口之下,并称为壁纸目标,否则壁纸会被放置在窗口列表
的底部。一个特殊情况是在两个壁纸目标窗口之间切换时,倘若它们都在执行动画,则z序
较低的那个目标窗口将是真正的壁纸目标。
壁纸可见性的处理目的是能够提高壁纸显示与隐藏的效率。当WMS认为壁纸不可见时,
会通过Surface.hide()将壁纸隐藏,否则通过Surface.show()将其显示。在隐藏与显示切换的过
程中不会发生Surface的创建与销毁工作,因而免去了壁纸服务中Engine的重绘过程,提高
了效率。WMS认为壁纸可见的基本条件是有窗口可以作为壁纸目标,在满足这个基本条件之
后,还需要能够满足三个条件之一:壁纸目标窗口未被遮挡,壁纸目标窗口所属的Activity正
在进行动画,或者新旧两个壁纸目标都在进行动画。
至于壁纸动画,默认情况下壁纸窗口会随着目标窗口同步进行动画。除非目标窗口的
Animation中设置了DetachWallpaper,或者新旧两个壁纸目标都在进行动画。
8.5本章小结
关于壁纸的内容就介绍到这里。希望读者经过本章的学习之后能够深入了解Android壁
纸的运行机制,体会WallpaperService中Engine对象如何通过WMS的接口直接创建与操
作窗口的方法,以及WallpaperManagerService如何通过窗口令牌对Engine对象创建 TYPE
WALLPAPER类型的窗口进行授权。理解WMS对壁纸窗口的特殊处理也很重要,因为这些
特殊处理说明除了使用BaseLayer以及SubLayer以外精细地调整窗口的Z序的方法,以及对
于那些需要频繁显示与隐藏的窗口所进行的优化。
Android壁纸使用了系统服务(管理者)加标准Android服务(实现者)的两层架构。当
系统希望某些系统级UI由第三方进行实现与扩展,同时又不希望给予第三方过多的操作窗口
的权限时,这种两层架构无疑是最佳选择。系统服务负责提供必要的系统级操作,而标准的
Android 服务可以在系统服务所规范的框架范围实现自由定制。第7章所介绍的SystemUI以
及输入法都采用了这种架构。在深入理解壁纸的运行机制之后,感兴趣的读者可以类比地研
究Android输入法的实现。
---------------------
作者:阿拉神农 来源:CSDN 原文:https://blog.csdn.net/Innost/article/details/47660645?utm_source=copy 版权声明:本文为博主原创文章,转载请附上博文链接!