最近在做App启动优化和卡顿优化的时候涉及到统计方法耗时,使用插桩的方式能够比较方便的解决使用代码硬编码的工作量。函数插桩还可以实现其他的功能,如无埋点统计上报、轻量级AOP等。
1.函数插桩 是什么函数插桩 插桩:目标程序代码中某些位置插入或修改成一些代码,从而在目标程序运行过程中获取某些程序状态并加以分析。简单来说就是在代码中插入代码。 那么函数插桩,便是在函数中插入或修改代码。 本文将介绍在Android编译过程中,往字节码里插入自定义的字节码,所以也可以称为字节码插桩。
2 前奏 实现字节码插桩有多种工具,了解了下AspectJ和ASM,AspectJ使用方法比较简单,作用在java编译阶段,扩展较低,不支持第三方库的插桩,但是框架整体比较重,对插桩后的文件体积也比较大, 但是有扩展库 可以解决上述的缺陷,ASM虽然使用方法稍微复杂一点,但是比较轻量级,性能也好。 使用Gradle插件实现插桩还需要了解一下其他的知识 2.1 Android打包流程 2.2 自定义Gradle插件 2.3 ASM 相关资料
3. 实战 了解上述的知识后,就开始实战了,实现自定义Gradle插件 + ASM插桩。 Transform API 是在1.5.0-beta1版开始使用,利用Transform API,第三方的插件可以在.class文件转为dex文件之前,对一些.class 文件进行处理。Transform API 简化了这个处理过程,而且使用起来很灵活。所以我们可以自己实现一个Transform,使用ASM实现插桩。
3.1 创建插件 首先创建一个插件,然后注册一个Transform
1 2 3 4 5 6 7 class CodeInsert implements Plugin <Project > { @Override void apply (Project project) { def android = project.extensions.findByType(AppExtension.class ) android .registerTransform (new CodeTransform (project )) } }
然后看主要实现函数transform
, 就是遍历编译完的class文件和遍历第三方库的jar文件,使用ASM对其进行处理。这里介绍一个插件——ASM Bytecode Outline,将方便直接转化为ASM的调用方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 void transform (TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { transformInvocation.inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> if (directoryInput.file.isDirectory()) { directoryInput.file.eachFileRecurse { File file -> def name = file.name if (name.endsWith(".class" ) && !(name == ("R.class" )) && !name.startsWith("R\$" ) && !(name == ("BuildConfig.class" ))) { ClassReader reader = new ClassReader(file.bytes) ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS) ClassVisitor visitor = new CodeClassVisitor(writer) reader.accept(visitor, ClassReader.EXPAND_FRAMES) byte [] code = writer.toByteArray() def classPath = file.parentFile.absolutePath + File.separator + name FileOutputStream fos = new FileOutputStream(classPath) fos.write(code) fos.close() } } } def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) } final File rootOutput = new File(project.buildDir, "classes/${getName()}/" ) input.jarInputs.each { JarInput jarInput -> def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) boolean success = false ; File output; if (jarInput.file.getAbsolutePath().endsWith(".jar" )) { jarName = jarName.substring(0 , jarName.length() - 4 ) output = new File(rootOutput, jarName + "_trace.jar" ); if (!output.getParentFile().exists()) { output.getParentFile().mkdirs(); } ZipOutputStream zipOutputStream; ZipFile zipFile; try { zipOutputStream = new ZipOutputStream(new FileOutputStream(output)); zipFile = new ZipFile(jarInput.file); println(jarInput.file.getAbsolutePath()) Enumeration<? extends ZipEntry> enumeration = zipFile.entries(); while (enumeration.hasMoreElements()) { ZipEntry zipEntry = enumeration.nextElement(); String zipEntryName = zipEntry.getName(); println(zipEntryName) if (zipEntryName.endsWith(".class" ) && !zipEntryName.contains("R\$" ) && !zipEntryName.contains("R.class" ) && !zipEntryName.contains("BuildConfig.class" )) { InputStream inputStream = zipFile.getInputStream(zipEntry); ClassReader classReader = new ClassReader(inputStream); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassVisitor classVisitor = new CodeClassVisitor(classWriter); classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES); byte [] data = classWriter.toByteArray(); InputStream byteArrayInputStream = new ByteArrayInputStream(data); ZipEntry newZipEntry = new ZipEntry(zipEntryName); Util.addZipEntry(zipOutputStream, newZipEntry, byteArrayInputStream); } else { InputStream inputStream = zipFile.getInputStream(zipEntry); ZipEntry newZipEntry = new ZipEntry(zipEntryName); Util.addZipEntry(zipOutputStream, newZipEntry, inputStream); } } success = true ; } catch (Exception e) { println("error " + e.toString()) } finally { try { if (zipOutputStream != null ) { zipOutputStream.finish(); zipOutputStream.flush(); zipOutputStream.close(); } if (zipFile != null ) { zipFile.close(); } } catch (Exception e) { println(e.toString()) } } } def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) if (success && output != null ) { println("success set jar " + output.getAbsolutePath()) FileUtils.copyFile(output, dest) } else { println("fail set jar " + jarInput.file.getAbsolutePath()) FileUtils.copyFile(jarInput.file, dest) } } } }
4 结语 本文内容涉及知识较多,这里只讲了插件的实现,在熟悉Android打包过程、字节码、Gradle Transform API、ASM等之前,阅读起来会很困难。不过,在了解并学习这些知识的之后,相信你对Android会有新的认识。源码
参考资料函数插桩(Gradle + ASM) Android字节码插桩采坑笔记 打包Apk过程中的Transform API