ProtoLog

Android 日志记录系统旨在实现普遍可访问性和易用性,并假设所有日志数据都可以表示为字符序列。这种假设与大多数用例一致,尤其是在没有专门工具的情况下,日志可读性至关重要时。但是,在需要高日志记录性能和受限日志大小的环境中,基于文本的日志记录可能不是最佳选择。WindowManager 就是这样一种情况,它需要一个强大的日志记录系统,能够处理实时窗口转换日志,且对系统影响最小。

ProtoLog 是解决 WindowManager 和类似服务的日志记录需求的替代方案。与 logcat 相比,ProtoLog 的主要优势在于

  • 用于日志记录的资源量更小。
  • 从开发者的角度来看,它与使用默认的 Android 日志记录框架相同。
  • 支持在运行时启用或停用日志语句。
  • 如果需要,仍然可以记录到 logcat。

为了优化内存使用量,ProtoLog 采用了一种字符串驻留机制,该机制涉及计算和保存消息的编译哈希值。为了提高性能,ProtoLog 在编译期间(对于系统服务)执行字符串驻留,运行时仅记录消息标识符和参数。此外,在生成 ProtoLog 跟踪记录或获取错误报告时,ProtoLog 会自动合并编译时创建的消息字典,从而能够从任何版本解码消息。

使用 ProtoLog 时,消息以二进制格式 (proto) 存储在 Perfetto 跟踪记录中。消息解码发生在 Perfetto 的 trace_processor 内部。该过程包括解码二进制 proto 消息,使用嵌入式消息字典将消息标识符转换为字符串,以及使用动态参数格式化字符串。

ProtoLog 支持与 android.utils.Log 相同的日志级别,即:dviwewtf

客户端 ProtoLog

最初,ProtoLog 仅适用于 WindowManager 的服务器端,在单个进程和组件中运行。随后,它扩展到包括 System UI 进程中的 WindowManager shell 代码,但 ProtoLog 的使用需要复杂的样板设置代码。此外,Proto 日志记录仅限于系统服务器和 System UI 进程,使得将其合并到其他进程中变得繁琐,并且每个进程都需要单独的内存缓冲区设置。但是,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_indexflags

在新服务中使用 ProtoLog

要在新进程中使用 ProtoLog,请执行以下操作

  1. 为此服务创建 ProtoLogGroup 定义。

  2. 在其首次使用之前初始化定义(例如,在进程创建时)

    Protolog.init(ProtologGroup.values());

  3. 以与 android.util.Log 相同的方式使用 Protolog

    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);
}

在此示例中,ProtoLogProtoLogImplProtoLogGroup 是作为参数提供的类(可以导入、静态导入或完整路径,不允许使用通配符导入),而 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 的主要优势之一是您可以在运行时启用或停用它。例如,您可以在构建中拥有更详细的日志记录(默认情况下禁用),并在本地开发期间启用它,以调试特定问题。例如,WindowManager 中使用了这种模式,其中组 WM_DEBUG_WINDOW_TRANSITIONSWM_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);

如果在代码中使用参数 A,B,C 调用 ProtoLog

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 表格滚动回时间线中选定的时间戳

ProtoLog viewer

图 1. ProtoLog 查看器