一、壁纸系统的整体架构
壁纸系统,说白了就是 Android 桌面的「皮肤引擎」。我刚开始接触这块时,以为壁纸就是个背景图片,后来才发现——它背后藏着一整套显示层级、生命周期管理和裁剪流程。
先看一张整体架构图,帮你快速建立认知:
二、壁纸的显示层级
壁纸在 Android 系统中到底显示在哪一层?这个问题我当年也困惑过。其实,壁纸是作为 WindowManager 的一个特殊窗口存在的。
2.1 层级位置
在 Android 的窗口层级中,壁纸窗口位于 TYPE_WALLPAPER 层级,它的 z-order 在桌面图标之下、系统状态栏之上。具体来说:
- 底层:壁纸窗口(Wallpaper Window)
- 中间层:桌面 Launcher 的窗口
- 顶层:系统 UI(状态栏、导航栏)
嗯,这里要注意:壁纸窗口其实是一个 Surface,它由 SurfaceFlinger 合成。我调试过一个 bug,发现壁纸闪烁是因为 Surface 的 z-order 被其他应用窗口覆盖了——后来在 WindowManagerService 里加了层级校验才解决。
2.2 壁纸窗口的创建
壁纸窗口的创建流程大致如下:
// WallpaperManagerService 中的核心逻辑
WallpaperWindowToken token = new WallpaperWindowToken(...);
WindowState wallpaperWindow = new WindowState(session, token, ...);
wallpaperWindow.setType(WindowManager.LayoutParams.TYPE_WALLPAPER);
wallpaperWindow.setWallpaperZoomOut(1.0f); // 缩放比例
mWindowManager.addWindow(wallpaperWindow);
我个人习惯在 addWindow 之前先检查壁纸的 displayId,因为多屏场景下壁纸可能跑错屏幕——这个坑我踩过两次。
三、静态壁纸与动态壁纸
静态壁纸和动态壁纸,本质区别在于「谁来渲染内容」。
3.1 静态壁纸
静态壁纸就是一张图片。系统把它解码成 Bitmap,然后直接绘制到壁纸 Surface 上。流程很简单:
- 用户选择图片 → WallpaperPicker 裁剪
- 裁剪后的图片写入
/data/system/users/0/wallpaper - WallpaperManagerService 读取文件,解码为 Bitmap
- 通过
SurfaceControl.openTransaction()更新壁纸 Surface 的内容
Bitmap.Config.RGB_565,能省一半内存。我优化过一个低端机壁纸 OOM 问题,就是靠这个配置解决的。
3.2 动态壁纸
动态壁纸本质上是一个 Service,它运行在独立的进程中。系统通过 WallpaperService.Engine 来与动态壁纸通信。
核心接口如下:
public class MyLiveWallpaper extends WallpaperService {
@Override
public Engine onCreateEngine() {
return new MyEngine();
}
class MyEngine extends Engine {
@Override
public void onCreate(SurfaceHolder holder) {
// 初始化渲染线程
mRenderer = new WallpaperRenderer(holder.getSurface());
mRenderer.start();
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// 处理 Surface 尺寸变化
mRenderer.setSize(width, height);
}
@Override
public void onOffsetsChanged(float xOffset, float yOffset,
float xStep, float yStep, int xPixels, int yPixels) {
// 桌面滑动时的偏移回调
mRenderer.setOffset(xOffset, yOffset);
}
}
}
你想想看,动态壁纸的渲染完全由开发者控制,你可以用 OpenGL、Canvas 甚至 Vulkan 来画。但代价是——它一直在跑,耗电啊!
onOffsetsChanged 回调频率很高,桌面每滑动一帧都会触发。我曾经在项目里在这个回调里做了大量计算,结果导致桌面滑动卡顿——后来改用插值器做平滑处理才解决。
四、壁纸裁剪与设置流程
壁纸裁剪是 WallpaperPicker 的核心功能。用户选了一张图,系统得把它裁成合适的尺寸,再设置到桌面上。
4.1 裁剪流程
裁剪流程分三步走:
| 步骤 | 操作 | 关键类 |
|---|---|---|
| 1 | 用户选择图片,进入裁剪界面 | CropView |
| 2 | 根据屏幕尺寸计算裁剪区域 | WallpaperCropHelper |
| 3 | 执行裁剪并保存 | BitmapRegionDecoder |
裁剪的核心代码:
// WallpaperCropHelper 中的裁剪逻辑
public Bitmap cropWallpaper(Bitmap source, int targetWidth, int targetHeight) {
// 计算裁剪区域,保持宽高比
float scale = Math.max(
(float) targetWidth / source.getWidth(),
(float) targetHeight / source.getHeight()
);
int cropWidth = (int) (targetWidth / scale);
int cropHeight = (int) (targetHeight / scale);
int cropX = (source.getWidth() - cropWidth) / 2;
int cropY = (source.getHeight() - cropHeight) / 2;
// 使用 BitmapRegionDecoder 避免 OOM
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(source, false);
Rect cropRect = new Rect(cropX, cropY, cropX + cropWidth, cropY + cropHeight);
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.RGB_565;
return decoder.decodeRegion(cropRect, opts);
}
BitmapRegionDecoder,而不是直接加载整张图再裁剪。我见过太多开发者直接加载 4000x3000 的图片,结果在 512MB 内存的机器上直接崩了。
4.2 设置流程
裁剪完成后,设置壁纸的流程如下:
- WallpaperPicker 调用
WallpaperManager.setBitmap() - WallpaperManager 通过 Binder 调用
WallpaperManagerService - WallpaperManagerService 将 Bitmap 写入文件系统
- 通知 SystemUI 刷新壁纸显示
- Launcher 收到
ACTION_WALLPAPER_CHANGED广播,重绘背景
设置壁纸的代码示例:
// WallpaperPicker 中设置壁纸
WallpaperManager wm = WallpaperManager.getInstance(context);
try {
// 设置静态壁纸
wm.setBitmap(croppedBitmap);
// 或者设置动态壁纸
Intent intent = new Intent(WallpaperManager.ACTION_LIVE_WALLPAPER_CHOOSER);
intent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
new ComponentName(context, MyLiveWallpaper.class));
startActivity(intent);
} catch (IOException e) {
// 写入失败处理
Log.e(TAG, "设置壁纸失败", e);
}
WallpaperManager.getSuggestedFrameSize() 获取系统推荐的壁纸尺寸。不同设备的壁纸尺寸可能不一样,比如折叠屏展开和折叠状态下的尺寸就不同。
五、避坑指南
做壁纸系统这些年,我踩过不少坑。挑几个典型的说说:
- 壁纸闪烁:我曾经遇到壁纸在桌面切换时闪烁,后来发现是
SurfaceControl的setPosition和setLayer调用顺序反了——先设位置再设层级,否则 SurfaceFlinger 会短暂显示错误位置。 - OOM 崩溃:裁剪大图时,如果不做采样率控制,直接解码原图必崩。建议用
BitmapFactory.Options.inSampleSize先缩放到目标尺寸的 2 倍,再裁剪。 - 动态壁纸耗电:动态壁纸的
onDraw频率要控制,我一般限制在 30fps,除非是视频壁纸才开到 60fps。
好了,壁纸系统的核心内容就这些。下一章我们会深入 WallpaperManagerService 的内部实现,看看壁纸是怎么和 WindowManager 打交道的。
公众号:蓝海资料掘金营,微信deep3321