作者|刘素云,单位:智能家居运营中心实验室导读仪器仪表技术很有趣,很有价值。学习了这项技术后,我们可以随心所欲地操作代码,以满足不同场景的需要。很多框架都离不开这项技术,比如常见的ButterKnife注解框架、数据库ORM框架、APM性能监控、埋点统计等。和家勤是一款智能家居综合服务门户APP。客户端的性能直接影响用户体验。在和家沁APP的性能优化中,特别是启动到首屏的专项优化中,采用了Gradle+ASM编译插桩技术,实现了apk的全局耗时方法统计。本文以此为例,让你认识“仪表化”这个效率利器。Part01Compilationinstrumentation顾名思义,所谓compilationinstrumentation就是在代码编译过程中修改已有的代码或者生成新的代码。在学习存根插入之前,首先需要了解相关的基础技术,包括Android打包的大致流程、类字节码文件结构、gradleTransform任务和ASM字节码运行框架等,后面会做简单介绍,如果想了解更多,可以仔细阅读参考资料。下图是android编译和检测的示意图。字节码:“.class”文件是Java字节码,“.dex”文件是Dalvik字节码。我们这里的ASM插桩方式是操作Java字节码。使用场景:针对代码监控、代码修改、代码分析这三个场景,一般采用操作字节码的方式,如无埋点统计上报、轻量级AOP等。应用于Android,可用于行为统计、方法耗时统计等功能。Part02ASM字节码框架ASM是一个java字节码操作框架,可用于动态生成类或增强现有类的功能。2.1class文件在了解ASM框架的使用之前,首先要了解class文件格式。一个完整的类字节码文件包括:幻数和类文件版本常量池访问标志类索引、父类索引、接口索引字段表集合方法表集合属性表集合为了方便查看字节码文件,kotlin代码androidstudio自带工具tools--showkotlin字节码,java代码可以通过安装jclasslib查看。2.2ASM框架ASM的体系结构主要采用访问者模式设计。所谓访问者模式就是将一些作用于某个数据结构中的元素的操作封装起来。它可以在不改变数据结构的前提下定义动作。这些元素的新操作。ASM框架中的具体应用是从头到尾扫描.class类文件的内容,每次扫描到class文件对应的内容,就会调用ClassVisitor内部对应的方法。该方法会返回一个对应的字节码操作对象(比如visitMethod()返回一个MethodVisitor实例),通过修改这个对象,可以修改class文件对应的结构部分,最后用字节码内容覆盖原来的.class这个ClassVisitor文件实现了class文件的代码切入。2.3ASM工具工,工欲善其事,必先利其器。在使用ASM插入字节码时,如果不熟悉字节码相关的语法和规则,可能会对插入字节码代码一头雾水。好在ASM官方已经开发了一个IDE插件,可以将Java代码转换成ASM字节码类型的代码,这样使用ASM插入字节码就更方便了。使用插件ASM字节码大纲,方便查看字节码和对应的ASM框架代码。Part03GradleTransformGradleTransform是Android官方提供的一套标准API,用于在apk编译打包过程中将.class文件转换为.dex的过程中修改.class文件。这个应用现在主要关注字节码查找,代码注入等。3.1Transform原理Transform是androidgradleapi的一部分,它可以在将android项目的.class文件编译成.dex文件之前获取所有的.class文件,并在Transform中处理它们。使用TransformAPI,我们不必关注相关任务的生成和执行过程,它让我们只关注如何处理输入的类文件。每个Transform实际上是一个gradle任务。Android编译器中的TaskManager将每个Transform串联起来。第一个Transform接收javac编译的结果和本地拉取的第三方依赖(jar,aar)。),和资源资源。这些编译后的中间产物在由Transform组成的链上流动,每个Transform节点都可以对类进行处理,传递给下一个Transform。我们常见的混淆,Desugar等逻辑,它们的实现现在封装在每个Transform中,我们自定义的Transform会插入到Transform链的最前面。自定义transform可以在构建控制台看到对应的task,输出内容可以在build\intermediates\transforms\对应目录下找到。3.2Transform自定义实现如果要自定义transform,必须实现如下方法:getName():返回transform名称标识符getInputTypes():有两种输入类型,CLASSES和RESOURCES代表java类文件和资源文件getScopes():定义Transform需要处理的输入文件。isIncremental():表示是否支持增量编译。支持增量编译可以节省一些编译时间和资源。一个好的转换应该支持增量编译。Transform():Main方法,入参TransformInvocation是一个接口,提供一些输入的基本信息。使用这些接口,可以获取编译过程中的class文件进行操作。在apk打包过程中,除了自定义的Transform之外,还有一些系统自带的Transform,每个Transform在处理后交给下一个Transform,是一个链式结构。下图是自定义Transform实现的apk打包过程中的字节码插入过程示意图。简单来说就是以下几个步骤:过滤符合条件的Class文件,其中Class有两种可能的文件来源:jar包和特定目录;利用ASM框架读取Class文件中包含的类信息(如接口、注解等),进一步筛选合格的Class文件;处理最终合格的类(修改字节码、插入存根等);将乘积复制到Transform的输出目录,作为下一次Transform的输入;Part04实战:APK功能耗时打桩,家勤是智能家居综合服务的入口APP。APP使用体验的门面,启动时间过长很可能会降低用户使用APP的兴趣。在这次启动到首屏的专项优化中,需要找到并优化启动过程的耗时方法。由于业务复杂,SDK接入量大,虽然有原生的工具配置文件,但用过的人都知道很难抓取,尤其是在启动阶段,无法输出调用栈等问题。有必要实施一种工具来快速排除高耗时方法的故障。本次优化使用GradleTransForm+ASM实现编译和插桩的全局耗时方法统计,辅助启动优化分析,最终启动首屏显示。耗时从4.5s减少到3.2s,启动速度提升30%,效果显着。4.1实现思路在性能优化阶段,需要统计函数耗时,解决启动慢、卡顿等问题。了解了Android打包流程和自定义Gradle插件后发现,java文件会先转成class文件,再转成dex文件。并且通过Gradle插件提供的TransformAPI,可以在编译成dex文件之前获取class文件。得到class文件后,可以通过ASM修改字节码,完成字节码插入,插入时间统计打印代码,超过阈值则输出调用栈。主要实现以下功能:自定义Gradle插件处理类,在方法出口入口处插入耗时统计文件替换创建一个buildsrc模块在Android项目中,buildSrc是gradle默认的插件目录,并且在编译gradle的时候会自动识别这个目录。因此,我们可以直接引用buildSrc下编写的插件。通常我们使用这种方法进行插件调试。创建buildSrc目录,配置plugin插件的配置和依赖(新版Gradle插件已经支持kotlin语言编写)。注册Transform要使用gradle-transform-api,首先要实现一个gradle插件,然后在插件中注册一个Transform,在gradle-plugins目录的.properties文件中声明插件实现者,如as:implementation-class=com.xxx.xxx.SystemTracePluginTest获取所有类文件transform()通过参数输入获取所有类文件,包括源码编译的类文件和第三方jar包。字节码修改和文件回写经过以上步骤,我们已经到了输入文件,确定了输出路径。现在我们只需要处理这些文件并输出到输出路径即可。这里需要注意的是,即使不想修改某个class文件,也应该原样复制,否则文件会丢失。使用ASM框架在方法出口和入口插入耗时统计字节码,即onMethodEnter和onMethodExit回调。对应的字节码可以用上面的工具jclaslib或者asmcodeoutline查看。(以下代码只是一些例子,细节完善,比如一些包名的统计,getset方法的排除等这里就不一一列举了)应用插件完成app项目applyplugin'pluginname',Gradletask会有对应的taskname输出,然后TransformTask执行,运行apk,可以看到插入的自定义耗时统计方法的输出,比如编辑器在耗时统计中添加了逻辑方法,当耗时超过自定义阈值时,logcat会打印日志和堆栈信息。通过stub插入,在使用apk的时候,可以清楚的统计出耗时的方法和调用栈,方便后续的性能优化。可以弥补传统剖析工具性能分析的一些不足,比如只能抓取短时间段,需要自己寻找长时间耗时的方法。Part05结语编译和插桩技术的应用场景越来越多,涉及的知识也越来越多,但是相信在你熟悉了Android打包流程、class字节码文件结构、GradleTransformAPI、ASM之后,相信你会感觉instrumentationsoeasy,android开发大师班编译stubs,又上了一门新技能!在性能优化的过程中,不止一次地使用了编译和检测技术。除了方法的耗时统计,我们还使用插桩和hook代理的方法来做大规模的监控,网络监控,线程优化等,比如网络数据监控的实现就是实现通过hook网络库方式和网络层自动注入拦截器对网络请求进行全程监控,包括获取握手时间、首包时间、DNS耗时、网络耗时等网络阶段。信息。大图监控是通过hookGlide、picasso等主流图片加载库,在图片加载时增加监控和计算图片大小,并对大图进行过滤输出。一起来学习“仪表化”这个效率利器吧。参考文档[1]https://rebooters.github.io/2020/01/04/Gradle-Transform-ASM-%E6%8E%A2%E7%B4%A2/[2]https://cloud.tencent.com/developer/article/1399805[3]https://time.geekbang.org/column/intro/142?tab=catalog
