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、方案对比

方案对比 Sophix Tinker nuwa AndFix Robust Amigo
类替换 yes yes yes no no yes
So替换 yes yes no no no yes
资源替换 yes yes yes no no yes
全平台支持 yes yes yes no yes yes
即时生效 同时支持 no no yes yes no
性能损耗 较少 较小 较大 较小 较小 较小
补丁包大小 较小 较大 一般 一般 较大
开发透明 yes yes yes no no yes
复杂度 傻瓜式接入 复杂 较低 复杂 复杂 较低
Rom体积 较小 Dalvik较大 较小 较小 较小
成功率 较高 较高 一般 最高 较高
热度
开源 no yes yes yes yes yes
收费 收费(设有免费阈值) 收费(基础版免费,但有限制) 免费 免费 免费 免费
监控 提供分发控制及监控 提供分发控制及监控 no no no no

三、热修复技术方案

总体而言,热修复技术方案主要分为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, "===== 清理完成 =====");
    }
}
	
发布了36 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_43278826/article/details/102837347