插件开发:
如在build.gradle(App)中,声明使用自定义插件
apply plugin: 'com.hyu.asmpatch' patch{ debugOn true applicationName "com.example.mytest.App" }
a.在项目中新建一个java-library项目(buildSrc)去实现此插件,在src/main/文件夹中新建resources/META-INF/gradle-plugins/com.hyu.asmpatch.properties文件,并添加以下代码声明;
//声明此插件名"com.hyu.asmpatch"的实现代码入口 implementation-class=com.example.patch.PatchPlugin
b.添加此插件的依赖
//代码中要计算md5值,在libs文件夹下添加commons-codec.jar dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.tools.build:gradle:4.2.1' }
c.新建插件的扩展,即面向用户可自定义的入参
public class PatchExtension { //是否在debug模式下处理 private boolean debugOn; //输出路径 private String output; //application名称 private String applicationName; public PatchExtension() { this.debugOn = false; } public boolean isDebugOn() { return debugOn; } public void setDebugOn(boolean debugOn) { this.debugOn = debugOn; } public String getOutput() { return output; } public void setOutput(String output) { this.output = output; } public String getApplicationName() { return applicationName; } public void setApplicationName(String applicationName) { this.applicationName = applicationName; } }
d.gradle打包的流程,gradle打包时即执行多个任务,如预编译,打包资源文件、编译java文件为class文件,将多个class文件打包成dex文件等。正是由于这么多的任务执行后才打包成最后的apk文件;而我们自定义的gradle插件,就是在android插件执行打包的各个任务执行的前后过程中进行监听,插入自已的代码,如这次我们实现的:对生成的class文件进行字节码插桩,然后再将插桩完成的class文件重新放入原位置,这样最后生成的apk文件里的dex文件就是经过我们插桩过的代码。
package com.example.patch; import com.android.build.gradle.AppExtension; import com.android.build.gradle.AppPlugin; import com.android.build.gradle.api.ApplicationVariant; import com.android.tools.r8.utils.F; import com.android.utils.FileUtils; import com.sun.jna.platform.unix.solaris.LibKstat; import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.FileCollection; import org.gradle.api.provider.Property; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; public class PatchPlugin implements Plugin<Project> { private Project project; private boolean isDebugOn; private String applicationName; private String output; private String pkgName; private PatchGenerator patchGenerator; @Override public void apply(Project project) { //判断如果没有apply com.android.application就不能使用我们的插件 if(!project.getPlugins().hasPlugin(AppPlugin.class)){ throw new GradleException("can not use com.hyu.asmpatch in unused application plugin "); } this.project = project; //创建一个patch{}配置,供gradle文件中配置PatchExtension对象参数,定义插件名为patch, //类似Android项目使用的插件为android //就和引入了 apply plugin: 'com.android.application' 一样,可以配置android{} project.getExtensions().create("patch",PatchExtension.class); //gradle执行会解析build.gradle文件,afterEvaluate表示在解析完成之后才能拿到参数, // 再执行我们的代码 project.afterEvaluate(new Action<Project>() { @Override public void execute(Project project) { //获取扩展参数 final PatchExtension patchExtension = project.getExtensions().getByType(PatchExtension.class); applicationName = patchExtension.getApplicationName(); isDebugOn = patchExtension.isDebugOn(); output = patchExtension.getOutput(); //得到android插件扩展参数 final AppExtension appExtension = project.getExtensions().getByType(AppExtension.class); // android项目默认会有 debug和release, // 那么getApplicationVariants就是包含了debug和release的集合,all表示对集合进行遍历 appExtension.getApplicationVariants().all(new Action<ApplicationVariant>() { @Override public void execute(ApplicationVariant applicationVariant) { //获取包名: pkgName = applicationVariant.getGenerateBuildConfigProvider().get().getAppPackageName().get(); //String a = applicationVariant.getProductFlavors().get(0).getApplicationIdSuffix(); String b= applicationVariant.getGenerateBuildConfig().getAppPackageName().get(); System.out.println("a========"+pkgName); System.out.println("b========"+b); //variantName==debug/release String variantName = applicationVariant.getName(); System.out.println("variantName="+variantName); //配置热修复插件生成补丁的一系列任务 configTasks(project, applicationVariant, patchExtension); } }); } }); } private void configTasks(Project project, ApplicationVariant applicationVariant, PatchExtension patchExtension) { String variantName = applicationVariant.getName(); //首字母大写 String VariantName = Utils.capitalize(variantName); File outputDir; //如果没有指名输出目录,默认输出到 build/patch/debug(release) 下 if(!Utils.isEmpty(output)){ outputDir = new File(output,variantName); }else{ outputDir = new File(project.getBuildDir(),"patch/"+variantName); } outputDir.mkdirs(); Task proguardTask = null; File mappingBak = null; if(variantName!=null && variantName.equals("release")){ //获得android的混淆任务 //proguardTask = project.getTasks().getByName("transformClassesAndResourcesWithProguardFor"+VariantName); proguardTask = project.getTasks().getByName("merge"+VariantName+"GeneratedProguardFiles"); //备份本次生成的mapping文件 mappingBak = new File(outputDir,"mapping.txt"); //如果没开启混淆则为Null,不需要备份 if(proguardTask != null){ // dolast:在这个任务之后再干一些事情 // 在混淆后备份mapping File finalMappingBak = mappingBak; proguardTask.doLast(new Action<Task>() { @Override public void execute(Task task) { Set<File> files = task.getOutputs().getFiles().getFiles(); for(File file : files){ if(file.getName().endsWith("mapping.txt")){ try { //复制编译的mapping.txt文件到mappingBak文件 FileUtils.copyFile(file, finalMappingBak); project.getLogger().info("复制mapping.txt文件到"+ finalMappingBak.getCanonicalPath()); }catch (Exception e){ e.printStackTrace(); } } } } }); } } //将上次混淆的mapping应用到本次,如果没有上次的混淆文件就没操作 Utils.applyMapping(proguardTask, mappingBak); //记录类的hash值,并生成补丁包 final File hexFile = new File(outputDir,"hex.txt"); //需要打包成补丁的类 final File patchClassFile = new File(outputDir,"patchClass.jar"); //用dx打包后的jar包:补丁包 final File patchFile = new File(outputDir,"patch.jar"); final Task dexTask = project.getTasks().getByName("dexBuilder"+VariantName); //final Task dexTask = project.getTasks().getByName("mergeProjectDexDebug"); //在打包之前执行,即可以拿到经过编译的类的class文件 dexTask.doFirst(new Action<Task>() { @Override public void execute(Task task) { patchGenerator = new PatchGenerator(project, patchFile, patchClassFile, hexFile); /** * 插桩记录md5值 */ Map<String,String> newHexMap = new HashMap<>(); Set<File> inputFiles = dexTask.getInputs().getFiles().getFiles(); for(File file : inputFiles){ String filePath = file.getAbsolutePath(); //会得到各种资源Jar包及我们代码打包的class文件夹,我们仅需对我们自已代码生成的class文件进行处理 if(filePath.endsWith("\\classes")){ try { processClassFile(file,applicationVariant.getDirName(), newHexMap); }catch (Exception e){ e.printStackTrace(); } } //类的md5集合 写入到文件 Utils.writeHex(newHexMap, hexFile); try { //生成补丁 patchGenerator.generate(); } catch (Exception e) { e.printStackTrace(); } } } }); } private void processClassFile(File file, String dirName, Map hexFile) throws IOException { String fileDirPath = file.getAbsolutePath(); String convertPkg = pkgName.replace(".","\\"); File classFileDir = new File(fileDirPath+"\\"+convertPkg); File[] files = classFileDir.listFiles(); //得到class文件夹下的class文件 for(int i= 0;i<files.length;i++){ File classFile = files[i]; String classFilePath = classFile.getAbsolutePath(); System.out.println("classFilePath="+classFilePath); System.out.println("fileDirPath="+String.valueOf(fileDirPath)); String classFilePathDot = classFilePath.replace("\\","."); String fileDirPathDot = fileDirPath.replace("\\","."); //得到class文件的文件名:com.example.mytest.MainActivity2.class String className = classFilePathDot.split(fileDirPathDot+".")[1]; if(className != null && className.equals("com.example.mytest.MainActivity2.class")){ try { System.out.println("exec className="+className); FileInputStream is = new FileInputStream(classFilePath); //执行插桩 byte[] byteCode = ClassUtils.referHackWhenInit(is); String hex = Utils.hex(byteCode); is.close(); FileOutputStream os = new FileOutputStream(classFilePath); os.write(byteCode); os.close(); hexFile.put(className, hex); //对比缓存的md5,不一致则放入补丁 patchGenerator.checkClass(className, hex, byteCode); } catch (Exception e) { e.printStackTrace(); } } } } }
e.具体执行插桩的代码:
public class ClassUtils { public static byte[] referHackWhenInit(InputStream inputStream) throws IOException { ClassReader cr = new ClassReader(inputStream); ClassWriter cw = new ClassWriter(cr, 0); ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) { @Override public MethodVisitor visitMethod(int access, final String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); mv = new MethodVisitor(api, mv) { @Override public void visitInsn(int opcode) { //在构造方法中插入AntilazyLoad引用 if ("<init>".equals(name) && opcode == Opcodes.RETURN) { super.visitLdcInsn(Type.getType("Lcom/example/hack/AntilazyLoad;")); } super.visitInsn(opcode); } }; return mv; } }; cr.accept(cv, 0); return cw.toByteArray(); } }