Android动态日志ProtoLog原理与架构: How ProtoLogTool works

您所在的位置:网站首页 transform工具 Android动态日志ProtoLog原理与架构: How ProtoLogTool works

Android动态日志ProtoLog原理与架构: How ProtoLogTool works

#Android动态日志ProtoLog原理与架构: How ProtoLogTool works | 来源: 网络整理| 查看: 265

11:33 amThursday, 7 July 2022 (GMT+8)Time in Guangzhou, Guangdong Province, China

概述 原理 编译 ProtoLogTool 总结 参考 概述

本文基于Android 11介绍动态日志ProtoLog的实现原理。

ProtoLog在编译期插入代码,用于实现动态启停的判断逻辑,在编译期实现字符串哈希管理,用于实现日志数据量的压缩。执行这些工作的主要工具是ProtoLogTool。

原理

本节介绍ProtoLog在编译期的工作、以及动态启停的实现。

编译

在Framework Services编译过程中,会执行动态日志相关的编译。关键的编译配置在framework/services/core/Android.bp中,如下。可以看到,services在编译过程中会调用protologtool。

1234567891011121314151617genrule { name: "services.core.protologsrc", srcs: [ ":services.core.wm.protologgroups", ":services.core-sources", ], tools: ["protologtool"], cmd: "$(location protologtool) transform-protolog-calls " + "--protolog-class com.android.server.protolog.common.ProtoLog " + "--protolog-impl-class com.android.server.protolog.ProtoLogImpl " + "--protolog-cache-class 'com.android.server.protolog.ProtoLog$$Cache' " + "--loggroups-class com.android.server.wm.ProtoLogGroup " + "--loggroups-jar $(location :services.core.wm.protologgroups) " + "--output-srcjar $(out) " + "$(locations :services.core-sources)", out: ["services.core.protolog.srcjar"],}

protologtool的编译配置如下。它通过javaparser将源码中ProtoLog相关的调用进行替换,并通过jsonlib生成Config、通过platformprotos提供ProtoBuf支持。

1234567891011121314151617181920java_library_host { name: "protologtool-lib", srcs: [ "src/com/android/protolog/tool/**/*.kt", ], static_libs: [ "protolog-common", "javaparser", "platformprotos", "jsonlib", ],}java_binary_host { name: "protologtool", manifest: "manifest.txt", static_libs: [ "protologtool-lib", ],}

总结来说,**ProtoLog会借助ProtoLogTool在编译期通过JavaParser将动态日志进行实现**。下面分析ProtoLogTool即可了解ProtoLog的实现了。

ProtoLogTool

ProtoLogTool主要负责三大重要事务。一是找到源码中ProtoLog的调用;二是将ProtoLog调用替换为真正的动态日志实现,这个过程会执行日志数据量压缩工作所要求的哈希计算和引用替换;三是将生成对应的哈希-字符串Map、Group、TAG到Json中——称为Config,没有该文件的情况下是无法解析ProtoBuf的。

其中,找到ProtoLog的调用、替换调用等功能一般会基于源码词法分析或字节码分析、插桩。ProtoLog采用的是JavaParser,它是源码级的Analyse、Transform工具。

ProtoLogTool关键流程如下(源码可以参考“参考”节中列出的源码地址)。ProtoLogTool将三个关键步骤分为对应的三个分支流程,包括生成Config、解析读取PB文件以及编译处理。

123456789101112fun invoke(command: CommandOptions) { StaticJavaParser.setConfiguration(ParserConfiguration().apply { setLanguageLevel(ParserConfiguration.LanguageLevel.RAW) setAttributeComments(false) }) when (command.command) { CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command) CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command) CommandOptions.READ_LOG_CMD -> read(command) }}

processClasses()是决定动态日志功能实现的关键。阅读源码可知,它通过JavaParser实现了源码级插桩。细节是找到所有ProtoLog的日志方法的调用,然后通过Transformer进行源码级别的替换。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657private fun processClasses(command: CommandOptions) { val groups = injector.readLogGroups( command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg) val out = injector.fileOutputStream(command.outputSourceJarArg) val outJar = JarOutputStream(out) val processor = ProtoLogCallProcessor(command.protoLogClassNameArg, command.protoLogGroupsClassNameArg, groups) val executor = newThreadPool() try { command.javaSourceArgs.map { path -> executor.submitCallable { val transformer = SourceTransformer(command.protoLogImplClassNameArg, command.protoLogCacheClassNameArg, processor) val file = File(path) val text = injector.readText(file) ......open fun process(code: CompilationUnit, callVisitor: ProtoLogCallVisitor?, fileName: String): CompilationUnit { CodeUtils.checkWildcardStaticImported(code, protoLogClassName, fileName) CodeUtils.checkWildcardStaticImported(code, protoLogGroupClassName, fileName) val isLogClassImported = CodeUtils.isClassImportedOrSamePackage(code, protoLogClassName) val staticLogImports = CodeUtils.staticallyImportedMethods(code, protoLogClassName) val isGroupClassImported = CodeUtils.isClassImportedOrSamePackage(code, protoLogGroupClassName) val staticGroupImports = CodeUtils.staticallyImportedMethods(code, protoLogGroupClassName) code.findAll(MethodCallExpr::class.java) .filter { call -> isProtoCall(call, isLogClassImported, staticLogImports) }.forEach { call -> val context = ParsingContext(fileName, call) if (call.arguments.size < 2) { throw InvalidProtoLogCallException("Method signature does not match " + "any ProtoLog method: $call", context) } val messageString = CodeUtils.concatMultilineString(call.getArgument(1), context) val groupNameArg = call.getArgument(0) val groupName = getLogGroupName(groupNameArg, isGroupClassImported, staticGroupImports, fileName) if (groupName !in groupMap) { throw InvalidProtoLogCallException("Unknown group argument " + "- not a ProtoLogGroup enum member: $call", context) } callVisitor?.processCall(call, messageString, LogLevel.getLevelForMethodName( call.name.toString(), call, context), groupMap.getValue(groupName)) } ...

可以看到,ProtoLogTool为了实现日志压缩,会将字符串做哈希替换,并将日志打印方法替换为真正具有动态能力的实现。这是源码级别的插桩,它通过JavaParser实现。如果熟悉JavaParser容易读懂代码。

下面看看经过ProtoLogTool处理后的产物,看看ProtoLog的动态能力的技术原理。

反编译service.jar,找一个ProtoLog的案例。如下,以DisplayPolicy.finishScreenTurningOn()为例。

12345public boolean finishScreenTurningOn() { ... ProtoLog.i(WM_DEBUG_SCREEN_ON, "Finished screen turning on..."); ...}

编译后的代码如下(从dex反编译而来,只列出ProtoLog部分):

12345678if (!protoLogParam02 && this.mScreenOnEarly && this.mWindowManagerDrawComplete && (!this.mAwake || this.mKeyguardDrawComplete)) { if (ProtoLog.Cache.WM_DEBUG_SCREEN_ON_enabled) { ProtoLogImpl.i(ProtoLogGroup.WM_DEBUG_SCREEN_ON, 1140424002, 0, (String) null, (Object[]) null); } this.mScreenOnListener = null; this.mScreenOnFully = true; return true;}

可以看到,动态日志的动态能力实现的基本思路是判断一个开关是否被打开,该开关控制了对应的动态日志打印代码是否被执行

动态日志打印方法ProtoLog实际上被替换成了ProtoLogImpl,并且不再直接使用字符串,而是使用了一个整型常量

动态日志开关

动态日志开关(即上面的WM_DEBUG_SCREEN_ON_enabled)是由ProtoLogImpl在编译期生成的。该值是Group内的一个属性,并有IProtoLogGroup接口用作动态设置:

123456789101112public interface IProtoLogGroup { String getTag(); boolean isEnabled(); boolean isLogToLogcat(); boolean isLogToProto(); String name(); void setLogToLogcat(boolean z); void setLogToProto(boolean z); default boolean isLogToAny() { return isLogToLogcat() || isLogToProto(); }}

WindowMangager暴露了adb ShellCommand接口用于接收动态日志的启停命令:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// frameworks/base/services/core/java/com/android/server/protolog/ProtoLogImpl.java private int setLogging(ShellCommand shell, boolean setTextLogging, boolean value) { String group; while ((group = shell.getNextArg()) != null) { IProtoLogGroup g = LOG_GROUPS.get(group); if (g != null) { if (setTextLogging) { g.setLogToLogcat(value); } else { g.setLogToProto(value); } } else { logAndPrintln(shell.getOutPrintWriter(), "No IProtoLogGroup named " + group); return -1; } } sCacheUpdater.run(); return 0; } public int onShellCommand(ShellCommand shell) { PrintWriter pw = shell.getOutPrintWriter(); String cmd = shell.getNextArg(); if (cmd == null) { return unknownCommand(pw); } switch (cmd) { case "start": startProtoLog(pw); return 0; case "stop": stopProtoLog(pw, true); return 0; case "status": logAndPrintln(pw, getStatus()); return 0; case "enable": return setLogging(shell, false, true); case "enable-text": mViewerConfig.loadViewerConfig(pw, VIEWER_CONFIG_FILENAME); return setLogging(shell, true, true); case "disable": return setLogging(shell, false, false); case "disable-text": return setLogging(shell, true, false); default: return unknownCommand(pw); } }

总结:ProtoLog动态启停的原理是以一个个的Group的开关为桥梁的。动态日志打印前会判断这个开关,并能够通过ShellCommand这一接口实时设置开关值。其中,对开关的判断和管理代码均是由ProtoLogTool在编译期完成生成的,属于源码级Transform,并不需要用户手动添加难看的开关判断逻辑。

日志数据量压缩

可以看到ProtoLogTool除了将ProtoLog替换成ProtoLogImpl外,还将打印的字符串替换成哈希值。ProtoLogImpl实现数据量压缩的原理如下。

12345678910111213141516171819202122232425262728public static void i(IProtoLogGroup group, int messageHash, int paramsMask, @Nullable String messageString, Object... args) { getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args);}public void log(LogLevel level, IProtoLogGroup group, int messageHash, int paramsMask, @Nullable String messageString, Object[] args) { if (group.isLogToProto()) { logToProto(messageHash, paramsMask, args); } if (group.isLogToLogcat()) { logToLogcat(group.getTag(), level, messageHash, messageString, args); }}private void logToProto(int messageHash, int paramsMask, Object[] args) { if (!isProtoEnabled()) { return; } try { ProtoOutputStream os = new ProtoOutputStream(); long token = os.start(LOG); os.write(MESSAGE_HASH, messageHash); os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos()) ... ...}

日志数据量压缩的关键逻辑就是ProtoOutputStream.write(MESSAGE_HASH,messageHash)了。可见,Message确实被ProtoLogTool转化成了对应的哈希,并通过ProtoOutputStream写入到ProtoBuf中,最后输出成为二进制日志。这就是日志数据量压缩的基本原理——日志不再存储字符串本身,而是存储字符串的哈希。解析器根据字符串与哈希的映射,反向操作解析出字符串。这样做能够使冗余的字符串(一个日志打印方法在业务逻辑中、进程运行过程中会多次调用,使日志存储重复的冗余的串)被替换成定长的简短的哈希。

总结

ProtoLog提供的简单接口,需要在编译期由ProtoLogTool进行源码级的替换,生成对日志开关的检查代码来实现动态日志。并通过哈希-String映射实现数据量压缩。

参考 Android Framework Service.core Android.bp ProtoLogTool


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3