Android 平台water mark实现
摘要本文给出一个基于 Android SurfaceFlinger 的 C 水印绘制示例先通过 SurfaceComposerClient 初始化并获取主屏幕的宽高、DPI 等信息再创建置顶的 SurfaceControl 图层随后用 Skia 将半透明颜色绘制到该图层的 Surface 上并提交显示最后附上对应的 Android.mk 编译脚本说明依赖库与模块配置方式。核心流程拆解这段示例代码虽然不长但完整走了一遍「从连接 SurfaceFlinger 到把像素真正显示到屏幕上」的链路。下面按五个关键步骤拆开说明每一步都对应代码中的一段逻辑理解之后再看完整代码会清晰很多。flowchart TD A[初始化 SurfaceComposerClient new SurfaceComposerClient / initCheck()] -- B[获取 DisplayInfo getBuiltInDisplay() / getDisplayInfo()] B -- C[创建 SurfaceControl 并 setLayer/show createSurface() / openGlobalTransaction() / setLayer() / show() / closeGlobalTransaction()] C -- D[lock 后用 Skia 绘制 getSurface() / lock() / SkBitmap::setInfo() / setPixels() / SkCanvas::drawColor()] D -- E[unlockAndPost 提交 unlockAndPost() / while(1) 常驻]初始化 SurfaceComposerClientnew SurfaceComposerClient会与系统surfaceflinger进程建立 Binder 连接随后调用initCheck()校验连接是否可用。只有返回NO_ERROR才能继续否则说明当前进程没有访问合成服务的权限常见原因是 SELinux 策略限制或未以 system 身份运行。获取主屏 DisplayInfogetBuiltInDisplay(ISurfaceComposer::eDisplayIdMain)拿到主屏幕的 Binder 句柄再通过getDisplayInfo()填充DisplayInfo结构体。其中w、h是屏幕宽高像素xdpi、ydpi是物理 DPIfps是刷新率density是逻辑密度。后续创建图层和 Skia 位图都依赖这里的宽高所以必须先取到。创建 SurfaceControl 并 setLayer/showcreateSurface(name, w, h, format)创建一个图层参数分别是图层名、宽高和像素格式这里用PIXEL_FORMAT_RGBA_8888保证带 Alpha 通道。创建后需在openGlobalTransaction()与closeGlobalTransaction()之间批量提交属性setLayer(0x7FFFFFFF)把图层层级设为最大实现全局置顶show()让图层可见。事务机制保证这些属性原子生效避免中间状态闪烁。lock 后用 Skia 绘制通过getSurface()拿到Surface调用lock(outBuffer, NULL)锁定一块可写的图形缓冲区outBuffer.bits就是像素首地址。接着用SkBitmap::setInfo()描述位图格式宽高、kRGB_565_SkColorType颜色类型、kUnpremul_SkAlphaType透明度类型setPixels()绑定缓冲区内存最后用SkCanvas::drawColor(0x80509a47, SkXfermode::kSrc_Mode)以 Src 模式填充半透明颜色其中0x80是 Alpha 值。unlockAndPost 提交显示绘制完成后必须调用unlockAndPost()解锁缓冲区并把它提交给 SurfaceFlinger 合成上屏。若只lock不unlockAndPost画面不会更新缓冲区也会一直被占用。代码最后的while(1)是为了让进程常驻保持图层持续显示。#include stdlib.h #include stdio.h #include gui/SurfaceComposerClient.h #include gui/ISurfaceComposer.h #include gui/Surface.h #include ui/DisplayInfo.h #include android/native_window.h #include core/SkCanvas.h #include core/SkBitmap.h #include core/SkRegion.h #include SkColorPriv.h using namespace android; int main(int argc, char** argv) { status_t err; int pid getpid(); int x 176, y 144; int width 720, height 480; fprintf(stderr, SurfaceComposerClient::initCheck : %#x\n, 1); // 步骤 1初始化 SurfaceComposerClient与 surfaceflinger 建立 Binder 连接 spSurfaceComposerClient videoClient new SurfaceComposerClient; err videoClient-initCheck(); // 校验连接是否可用非 NO_ERROR 说明无权限或服务异常 if (err ! NO_ERROR) { fprintf(stderr, SurfaceComposerClient::initCheck error: %#x\n, err); return -1; } // 步骤 2获取主屏 DisplayInfo拿到屏幕宽高、DPI、刷新率等信息 // Get main display parameters. spIBinder mainDpy SurfaceComposerClient::getBuiltInDisplay(ISurfaceComposer::eDisplayIdMain); // 主屏句柄 DisplayInfo dinfo; // 获取屏幕的宽高等信息 err SurfaceComposerClient::getDisplayInfo(mainDpy, dinfo); // 填充 dinfow/h 为像素宽高xdpi/ydpi 为物理 DPI width dinfo.w; height dinfo.h; printf(w%d,h%d,xdpi%f,ydpi%f,fps%f,ds%f\n, dinfo.w, dinfo.h, dinfo.xdpi, dinfo.ydpi, dinfo.fps, dinfo.density); if (err ! NO_ERROR) { fprintf(stderr, SurfaceComposerClient::getDisplayInfo error: %#x\n, err); return -1; } // 步骤 3创建 SurfaceControl 图层参数为图层名、宽、高、像素格式RGBA_8888 带 Alpha spSurfaceControl spSurCtrl videoClient-createSurface(String8(pid), dinfo.w, dinfo.h, PIXEL_FORMAT_RGBA_8888); if (spSurCtrl NULL || !spSurCtrl-isValid()) { fprintf(stderr, Failed to create SurfaceControl\n); return -1; } SurfaceControl* surContrl spSurCtrl.get(); // 开启全局事务批量提交图层属性保证原子生效 SurfaceComposerClient::openGlobalTransaction(); // surContrl-setSize(x, y); // surContrl-setPosition(10, 10); err surContrl-setLayer(0x7FFFFFFF); // always on top层级设为最大实现全局置顶 if (err ! NO_ERROR) { fprintf(stderr, SurfaceComposer::setLayer error: %#x\n, err); return -1; } err surContrl-show(); // 让图层可见 if (err ! NO_ERROR) { fprintf(stderr, SurfaceComposer::show error: %#x\n, err); return -1; } SurfaceComposerClient::closeGlobalTransaction(); // 提交事务属性正式生效 fprintf(stderr, SurfaceComposerClient::closeGlobalTransaction : %#x\n, 3); const spSurface sur surContrl-getSurface(); // 从图层拿到可绘制的 Surface // 步骤 4lock 缓冲区后用 Skia 绘制 SkBitmap *pmap new SkBitmap(); ANativeWindow_Buffer outBuffer; sur-lock(outBuffer, NULL); // 锁定图形缓冲区outBuffer.bits 为像素首地址 pmap-setInfo(SkImageInfo::Make(dinfo.w, dinfo.h, kRGB_565_SkColorType, kUnpremul_SkAlphaType)); // 描述位图格式 pmap-setPixels(outBuffer.bits); // 把 Skia 位图绑定到缓冲区内存 SkCanvas *screen_canvas new SkCanvas(*pmap); screen_canvas-drawColor(0x80509a47, SkXfermode::kSrc_Mode); // 以 Src 模式填充半透明颜色0x80 为 Alpha fprintf(stderr, SurfaceComposerClient::drawColor : %#x\n, 2); // 步骤 5解锁缓冲区并提交给 SurfaceFlinger 合成上屏 sur-unlockAndPost(); while (1) // 常驻进程保持图层持续显示 ; return 0; }编译脚本LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_SRC_FILES : \ main.cpp LOCAL_SHARED_LIBRARIES : \ libcutils \ libutils \ libui \ libgui \ libskia LOCAL_C_INCLUDES \ external/skia/include/ LOCAL_MODULE : Watermark LOCAL_MODULE_TAGS : optional include $(BUILD_EXECUTABLE)模块放置与编译运行把上面的main.cpp和Android.mk放到 AOSP 源码树的同一个目录下例如frameworks/base/cmds/Watermark/也可以放在device/vendor/device/Watermark/这类设备目录中。目录结构如下frameworks/base/cmds/Watermark/ ├── Android.mk └── main.cpp如果放在frameworks/base/cmds/下需要确认该目录的Android.mk已经通过include $(call all-subdir-makefiles)递归包含子目录否则模块不会被扫描到放在设备目录时则要在设备主Android.mk或device.mk中显式引入该子目录。编译命令在源码根目录执行source build/envsetup.sh并lunch选择目标产品后进入模块目录即可使用mm或mmm单独编译# 进入模块目录后编译当前目录 cd frameworks/base/cmds/Watermark mm 或者在源码根目录按路径编译 mmm frameworks/base/cmds/Watermark 清理后重新编译 mm -B编译产物位于out/target/product/device/system/bin/Watermark。若使用 Android 10 及以上版本的 Soong 与 Make 混合构建也可以改用make Watermark直接按模块名编译。推送与运行编译完成后把可执行文件推送到设备的/system/bin目录并赋予执行权限adb root adb remount adb push out/target/product/device/system/bin/Watermark /system/bin/ adb shell chmod 755 /system/bin/Watermark adb shell Watermark如果设备未开启adb root也可以先adb push到/data/local/tmp再执行但此时进程的 SELinux 域与权限和系统分区运行并不完全一致仅适合临时验证。为什么需要 root 或 system 权限访问 SurfaceFlinger 服务SurfaceComposerClient需要与系统surfaceflinger进程通过 Binder 通信普通应用受 SELinux 策略限制无法直接创建系统级图层。创建置顶图层代码中通过setLayer(0x7FFFFFFF)把图层置于最顶层这类全局显示操作属于系统权限范畴普通应用只能在自己的窗口内绘制。写入系统分区可执行文件需要放到/system/bin才能被系统正常加载执行写入该分区必须remount而remount依赖 root 权限。SELinux 约束在 enforcing 模式下还需要为模块配置合适的sepolicy域或临时执行setenforce 0验证否则即使有 root 也可能被拒绝访问。常见问题与排查在实际设备上跑这个示例时最容易卡住的往往不是代码逻辑而是权限、SELinux 和图形缓冲区格式这几类问题。下面按典型报错场景整理排查思路每条都给出可能原因和对应的排查命令或修改点。createSurface 返回 NULL 或 isValid() 为 false常见原因是SurfaceComposerClient未成功初始化、传入的宽高为 0或当前进程没有创建系统级图层的权限。排查时先确认initCheck()与getDisplayInfo()都返回NO_ERROR并打印dinfo.w、dinfo.h确认非 0再用adb shell dumpsys SurfaceFlinger --list观察图层是否被创建。若权限不足可执行adb shell dumpsys SurfaceFlinger查看服务状态并确认进程以 system 身份运行。setLayer 或 show 返回非 NO_ERROR多因图层句柄无效、事务未正确开启或在openGlobalTransaction()与closeGlobalTransaction()之间发生了提前 return导致事务悬挂。排查时检查spSurCtrl是否有效确保所有属性设置都在事务区间内完成并在失败分支中补上closeGlobalTransaction()。可用adb logcat | grep -i surfaceflinger观察服务端返回的错误码。lock 之后画面不刷新典型原因是只调用了lock()而没有调用unlockAndPost()或绘制后进程立即退出导致缓冲区未提交。排查时确认unlockAndPost()的返回值并在其后保留while(1)让进程常驻同时检查drawColor的 Alpha 值若为0x00则完全透明看起来就像没有绘制。可用adb shell dumpsys SurfaceFlinger确认图层是否处于可见状态。SELinux 拒绝访问avc denied在 enforcing 模式下自定义可执行文件默认没有访问surfaceflinger的域权限。排查时执行adb shell dmesg | grep avc或adb logcat | grep avc查看拒绝记录确认scontext与tcontext。临时验证可执行adb shell setenforce 0正式方案是在设备sepolicy中为新域添加allow规则或直接复用已有的 system 域运行。Skia 颜色格式不匹配导致花屏或偏色代码中createSurface使用PIXEL_FORMAT_RGBA_8888而SkBitmap::setInfo()却声明为kRGB_565_SkColorType两者位宽不一致就会造成像素错位、颜色异常。排查时用adb shell dumpsys SurfaceFlinger查看图层实际格式并把SkImageInfo::Make()的颜色类型改为与创建图层一致的kRGBA_8888_SkColorType同时确认outBuffer.stride与位图宽度匹配。