市面上目前app的热修复技术众多,比较主流的有: 1)腾讯系:微信的Thinker、QQ空间的超级补丁、手Q的QFix 2)阿里系:AndFix、阿里百川HotFix、Sophix 3)美团:Robust 4)饿了么:Amigo 5)美丽说蘑菇街:Aceso
1、阿里系
名称说明AndFix开源,实时生效,最新更新是3年前HotFix阿里百川,未开源,免费、实时生效Sophix未开源,商业收费,实时生效/冷启动修复HotFix是AndFix的优化版本,Sophix是HotFix的优化版本。目前阿里系主推是Sophix。 2、腾讯系
名称说明Qzone超级补丁QQ空间,未开源,冷启动修复QFix手Q团队,未开源,冷启动修复Tinker微信团队,开源,冷启动修复。提供分发管理,基础版免费 ,持续更新3、其他
名称说明Robust美团, 开源,实时修复,支持到android 9.0,持续更新Nuwa大众点评,开源,冷启动修复,最新更新是4年前Amigo饿了么,开源,冷启动修复,仅支持到android 7.1,最新更新是2年前4、方案对比
方案对比SophixTinkernuwaAndFixRobustAmigo类替换yesyesyesnonoyesSo替换yesyesnononoyes资源替换yesyesyesnonoyes全平台支持yesyesyesnoyesyes即时生效同时支持nonoyesyesno性能损耗较少较小较大较小较小较小补丁包大小小较小较大一般一般较大开发透明yesyesyesnonoyes复杂度傻瓜式接入复杂较低复杂复杂较低Rom体积较小Dalvik较大较小较小较小大成功率高较高较高一般最高较高热度高高低低高低开源noyesyesyesyesyes收费收费(设有免费阈值)收费(基础版免费,但有限制)免费免费免费免费监控提供分发控制及监控提供分发控制及监控nononono总体而言,热修复技术方案主要分为3类: 1)类加载方案(参考android multidex的思想):腾讯系 2)底层替换方案(参考xposed框架的思想):阿里系 3)Instant Run方案(参考Android Studio热部署思想):美团
具体分析可以阅读以下技术博客: 1、Android热修复技术原理详解(最新最全版本) 2、Android热修复原理(一)热修复框架对比和代码修复 3、Android热更新方案Robust–Instant Run代码插桩方案 4、android热修复相关之Multidex解析 5、热修复——深入浅出原理与实现–类加载方案原理 6、Android 冷启动热修复技术杂谈-QQ热修复原理 7、Android 热修复原理-类加载方案原理 8、Android热修复原理-各热修复框架原理
1、首先直接pass掉native hook底层替换方案,这个方案对android版本与机型的适配兼容工作量太大,不适合sdk的开发 2、Instant Run代码预插桩方案,这个方案需要在每个方法前插入判断跳转逻辑的代码,对代码的侵入太大,而且代码进行混淆的话,出补丁比较麻烦 3、最终选用了类加载方案,可以实现类级别与方法级别的修复,同时兼容性也是比较高的方案,毕竟是参考android官方multidex的实现思想,不过网上能够搜索到的类加载方案普遍都会有问题 1)问题点一: 即使加载到补丁的dex插入到dexpathList数组第一位,但是代码依然还是走的是旧的代码逻辑 2)问题点二: 在android P以上的机子,会出现以下报错:
06-20 19:07:24.597 30376 30376 F m.taobao.taoba:entrypoint_utils-inl.h:94] Inlined method resolution crossed dex file boundary: from void com.ali.mobisecenhance.Init.doInstallCodeCoverage (android.app.Application, android.content.Context) in/data/app/com.taobao.taobao-YPDeV7WbuyZckOfy-5AuKw==/base.apk!classes3.dex/0xece238f0to void com.ali.mobisecenhance.code.CodeCoverageEntry.CoverageInit (android.app.Application, android.content.Context) in/data/user/0/com.taobao.taobao/files/storage/com.taobao.maindex /dexpatch/1111/com_taobao_maindex.zip!classes4.dex/0xebda4320. This must be due to duplicate classes or playing wrongly with class loaders上述就是在Android P上被内联的方法不能在不同的dex(classN.dex为同一个dex)导致的闪退,内联相关知识:ART下的方法内联策略及其对Android热修复方案的影响分析 3)问题点三: 在dalvik虚拟机的手机(android 4.4之前的机子)会出现UNEXPECT_DEX_EXCEPTION 4、原因分析 1)问题点一在于app首次运行时,会优化生成对应的运行代码缓存,所以之后再加入新的补丁也不会进行调用 2)问题点二在于android P会优化调用逻辑,对同一个dex的方法调用进行内联处理,要是执行热修复之后,假如检测被内联的方法不是在同一个dex就会抛出异常 3)问题点三:这个主要是是在dalvik虚拟机有的问题,在android 5.0+的机子正常CLASS_PRIVEREIED问题
5、解决方案 1)对后续需要进行热修复的那部分代码,先生成一个dex文件放到assets目录下 2)在app首次打开时候,就预先加载放在assets这个dex,插入到dexpathList数组第一位,这样系统就会记得这部分代码是需要依赖外部dex,不会对这部分代码进行优化缓存,就可以避免后续下发补丁的时候不起作用 3)同时,因为首次启动需要热修复的那部分代码跟其他代码也不是在同一个dex,故也不会被系统自动内联,这样解决了问题点二 4)只要首次加载一次即可,后续可不再加载那个生成的dex,有新的补丁时再加载即可,不过测试发现首次加载只会耗时100+ms,第二次后续也就10ms以内,不会太影响启动速度 6、关键代码片段
public class Fettler { private static final String TAG = "min77"; private HashSet<File> fixDexSet; private Context context; private FixListener listener; public static boolean DEBUG = false; private Fettler(Context context) { fixDexSet = new HashSet<>(); this.context = context; } /** * 构造Fettler对象,初始化成员变量 */ public static Fettler with(Context context) { return new Fettler(context); } /** * 初始化,在application的attachBaseContext()调用 */ public static void init(Context context) { with(context).start(); } /** * 清理磁盘缓存的dex文件(慎用,会导致需要修复的dex文件失效) */ public static void clear(Context context) { with(context).clear(); } /** * 添加补丁包 */ public Fettler add(String dexPath) { File dexFile = new File(dexPath); File targetFile = new File(context.getDir(Constants.TEMP_FOLDER, Context.MODE_PRIVATE) + File.separator + dexFile.getName()); if (targetFile.exists()) targetFile.delete(); FileUtils.copy(dexFile, targetFile); Log.i(TAG, "===== 成功添加 " + targetFile.getName() + " ====="); return this; } /** * 添加补丁包 */ public Fettler add(File dexFile) { File targetFile = new File(context.getDir(Constants.TEMP_FOLDER, Context.MODE_PRIVATE) + File.separator + dexFile.getName()); if (targetFile.exists()) targetFile.delete(); FileUtils.copy(dexFile, targetFile); Log.i(TAG, "===== 成功添加 " + targetFile.getName() + " ====="); return this; } /** * 添加监听 */ public Fettler listen(FixListener listener) { this.listener = listener; return this; } /** * 热修复 */ public void start() { Log.i(TAG, "===== 开始修复 ====="); fixDexSet.clear(); //清理集合 File externalFilesDir = context.getExternalFilesDir(null); File dexDir = new File(externalFilesDir, "sswl"); if (!dexDir.exists()) { dexDir.mkdir(); } Log.i("min77", "dexDir = " + dexDir.getAbsolutePath()); if (dexDir != null && dexDir.listFiles() != null && dexDir.listFiles().length > 0) {//sswl有文件,不管是否是.dex文件都不会走else Log.i("min77", "dexDir != null && dexDir.listFiles() != null"); //遍历所有dex文件添加到集合 for (File dex : dexDir.listFiles()) { String fileName = dex.getName(); Log.i("min77", "dexDir.listFiles() fileName = " + fileName); //非dex文件 if (fileName.endsWith(Constants.DEX_SUFFIX) && !fileName.equals(Constants.MAIN_DEX_NAME)) fixDexSet.add(dex); } } else { String fileName = "sswl.dex"; File dexFile = new File(dexDir, fileName); if (!dexFile.exists()) { Log.i("min77", "copyAssetsFileToStorage"); File dex = copyAssetsFileToStorage(fileName); if (dex != null) { fixDexSet.add(dex); } } else { fixDexSet.add(dexFile); } } //开始插桩修复 createDexClassLoader(dexDir); } /** * stream方式 */ public File copyAssetsFileToStorage(String fileName) { FileOutputStream fos = null; InputStream is = null; try { AssetManager assetManager = context.getAssets(); is = assetManager.open(fileName); File externalFilesDir = context.getExternalFilesDir(null); File dexDir = new File(externalFilesDir, "sswl"); File dexFile = new File(dexDir, fileName); fos = new FileOutputStream(dexFile); // 使用byte数组读取方式,缓存1KB数据 byte[] buffer = new byte[1024 * 300]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } fos.flush(); Log.i("min77", dexFile.getAbsolutePath() + "拷贝完毕"); return dexFile; } catch (IOException e) { Log.e("min77", "copyAssetsFileToStorage error :" + e.getMessage()); e.printStackTrace(); } finally { try { if (fos != null) { fos.close(); } if (is != null) { is.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; } /** * 创建自有类加载器生成dexElements对象 */ private void createDexClassLoader(File dexDir) { try { //创建临时解压目录 File filesDir = context.getFilesDir(); File optimizedDirectory = new File(filesDir, "sswl_optimizedDirectory"); // File tempDir = context.getDir(Constants.TEMP_UNZIP_FOLDER, Context.MODE_PRIVATE); Log.i("min77", "dex : " + dexDir.getAbsolutePath()); if (!optimizedDirectory.exists()) optimizedDirectory.mkdirs(); //遍历dex集合进行插桩修复 for (File dex : fixDexSet) { Log.i(TAG, "===== 正在修复 " + dex.getAbsolutePath() + " ====="); DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDirectory.getAbsolutePath(), null, context.getClassLoader()); hotFix(classLoader); } if (listener != null) listener.onComplete(); Log.i(TAG, "===== 修复完成 ====="); } catch (Throwable e) { e.printStackTrace(); } } /** * 插桩修复 */ private void hotFix(DexClassLoader loader) { try { //获取自有类加载器中的dexElements对象 Object patchElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(loader)); //获取系统类加载器中的dexElements对象 Object oldElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(context.getClassLoader())); //合并dexElements数组 Object newElements = ArrayUtils.merge(patchElements, oldElements); //获取系统类加载器中的pathList对象 Object pathList = ReflectUtils.getPathList(context.getClassLoader()); //将合并后的数组赋值给系统的类加载器pathList对象的dexElements属性 ReflectUtils.setField(pathList, pathList.getClass(), Constants.DEX_ELEMENTS, newElements); } catch (Throwable e) { e.printStackTrace(); } } /** * 清理磁盘缓存的dex文件(慎用,会导致需要修复的dex文件失效) */ public void clear() { fixDexSet.clear(); String dexDir = context.getDir(Constants.TEMP_FOLDER, Context.MODE_PRIVATE).getAbsolutePath(); File tempFile = new File(dexDir + File.separator + Constants.TEMP_UNZIP_FOLDER); for (File dex : tempFile.listFiles()) { if (dex.exists()) dex.delete(); } File dexFile = new File(dexDir); for (File dex : dexFile.listFiles()) { if (dex.exists()) dex.delete(); } Log.i(TAG, "===== 清理完成 ====="); } } public class ReflectUtils { /** * 获取某个属性对象 * * @param obj 该属性所属类的对象 * @param clazz 该属性的所属类 * @param field 属性名 */ private static Object getField(Object obj, Class<?> clazz, String field) { try { Field declaredField = clazz.getDeclaredField(field); declaredField.setAccessible(true); return declaredField.get(obj); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } /** * 给某个属性赋值 * * @param obj 该属性所属类的对象 * @param clazz 该属性的所属类 * @param field 属性名 * @param value 值 */ public static void setField(Object obj, Class<?> clazz, String field, Object value) { try { Field declaredField = clazz.getDeclaredField(field); declaredField.setAccessible(true); declaredField.set(obj, value); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } /** * 获取BaseDexClassLoader对象中的pathList对象 * * @param baseDexClassLoader baseDexClassLoader对象 */ public static Object getPathList(Object baseDexClassLoader) { try { return getField(baseDexClassLoader, Class.forName(Constants.BASE_DEX_CLASS_LOADER), Constants.PATH_LIST); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } /** * 获取PathList对象中的dexElements对象 * * @param pathList pathList对象 */ public static Object getDexElements(Object pathList) { return getField(pathList, pathList.getClass(), Constants.DEX_ELEMENTS); } } public interface FixListener { void onComplete(); }