Android studio项目编译期字节码插入实践Note
来源:互联网 发布:淘宝直通车宝贝标题 编辑:程序博客网 时间:2024/05/16 08:47
标签(空格分隔): Android
出发点
希望通过编译器的字节码插入,实现组件化项目,模块的生命周期初始化工作,在编码期完全不调用子模块的任何代码,包括子模块的生命周期初始化,达到完全解耦的目的.这个做法来自:Android彻底组件化方案实践,在作者的文中对这部分未做详细的说明,在文末贴出的gradle插件也还未做测试.但是在主要参考了: Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)一文后根据作者代码进行实践,终于达到目的,对gradle也有了进基本的了解,作笔记如下,代码主要复制于第二篇文章中.
实现
通过添加一个gradle插件添加一个构建过程中的TransformTask : transformClassesWithPreDexForDebug在这个task中获取到已经生成的class文件,使用javassist进行字节码直接插入.
步骤
几乎全部引用自AItsuki的文章
自定义一个plugin:
- 新建一个module,选择library module,module名字必须叫BuildSrc
删除module下的所有文件,除了build.gradle,替换build.gradle中的内容
apply plugin: 'groovy'repositories { jcenter()}dependencies { compile gradleApi() compile 'com.android.tools.build:gradle:2.3.3' compile 'org.javassist:javassist:3.20.0-GA'}
- 然后新建以下目录 src-main-groovy,同步
这时候就可以像普通module一样新建package和类了,不过这里的类是以groovy结尾,新建类的时候选择file,并且以.groovy作为后缀。
package com.longforusimport com.android.build.gradle.AppExtensionimport org.gradle.api.Pluginimport org.gradle.api.Projectpublic class TestPlugin implements Plugin<Project> { @Override public void apply(Project project) { project.logger.error "================自定义插件成功!==========" def android = project.extensions.findByType(AppExtension.class) android.registerTransform(new PreDexTransform(project))//调用自定义的transform进行 }}
5.在app module下的buiil.gradle中添apply 插件
import com.longforus.TestPluginapply plugin: 'com.android.application'apply plugin: TestPlugin//应用插件......
说明:如果plugin所在的module名不叫BuildSrc,这里是无法apply包名的,会提示找不到。所以之前也说明取名一定要叫buildsrc
运行一下项目就可以看到”================自定义插件成功!==========”这句话了
和gradle有关的输出都会显示在gradle console这个窗口中。
自定义Transfrom
新建一个groovy继承Transfrom,注意这个Transfrom是要com.android.build.api.transform.Transform这个包的
代码如下:
package com.longforusimport com.android.build.api.transform.*import com.android.build.gradle.internal.pipeline.TransformManagerimport com.android.utils.FileUtilsimport org.gradle.api.Projectimport org.apache.commons.codec.digest.DigestUtilspublic class PreDexTransform extends Transform { // http://blog.csdn.net/u010386612/article/details/51131642 Project project // 添加构造,为了方便从plugin中拿到project对象,待会有用 public PreDexTransform(Project project) { this.project = project } // Transfrom在Task列表中的名字 // TransfromClassesWithPreDexForXXXX @Override String getName() { return "preDex" } // 指定input的类型 @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } // 指定Transfrom的作用范围 @Override Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental() { return false } @Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { // inputs就是输入文件的集合 // outputProvider可以获取outputs的路径 // Transfrom的inputs有两种类型,一种是目录,一种是jar包,要分开遍历 inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> //TODO 这里可以对input的文件做处理,比如代码注入! Inject.injectDir(directoryInput.file.absolutePath)//调用方法进行注入 // 获取output目录 def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) // 将input的目录复制到output指定目录 FileUtils.copyDirectory(directoryInput.file, dest) } input.jarInputs.each { JarInput jarInput -> //TODO 这里可以对input的文件做处理,比如代码注入! String jarPath = jarInput.file.absolutePath; String projectName = project.rootProject.name; if(jarPath.endsWith("classes.jar") && jarPath.contains("exploded-aar\\"+projectName)//这里的路径在我的项目中并不存在, gradle版本不同可能已经不一样了 // hotpatch module是用来加载dex,无需注入代码 && !jarPath.contains("exploded-aar\\"+projectName+"\\hotpatch")) { Inject.injectJar(jarPath)//调用对jar进行注入的方法 } // 重命名输出文件(同目录copyFile会冲突) def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)// project.logger.error("dest = "+dest.absolutePath+"="+dest.exists())// project.logger.error("jarInput.file = "+jarInput.file.absolutePath+"="+jarInput.file.exists()) dest.mkdirs()//需要先创建文件才可以哦 dest.createNewFile() FileUtils.copyFile(jarInput.file, dest) } } }}
Clean项目运行就可以,在获取inputs复制到outpus目录之前,对class注入代码.
查看inputs和ouputs
在app module下的build.gradle的android节点中添加以下代码:
applicationVariants.all { variant->//输出 class文件的保存目录 def dexTask = project.tasks.findByName("transformClassesWithDexForDebug") def preDexTask = project.tasks.findByName("transformClassesWithPreDexForDebug") if(preDexTask) { project.logger.error "======preDexTask======" preDexTask.inputs.files.files.each {file -> project.logger.error "inputs =$file.absolutePath" } preDexTask.outputs.files.files.each {file -> project.logger.error "outputs =$file.absolutePath" } } if(dexTask) { project.logger.error "======dexTask======" dexTask.inputs.files.files.each {file -> project.logger.error "inputs =$file.absolutePath" } dexTask.outputs.files.files.each {file -> project.logger.error "outputs =$file.absolutePath" } } }
即可获取inputs和ouputs的目录我实际获取到的目录与原作者所说的不一样,可能是版本问题,未做深究.
使用javassist注入代码
- app module编译后class文件保存在debug目录,直接遍历这个目录使用javassist注入代码就行了
- app module依赖的module,编译后会被打包成jar,放在exploded-aar这个目录,需要将jar包解压–遍历注入代码–重新打包成jar
在插件中需要添加2个类,格式和上面的Transform一样:
操作javassist注入代码的inject类:
import com.longforus.JarZipUtilimport javassist.ClassPoolimport javassist.CtClassimport javassist.CtConstructorimport org.apache.commons.io.FileUtils/** * Created by AItsuki on 2016/4/7. * 注入代码分为两种情况,一种是目录,需要遍历里面的class进行注入 * 另外一种是jar包,需要先解压jar包,注入代码之后重新打包成jar */public class Inject { private static ClassPool pool= ClassPool.getDefault() /** * 添加classPath到ClassPool * @param libPath */ public static void appendClassPath(String libPath) { pool.appendClassPath(libPath) } /** * 遍历该目录下的所有class,对所有class进行代码注入。 * 其中以下class是不需要注入代码的: * --- 1. R文件相关 * --- 2. 配置文件相关(BuildConfig) * --- 3. Application * @param path 目录的路径 */ public static void injectDir(String path) { pool.appendClassPath(path) File dir = new File(path) if (dir.isDirectory()) { dir.eachFileRecurse { File file -> String filePath = file.absolutePath if (filePath.endsWith(".class") && !filePath.contains('R$') && !filePath.contains('R.class') && !filePath.contains("BuildConfig.class") // 这里是application的名字,可以通过解析清单文件获得,先写死了 && !filePath.contains("App.class")) { // 这里是应用包名,也能从清单文件中获取,先写死 int index = filePath.indexOf("com\\fec\\modifymethoddemo") if (index != -1) { int end = filePath.length() - 6 // .class = 6 String className = filePath.substring(index, end).replace('\\', '.').replace('/', '.') injectClass(className, path) } } } } } /** * 这里需要将jar包先解压,注入代码后再重新生成jar包 * @path jar包的绝对路径 */ public static void injectJar(String path) { if (path.endsWith(".jar")) { File jarFile = new File(path) // jar包解压后的保存路径 String jarZipDir = jarFile.getParent() + "/" + jarFile.getName().replace('.jar', '') // 解压jar包, 返回jar包中所有class的完整类名的集合(带.class后缀) List classNameList = JarZipUtil.unzipJar(path, jarZipDir) // 删除原来的jar包 jarFile.delete() // 注入代码 pool.appendClassPath(jarZipDir) for (String className : classNameList) { if (className.endsWith(".class") && !className.contains('R$') && !className.contains('R.class') && !className.contains("BuildConfig.class")) { className = className.substring(0, className.length() - 6) injectClass(className, jarZipDir) } } // 从新打包jar JarZipUtil.zipJar(jarZipDir, path) // 删除目录 FileUtils.deleteDirectory(new File(jarZipDir)) } } private static void injectClass(String className, String path) { println(path) CtClass c = pool.getCtClass(className) if (c.isFrozen()) { c.defrost() } println(className) if (c.name.contains("MainActivity")) { for (int i = 0; i < c.declaredMethods.size(); i++) { def method = c.declaredMethods[i] println(method.name) if (method.name.contains("init")){ method.insertAfter("com.fec.modifymethoddemo.Printer.print(\"测试插入\",mContext);") println("插入成功")//测试成功的插入代码 } } } /*CtConstructor[] cts = c.getDeclaredConstructors() if (cts == null || cts.length == 0) { insertNewConstructor(c) } else { cts[0].insertBeforeBody("System.out.println(123123);") }*/ c.writeFile(path) c.detach() } private static void insertNewConstructor(CtClass c) { CtConstructor constructor = new CtConstructor(new CtClass[0], c) constructor.insertBeforeBody("System.out.println(321321);") c.addConstructor(constructor) }}
解压缩jar包的类:
package com.longforusimport java.util.jar.JarEntryimport java.util.jar.JarFileimport java.util.jar.JarOutputStreamimport java.util.zip.ZipEntry/** * Created by hp on 2016/4/13. */public class JarZipUtil { /** * 将该jar包解压到指定目录 * @param jarPath jar包的绝对路径 * @param destDirPath jar包解压后的保存路径 * @return 返回该jar包中包含的所有class的完整类名类名集合,其中一条数据如:com.aitski.hotpatch.Xxxx.class */ public static List unzipJar(String jarPath, String destDirPath) { List list = new ArrayList() if (jarPath.endsWith('.jar')) { JarFile jarFile = new JarFile(jarPath) Enumeration<JarEntry> jarEntrys = jarFile.entries() while (jarEntrys.hasMoreElements()) { JarEntry jarEntry = jarEntrys.nextElement() if (jarEntry.directory) { continue } String entryName = jarEntry.getName() if (entryName.endsWith('.class')) { String className = entryName.replace('\\', '.').replace('/', '.') list.add(className) } String outFileName = destDirPath + "/" + entryName File outFile = new File(outFileName) outFile.getParentFile().mkdirs() InputStream inputStream = jarFile.getInputStream(jarEntry) FileOutputStream fileOutputStream = new FileOutputStream(outFile) fileOutputStream << inputStream fileOutputStream.close() inputStream.close() } jarFile.close() } return list } /** * 重新打包jar * @param packagePath 将这个目录下的所有文件打包成jar * @param destPath 打包好的jar包的绝对路径 */ public static void zipJar(String packagePath, String destPath) { File file = new File(packagePath) JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(destPath)) file.eachFileRecurse { File f -> String entryName = f.getAbsolutePath().substring(packagePath.length() + 1) outputStream.putNextEntry(new ZipEntry(entryName)) if(!f.directory) { InputStream inputStream = new FileInputStream(f) outputStream << inputStream inputStream.close() } } outputStream.close() }}
clean 再运行就能注入成功了.
插件的debug
在上面的项目中我想要在插件的代码中打断点,方便调试,但是以前对插件都没有过多的了解,很单纯的和项目代码一样点上bug,就点击调试运行,结果的无法进入断点的.搜索资料加实践后成功进入断点,参考了Intellij / Android Studio 调试 Gradle Plugin这篇文章,在原文的基础上多次尝试,写了一个bat文件在需要断点调试时运行:
@rem 只有在clean之后才会进入断点call gradlew cleancall gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=truepause>nul
放在项目根目录下,打好断点->运行bat->点击远程任务的调试启动 即可进入断点.
- Android studio项目编译期字节码插入实践Note
- Android Note-android studio 无法创建android项目
- Android Studio项目Gradle构建实践
- 初识Android studio与简单项目实践
- android studio编译android项目时出错
- Android Note项目
- [note]android studio创建工程
- Android Studio编译开源项目
- Android Studio 编译项目出错的解决办法
- android studio编译项目时出错
- Android Studio修改项目编译版本
- cocos2d-x 编译Android-Studio项目
- Android Studio导入eclipse项目编译出错
- Android Studio提升项目的编译速度
- Android studio 项目构建三|编译缓存
- Android Studio NDK项目编译备忘录
- android studio NDK使用,编译c生成.so实践记录
- android studio NDK使用,编译c生成.so实践记录
- SQL查询和优化(五)
- Linux 查看CPU信息,机器型号,内存等信息
- Android写文件报read only file system
- Shredding Company (dfs)
- css3实现从左右两边以动画的形式分别插入文字和图片
- Android studio项目编译期字节码插入实践Note
- 如何使用vs在调试时查看内存
- SQL语言
- 内核是如何管理进程的
- com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:112) [DUBBO
- 【C语言】循环队列
- 最长无重复字符子串
- Java面试题之五
- 历史总是会重复的-AROON(阿隆指标)策略