Android 日志记录系统旨在实现通用无障碍和易用性,假定所有日志数据都可以表示为字符序列。此假设适用于大多数用例,尤其是在没有专用工具的情况下,日志可读性至关重要时。不过,在需要高日志记录性能且日志大小受限的环境中,基于文本的日志记录可能不是最佳选择。WindowManager 就是其中一种情况,它需要一个强大的日志记录系统,能够在对系统影响最小的情况下处理实时窗口转换日志。
ProtoLog 是满足 WindowManager 和类似服务日志记录需求的替代方案。与 logcat 相比,ProtoLog 的主要优势包括:
- 用于日志记录的资源量更少。
- 从开发者的角度来看,这与使用默认的 Android 日志记录框架相同。
- 支持在运行时启用或停用日志语句。
- 如有需要,仍可将日志记录到 Logcat。
为了优化内存用量,ProtoLog 采用了字符串内嵌机制,该机制涉及计算和保存消息的已编译哈希。为了提高性能,ProtoLog 会在编译期间(对于系统服务)执行字符串内嵌,并仅在运行时记录消息标识符和参数。此外,在生成 ProtoLog 轨迹或获取 bug 报告时,ProtoLog 会自动纳入在编译时创建的消息字典,从而支持从任何 build 解码消息。
使用 ProtoLog 时,消息会以二进制格式 (proto) 存储在 Perfetto 轨迹中。消息解码在 Perfetto 的 trace_processor
中进行。该过程包括解码二进制 proto 消息、使用嵌入的消息字典将消息标识符转换为字符串,以及使用动态参数设置字符串格式。
ProtoLog 支持与 android.utils.Log
相同的日志级别,即:d
、v
、i
、w
、e
、wtf
。
客户端 ProtoLog
最初,ProtoLog 仅适用于 WindowManager 的服务器端,在单个进程和组件中运行。随后,它扩展为涵盖系统界面进程中的 WindowManager shell 代码,但使用 ProtoLog 需要复杂的样板设置代码。此外,Proto 日志记录仅限于系统服务器和系统界面进程,因此很难集成到其他进程中,并且需要为每个进程设置单独的内存缓冲区。不过,ProtoLog 现在已可用于客户端代码,无需额外的样板代码。
与系统服务代码不同,客户端代码通常会跳过编译时字符串内嵌。而是在后台线程中动态进行字符串内嵌。因此,虽然客户端上的 ProtoLog 与系统服务上的 ProtoLog 具有类似的内存用量优势,但其性能开销略高,并且缺少其服务器端对等项的固定内存减少优势。
ProtoLog 组
ProtoLog 消息会整理为名为 ProtoLogGroups
的组,类似于 Logcat 消息由 TAG
整理的方式。这些 ProtoLogGroups
充当消息集群,可在运行时集体启用或停用。此外,它们还控制是否应在编译期间剥离消息,以及应将消息记录到何处(proto 和/或 logcat)。每个 ProtoLogGroup
都包含以下属性:
enabled
:如果设置为false
,系统会在编译期间排除此组中的消息,并且这些消息在运行时不可用。logToProto
:定义此组是否以二进制格式记录日志。logToLogcat
:定义此组是否将日志记录到 Logcat。tag
:已记录消息的来源的名称。
使用 ProtoLog 的每个进程都必须配置 ProtoLogGroup
实例。
支持的参数类型
在内部,ProtoLog 使用 android.text.TextUtils#formatSimple(String, Object...)
格式化字符串,因此其语法相同。
ProtoLog 支持以下参数类型:
%b
- 布尔值%d
、%x
- 整数类型(short、integer 或 long)%f
- 浮点类型(float 或 double)%s
- 字符串%%
- 字面百分比字符
支持宽度和精度修饰符(例如 %04d
和 %10b
),但不支持 argument_index
和 flags
。
在新服务中使用 ProtoLog
如需在新进程中使用 ProtoLog,请执行以下操作:
为此服务创建
ProtoLogGroup
定义。在首次使用之前初始化定义(例如,在进程创建时):
Protolog.init(ProtologGroup.values());
使用
Protolog
的方式与android.util.Log
相同:ProtoLog.v(WM_SHELL_STARTING_WINDOW, "create taskSnapshot surface for task: %d", taskId);
启用编译时优化
如需在进程中启用编译时 ProtoLog,您必须更改其构建规则并调用 protologtool
二进制文件。
ProtoLogTool
是一个代码转换二进制文件,用于执行字符串内嵌并更新 ProtoLog 调用。此二进制文件会转换每个 ProtoLog
日志记录调用,如以下示例所示:
ProtoLog.x(ProtoLogGroup.GROUP_NAME, "Format string %d %s", value1, value2);
变为:
if (ProtoLogImpl.isEnabled(GROUP_NAME)) {
int protoLogParam0 = value1;
String protoLogParam1 = String.valueOf(value2);
ProtoLogImpl.x(ProtoLogGroup.GROUP_NAME, 1234560b0100, protoLogParam0, protoLogParam1);
}
在此示例中,ProtoLog
、ProtoLogImpl
和 ProtoLogGroup
是作为参数提供的类(可以是导入的、静态导入的或完整路径,不允许使用通配符导入),x
是日志记录方法。
转换是在源代码级完成的。系统会根据格式字符串、日志级别和日志组名称生成一个哈希,并将其插入 ProtoLogGroup 参数后面。实际生成的代码会内嵌,并添加一些新的行字符,以保留文件中的行编号。
示例:
genrule {
name: "wm_shell_protolog_src",
srcs: [
":protolog-impl", // protolog lib
":wm_shell_protolog-groups", // protolog groups declaration
":wm_shell-sources", // source code
],
tools: ["protologtool"],
cmd: "$(location protologtool) transform-protolog-calls " +
"--protolog-class com.android.internal.protolog.ProtoLog " +
"--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
"--loggroups-jar $(location :wm_shell_protolog-groups) " +
"--viewer-config-file-path /system_ext/etc/wmshell.protolog.pb " +
"--legacy-viewer-config-file-path /system_ext/etc/wmshell.protolog.json.gz " +
"--legacy-output-file-path /data/misc/wmtrace/shell_log.winscope " +
"--output-srcjar $(out) " +
"$(locations :wm_shell-sources)",
out: ["wm_shell_protolog.srcjar"],
}
命令行选项
ProtoLog 的一项主要优势是,您可以在运行时启用或停用它。例如,您可以在 build 中启用更详细的日志记录(默认处于停用状态),并在本地开发期间启用该日志记录,以调试特定问题。例如,在 WindowManager 中,此模式用于通过组 WM_DEBUG_WINDOW_TRANSITIONS
和 WM_DEBUG_WINDOW_TRANSITIONS_MIN
启用不同类型的转换日志记录,前者默认处于启用状态。
您可以在启动轨迹时使用 Perfetto 配置 ProtoLog。您还可以使用 adb
命令行在本地配置 ProtoLog。
adb shell cmd protolog_configuration
命令支持以下参数:
help
Print this help text.
groups (list | status)
list - lists all ProtoLog groups registered with ProtoLog service"
status <group> - print the status of a ProtoLog group"
logcat (enable | disable) <group>"
enable or disable ProtoLog to logcat
有效使用技巧
ProtoLog 会对消息和传递的任何字符串参数使用字符串内嵌。这意味着,为了从 ProtoLog 中获得更多好处,消息应将重复的值隔离到变量中。
例如,请参考以下语句:
Protolog.v(MY_GROUP, "%s", "The argument value is " + argument);
在编译时进行优化后,它会转换为:
ProtologImpl.v(MY_GROUP, 0x123, "The argument value is " + argument);
如果在代码中使用 ProtoLog 并使用参数 A,B,C
:
Protolog.v(MY_GROUP, "%s", "The argument value is A");
Protolog.v(MY_GROUP, "%s", "The argument value is B");
Protolog.v(MY_GROUP, "%s", "The argument value is C");
Protolog.v(MY_GROUP, "%s", "The argument value is A");
这会导致内存中出现以下消息:
Dict:
0x123: "%s"
0x111: "The argument value is A"
0x222: "The argument value is B"
0x333: "The argument value is C"
Message1 (Hash: 0x123, Arg1: 0x111)
Message2 (Hash: 0x123, Arg2: 0x222)
Message3 (Hash: 0x123, Arg3: 0x333)
Message4 (Hash: 0x123, Arg1: 0x111)
如果 ProtoLog 语句的写法如下:
Protolog.v(MY_GROUP, "The argument value is %s", argument);
内存缓冲区最终会变成:
Dict:
0x123: "The argument value is %s" (24 b)
0x111: "A" (1 b)
0x222: "B" (1 b)
0x333: "C" (1 b)
Message1 (Hash: 0x123, Arg1: 0x111)
Message2 (Hash: 0x123, Arg2: 0x222)
Message3 (Hash: 0x123, Arg3: 0x333)
Message4 (Hash: 0x123, Arg1: 0x111)
这种序列会使内存占用量减少 35%。
Winscope 查看器
Winscope 的 ProtoLog 查看器标签页会以表格格式显示 ProtoLog 轨迹。您可以按日志级别、标记、源文件(包含 ProtoLog 语句)和消息内容过滤轨迹。所有列均可过滤。 点击第一列中的时间戳,即可将时间轴移至消息时间戳。此外,点击前往当前时间可将 ProtoLog 表滚动回时间轴中所选的时间戳:
图 1. ProtoLog 查看器