Reference 与插件化区别 补丁包中的类和资源在宿主中已经存在,只是有 bug 需要被修复。
各框架实现原理 Andfix (兼容复杂已废弃) Java 中的类,方法,变量,对应到虚拟机里的实现是 Class,ArtMethod,ArtField。Andfix 是把旧方法的 ArtMethod 内容替换成新方法的 ArtMethod 内容,再次调用旧方法,就会跳转到新方法的入口。
Qzone(dex 插桩) Qzone 基于的是 dex 分包方案。把有 bug 的方法修复以后,放到一个单独的 dex 补丁文件,让程序运行期间加载 dex 补丁,执行修复后的方法。
Robust(方法重定向) 对每个函数都在编译打包阶段自动的插入了一段代码。类似于代理,将方法执行的代码重定向到其他方法中。
Tinker Tinker 通过计算对比指定的 base apk 中的 dex 与修改后的 apk 中的 dex 的区别,补丁包中的内容即为两者差分的描述。运行时将 base apk 中的 dex 与补丁包进行合成,重启后加载全新的合成后的 dex 文件。
Qzone 需要解决的问题 Dalvik 虚拟机中 CLASS_ISPREVERIFIED 导致的校验失败 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 E/AndroidRuntime(20525 ): FATAL EXCEPTION: main E/AndroidRuntime(20525 ): java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation E/AndroidRuntime(20525 ): at zeus.test.hotfix.TestHotFixActivity.onCreate(Unknown Source) E/AndroidRuntime(20525 ): at android.app.Activity.performCreate(Activity.java:5188 ) E/AndroidRuntime(20525 ): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1094 ) E/AndroidRuntime(20525 ): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2074 ) E/AndroidRuntime(20525 ): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2135 ) E/AndroidRuntime(20525 ): at android.app.ActivityThread.access$700 (ActivityThread.java:140 ) E/AndroidRuntime(20525 ): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1237 ) E/AndroidRuntime(20525 ): at android.os.Handler.dispatchMessage(Handler.java:99 ) E/AndroidRuntime(20525 ): at android.os.Looper.loop(Looper.java:137 ) E/AndroidRuntime(20525 ): at android.app.ActivityThread.main(ActivityThread.java:4921 ) E/AndroidRuntime(20525 ): at java.lang.reflect.Method.invokeNative(Native Method) E/AndroidRuntime(20525 ): at java.lang.reflect.Method.invoke(Method.java:511 ) E/AndroidRuntime(20525 ): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1038 ) E/AndroidRuntime(20525 ): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:805 ) E/AndroidRuntime(20525 ): at dalvik.system.NativeStart.main(Native Method)
如果一个类和它直接引用的类都在同一个 dex 中的话,那么这个类就会被打上 CLASS_ISPREVERIFIED 标记,而补丁包中的类存在于另一个 dex,不能满足 CLASS_ISPREVERIFIED 导致校验失败,所以需要避免类被打上 CLASS_ISPREVERIFIED 标记。
解决办法:
1、创建一个 dex;
比如随便创建一个 AntilazyLoad.java。
1 2 public class AntilazyLoad { }
make module 将 java 文件编译成 class 文件。
执行命令编译成 dex 文件。
1 dx --dex --output=hack.dex com\enjoy\patch\hack\AntilazyLoad.class
2、让每个类都引用这个 dex。
将 hack.dex 放入 assets 目录。
借助 gradle,在 compileDebugJavaWithJavac 任务后,且在 transformClassesWithDexBuilderForDebug 任务前,修改每个类。
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 afterEvaluate ({ android.getApplicationVariants.all { variant-> String variantName = variant.getName() String capitalizedName = variantName.capitalize() Task dexTask = project .getTasks().findByName("transformClassesWithDexBuilderFor" + capitalizedName) dexTask.doFirst { Set<File > files = dexTask.getInputs().getFiles().getFiles() for (File file : files) { String filePath = file .getAbsolutePath() if (filePath.endsWith(".jar" )) { processJar(file ) } else if (filePath.endsWith(".class" )) { processClass(variant.getDirName(), file ) } } } } })static void processClass(String dirName, File file ) { String filePath = file .getAbsolutePath() String className = filePath.split(dirName)[1 ].subString(1 ) println className if (className.startsWith("com\\hotfix\\MyApplication" ) || isAndroidClass(className)){ return } try { FileInputStream is = new FileInputStream(filePath) byte [] byteCode = referHackWhenInit(is) is.close() FileOutputStream os = new FileOutputStream(filePath) os.write (byteCode) os.close() } catch (Exception e) { e.printStackTrace() } }static byte [] referHackWhenInit(InputStream inputStream) throws IOException { ClassReader cr = new ClassReader(inputStream) ClassWriter cw = new ClassWirter(cr, 0 ) ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) { @Overide 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(Opcodes.ASM5, mv) { @Override public void visitInsn(int opcode) { if ("<init>" .equals(name) && opcode == Opcodes.RETURN ) { super .visitInsn(Type.getType("LpackageName/AntilazyLoad;" )) } super .visitInsn(opcode) } } return mv } } cr.accept(cv, 0 ) return cw.toByteArray() }
自定义插件实现插桩并生成补丁包 1、在 project 下新建名为 buildSrc 的 Directory(不是新建 Module);
2、build 编译项目后,会在 buildSrc 下生成 .gradle 文件夹和 build 文件夹;
3、拷贝一个 Module 的 build.gradle 文件放入 buildSrc 文件夹,并引入 gradleApi;
1 2 3 4 5 6 7 8 9 10 apply plugin: 'java-library' dependencies { implementation fileTree (dir: 'libs' , include : ['*.jar' ]) implementation gradleApi() }sourceCompatibility = "1.7" targetCompatibility = "1.7"
4、在 buildSrc 下新建 src/main/java 目录,用于存放插件的 Java 源代码;接着在该 src/main/java 目录下新建一个 Java 包,比如 com.enjoy.plugin 包;接着创建一个 PatchPlugin 类,PatchPlugin 作为插件类须实现 Plugin接口,并实现 apply() 方法;
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 package com.enjoy.plugin;import org.gradle.api.Plugin;import org.gradle.api.Project;public class PatchPlugin implements Plugin <Project> { @Oveerride public void apply (Project project) { if (!project.getPlugins().hasPlugin(AppPlugin.class)) { throw new GradleException ("无法在非android application插件中使用热修复插件" ); } project.getExtensions().create("patch" , PatchExtension.class); project.afterEvaluate(new Action <Project>() { @Override public void execute (final Project project) { final PatchExtension patchExtension = project.getExtensions().findByType(PatchExtension.class); final boolean debugOn = patchExtension.debugOn; AppExtension android = project.getExtensions().getByType(AppExtension.class); android.getApplicationVariants().all(new Action <ApplicationVariant>() { @Override public void execute (ApplicationVariant applicationVariant) { if (applicationVariant.getName().contains("debug" ) && !debugOn) { return ; } configTasks(project, applicationVariant, patchExtension); } }); } }); } }
5、在 app/build.gradle 中引用插件后,gradle 编译时就会执行插件的 apply() 方法;
1 apply plugin: com.enjoy.patch.plugin.PatchPlugin
6、暴露一些用户可配置的参数;
新建 PatchExtension 类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class PatchExtension { boolean debugOn; String output; String applicationName; public PatchExtension () { debugOn = false ; } public void setDebugOn (boolean debugOn) { this .debugOn = debugOn; } public void setOutput (String output) { this .output = output; } public void setApplicationName (String applicationName) { this .applicationName = applicationName; } }
在 gradle 中使用。
1 2 3 4 patch { debugOn true applicationName 'com.enjoy.qzonefix.MyApplication' }
PatchPlugin 设置 JavaBean 为 PatchExtension,并使用传递进来的参数。
1 2 3 project.getExtensions().create("patch" , PatchExtension.class);final PatchExtension patchExtension = project.getExtensions().findByType(PatchExtension.class);
7、保存 mapping 文件,给以后补丁包用;
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 final Task proguardTask = project.getTasks().findByName("transformClassesAndResourcesWithProguardFor" + capitalizeName);final File mappingBak = new File (outputDir, "mapping.txt" );if (proguardTask != null ) { proguardTask.doLast(new Action <Task>() { @Override public void execute (Task task) { TaskOutputs outputs = proguardTask.getOutputs(); Set<File> files = outputs.getFiles().getFiles(); for (File file : files) { if (file.getName().endsWith("mapping.txt" )) { try { FileUtils.copyFile(file, mappingBak); project.getLogger().error("备份混淆mapping文件:" + mappingBak.getCanonicalPath()); } catch (IOException e) { e.printStackTrace(); } break ; } } } }); }if (mappingBak.exists() && proguardTask != null ) { TransformTask task = (TransformTask) proguardTask; ProGuardTransform transform = (ProGuardTransform) task.getTransform(); transform.applyTestedMapping(mappingBak); }
8、记录 MD5,并生成补丁包。
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 final File hexFile = new File (outputDir, "hex.txt" );final File patchClassFile = new File (outputDir, "patchClass.jar" );final File patchFile = new File (outputDir, "patch.jar" );final Task dexTask = project.getTasks().findByName("transformClassesWithDexBuilderFor" + capitalizeName); dexTask.doFirst(new Action <Task>() { @Override public void execute (Task task) { PatchGenerator patchGenerator = new PatchGenerator (project, patchFile, patchClassFile, hexFile); String applicationName = patchExtension.applicationName; applicationName = applicationName.replaceAll("\\." , Matcher.quoteReplacement(File.separator)); Map<String, String> newHexs = new HashMap <>(); Set<File> files = dexTask.getInputs().getFiles().getFiles(); for (File file : files) { String filePath = file.getAbsolutePath(); if (filePath.endsWith(".jar" )) { processJar(applicationName, file, newHexs, patchGenerator); } else if (filePath.endsWith(".class" )) { processClass(applicationName, variant.getDirName(), file, newHexs, patchGenerator); } } Utils.writeHex(newHexs, hexFile); try { patchGenerator.generate(); } catch (Exception e) { e.printStackTrace(); } } });
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 static void processClass (String applicationName, String dirName, File file, Map<String, String> hexs, PatchGenerator patchGenerator) { String filePath = file.getAbsolutePath(); String className = filePath.split(dirName)[1 ].substring(1 ); if (className.startsWith(applicationName) || Utils.isAndroidClass(className)) { return ; } try { FileInputStream is = new FileInputStream (filePath); byte [] byteCode = ClassUtils.referHackWhenInit(is); String hex = Utils.hex(byteCode); is.close(); FileOutputStream os = new FileOutputStream (filePath); os.write(byteCode); os.close(); hexs.put(className, hex); patchGenerator.checkClass(className, hex, byteCode); } catch (Exception e) { e.printStackTrace(); } }static void processJar (String applicationName, File file, Map<String, String> hexs, PatchGenerator patchGenerator) { try { applicationName = applicationName.replaceAll(Matcher.quoteReplacement(File.separator), "/" ); File bakJar = new File (file.getParent(), file.getName() + ".bak" ); JarOutputStream jarOutputStream = new JarOutputStream (new FileOutputStream (bakJar)); JarFile jarFile = new JarFile (file); Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); jarOutputStream.putNextEntry(new JarEntry (jarEntry.getName())); InputStream is = jarFile.getInputStream(jarEntry); String className = jarEntry.getName(); if (className.endsWith(".class" ) && !className.startsWith(applicationName) && !Utils.isAndroidClass(className) && !className.startsWith("com/enjoy" + "/patch" )) { byte [] byteCode = ClassUtils.referHackWhenInit(is); String hex = Utils.hex(byteCode); is.close(); hexs.put(className, hex); patchGenerator.checkClass(className, hex, byteCode); jarOutputStream.write(byteCode); } else { jarOutputStream.write(IOUtils.toByteArray(is)); } jarOutputStream.closeEntry(); } jarOutputStream.close(); jarFile.close(); file.delete(); bakJar.renameTo(file); } catch (IOException e) { e.printStackTrace(); } }
ART 虚拟机在 Android 7.0 时因优化安装速度导致的无法插桩 应用在安装时不做编译,而是运行时解释字节码,同时在 JIT 编译了一些热点代码后将这些代码信息记录至 Profile 文件,等到设备空闲的时候使用 AOT(All-Of-the-Time compilation:全时段编译)编译生成称为 app_image 的 base.art(类对象映像)文件,这个 art 文件会在 app 启动时自动加载(相当于缓存)。根据类加载原理,类被加载后无法被替换,即无法修复。
解决办法: 1、因为 app_image 中的 class 会插入到 PathClassloader,所以不使用 PathClassloader,而是自定义 Classloader,并通过反射修改引用 PathClassloader 的地方为自定义的 Classloader;
2、参考 app_image 生成方式,生成修复后的 app_image。