Android Sdk热修复实践之旅

一、前言

市面上目前app的热修复技术众多,比较主流的有:
1)腾讯系:微信的Thinker、QQ空间的超级补丁、手Q的QFix
2)阿里系:AndFix、阿里百川HotFix、Sophix
3)美团:Robust
4)饿了么:Amigo
5)美丽说蘑菇街:Aceso

二、主流热修复方案对比

1、阿里系

名称说明
AndFix开源,实时生效,最新更新是3年前
HotFix阿里百川,未开源,免费、实时生效
Sophix未开源,商业收费,实时生效/冷启动修复

HotFixAndFix的优化版本,SophixHotFix的优化版本。目前阿里系主推是Sophix
2、腾讯系

名称说明
Qzone超级补丁QQ空间,未开源,冷启动修复
QFix手Q团队,未开源,冷启动修复
Tinker微信团队,开源,冷启动修复。提供分发管理,基础版免费 ,持续更新

3、其他

名称说明
Robust美团, 开源,实时修复,支持到android 9.0,持续更新
Nuwa大众点评,开源,冷启动修复,最新更新是4年前
Amigo饿了么,开源,冷启动修复,仅支持到android 7.1,最新更新是2年前

4、方案对比

方案对比SophixTinkernuwaAndFixRobustAmigo
类替换yesyesyesnonoyes
So替换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热修复原理-各热修复框架原理

四、sdk热修复技术方案选型

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();
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值