热修复原理-你想知道的都在这里


   本篇文章不会教你如何去使用某一个热修复框架,而是教会我们热修复的原理,只有明白原理,使用框架的时候才能做到水到渠成.事半功倍的效果.

产生背景

  • 刚发布的版本出现bug,解决bug后,测试并且打包在各个市场发布,用户再更新,如果短时间出现n个bug怎么办…
  • 有一小得功能需要添加,需要在短时间完成版本覆盖,

目前主流的热修复框架对比

类别成员
阿里系AndFix Dexposed 阿里百川
腾讯系tinker 超级补丁(Q空间) (手机QQ)QFix
其他美团Robust Rocoofix … 蘑菇街Aceso

目前主流的热修复框架的核心技术主要有三类 代码修复,资源修复,动态链库修复.

特性AndFixTinker/AmigoQQ空间Robust/Aceso
即时生效
方法替换
类替换
类结构修改
资源替换
so替换
支持gradle
支持ART
支持Android8.0

  我们可以根据上表和具体业务来选择合适的热修复框架,当然上表的信息很难做到完全准确,因为部分的热修复框架还在不断更新迭代。
  从表中也可以发现Tinker和Amigo拥有的特性最多,是不是就选它们呢?也不尽然,拥有的特性多也意味着框架的代码量庞大,我们需要根据业务来选择最合适的,假设我们只是要用到方法替换,那么使用Tinker和Amigo显然是大材小用了。另外如果项目需要即时生效,那么使用Tinker和Amigo是无法满足需求的。对于即时生效,AndFix、Robust和Aceso都满足这一点,这是因为AndFix的代码修复采用了底层替换方案,而Robust和Aceso的代码修复借鉴了Instant Run原理

资源修复

  首先我们来认识一下Instant Run ,Android Studio的setting中有一个Instant Run的复选框,勾选中后修改了代码就会很快的运行到我们的手机上.但是你明白其中的原理吗?

Instant Run简介

Instant Run是AS2.0以后出现的一个运行机制.能够减少除首次以外的代码构建和部署的时间.

  • Hot Swap 最高效的部署方式,代码的增量不需要重启APP或者Activity,一般用于方法中的代码修改
  • Warm Swap 需要重启Activity,但是不需要重启App 修改或者删除一个现有的资源文件
  • Cold Swap 重启App 但是不需要安装, 用于添加字段,添加类 删除字段等.
Instant Run资源修复
 public void onCreate() {
        if (!AppInfo.usingApkSplits) {
            MonkeyPatcher.monkeyPatchApplication(this, this,
                    this.realApplication, this.externalResourcePath);
            MonkeyPatcher.monkeyPatchExistingResources(this,
                    this.externalResourcePath, null);
        } else {
            MonkeyPatcher.monkeyPatchApplication(this, this,//反射 ActivityThread 替换Application
                    this.realApplication, null);
        }
        if (AppInfo.applicationId != null) {
            try {
                boolean foundPackage = false;
                int pid = Process.myPid();
                ActivityManager manager = (ActivityManager) getSystemService("activity");
                List processes = manager
                        .getRunningAppProcesses();
                boolean startServer = false;
                if ((processes != null) && (processes.size() > 1)) {
                    for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
                        if (AppInfo.applicationId
                                .equals(processInfo.processName)) {
                            foundPackage = true;
                            if (processInfo.pid == pid) {
                                startServer = true;
                                break;
                            }
                        }
                    }
                    if ((!startServer) && (!foundPackage)) {
                        startServer = true;
                    }
                } else {
                    startServer = true;
                }
                if (startServer) {
                    Server.create(AppInfo.applicationId, this);
                }
            } catch (Throwable t) {
                Server.create(AppInfo.applicationId, this);
            }
        }
        if (this.realApplication != null) {
            this.realApplication.onCreate();//真正的application回调onCreate
        }
    }
 public static void monkeyPatchApplication(Context context,
                                              Application bootstrap, Application realApplication,
                                              String externalResourceFile) {
        try {
            Class activityThread = Class
                    .forName("android.app.ActivityThread");
            Object currentActivityThread = getActivityThread(context,
                    activityThread);
            Field mInitialApplication = activityThread
                    .getDeclaredField("mInitialApplication");
            mInitialApplication.setAccessible(true);
            Application initialApplication = (Application) mInitialApplication
                    .get(currentActivityThread);
            if ((realApplication != null) && (initialApplication == bootstrap)) {
                mInitialApplication.set(currentActivityThread, realApplication);
            }
            if (realApplication != null) {
                Field mAllApplications = activityThread
                        .getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List allApplications = (List) mAllApplications
                        .get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                    if (allApplications.get(i) == bootstrap) {
                        allApplications.set(i, realApplication);//完成application的替换
                    }
                }
            }
            Class loadedApkClass;
            try {
                loadedApkClass = Class.forName("android.app.LoadedApk");
            } catch (ClassNotFoundException e) {
                loadedApkClass = Class
                        .forName("android.app.ActivityThread$PackageInfo");
            }
            Field mApplication = loadedApkClass
                    .getDeclaredField("mApplication");
            mApplication.setAccessible(true);
            Field mResDir = loadedApkClass.getDeclaredField("mResDir");
            mResDir.setAccessible(true);
            Field mLoadedApk = null;
            try {
                mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
            } catch (NoSuchFieldException e) {
            }
            for (String fieldName : new String[] { "mPackages",
                    "mResourcePackages" }) {
                Field field = activityThread.getDeclaredField(fieldName);
                field.setAccessible(true);
                Object value = field.get(currentActivityThread);
                for (Map.Entry> entry : ((Map>) value).entrySet()) {
                    Object loadedApk = ((WeakReference) entry.getValue()).get();
                    if (loadedApk != null) {
                        if (mApplication.get(loadedApk) == bootstrap) {
                            if (realApplication != null) {
                                mApplication.set(loadedApk, realApplication);
                            }
                            if (externalResourceFile != null) {
                                mResDir.set(loadedApk, externalResourceFile);//资源文件替换
                            }
                            if ((realApplication != null)
                                    && (mLoadedApk != null)) {
                                mLoadedApk.set(realApplication, loadedApk);
                            }
                        }
                    }
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }
  • 创建新的AssetManger,通过反射调用AddAssetpath方法加载外部资源,这样新创建的AssetManger就含有了外部资源
  • 将AssetManger类型的mAsset字段的引用全部替换为新创建的AssetManger

这里附加一份完整的Instant Run的源码 有兴趣可以自己研究一下

代码修复

1. 类加载方案

类加载方案基于Dex分包方案, 提起这个就先说一下65536限制和LinearAlloc限制

65536限制

DVM ByteCode的限制 DVM指令集的方法调用指令 invoke-kind索引为16bits 即2^16 最多使用65536个方法

LinearAlloc限制

安装应用时我们经常见INSTALL_FAILED_DEXOPT ,产生的原因就是LinearAlloc限制, DVM中的LinearAlloc是一哥固定的缓存区,当方法超出了缓存区的大小时就会出错,

为了解决上述二个错误,Dex分包方案由此产生,

Dex分包方案主要就是在打包的是候将应用代码分成多个dex,将应用启动的是候必须用到的类和这些类的直接引用存放到主Dex分包中,其他代码方法次要的Dex中,等到应用启动后动态加载次dex

public class FixDexUtils {
    private static final String DEX_SUFFIX = ".dex";
    private static final String APK_SUFFIX = ".apk";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    public static final String DEX_DIR = "odex";
    private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
    private static HashSet<File> loadedDex = new HashSet<>();

    static {
        loadedDex.clear();
    }

    /**
     * 加载补丁,使用默认目录:data/data/包名/files/odex
     *
     * @param context
     */
    public static void loadFixedDex(Context context) {
        loadFixedDex(context, null);
    }

    /**
     * 加载补丁
     *
     * @param context       上下文
     * @param patchFilesDir 补丁所在目录
     */
    public static void loadFixedDex(Context context, File patchFilesDir) {
        if (context == null) {
            return;
        }
        // 遍历所有的修复dex
        File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(这个可以任意位置)
        File[] listFiles = fileDir.listFiles();
        for (File file : listFiles) {
          //获取classes2.dex文件即获取补丁文件/dex/apk/jar/zip
            if (file.getName().startsWith("classes2") &&
                    (file.getName().endsWith(DEX_SUFFIX)
                            || file.getName().endsWith(APK_SUFFIX)
                            || file.getName().endsWith(JAR_SUFFIX)
                            || file.getName().endsWith(ZIP_SUFFIX))) {
                loadedDex.add(file);// 存入集合
            }
        }
        // dex合并之前的dex
        doDexInject(context, loadedDex);
    }

    private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
        String optimizeDir = appContext.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR;// data/data/包名/files/optimize_dex(这个必须是自己程序下的目录)
        File fopt = new File(optimizeDir);
        if (!fopt.exists()) {
            fopt.mkdirs();
        }
        try {
            // 1.加载应用程序的dex
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            for (File dex : loadedDex) {
                // 2.加载指定的修复的dex文件
                DexClassLoader dexLoader = new DexClassLoader(
                        dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
                        fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
                        null,// 加载dex时需要的库
                        pathLoader// 父类加载器
                );
                // 3.合并
                Object dexPathList = getPathList(dexLoader);
                Object pathPathList = getPathList(pathLoader);
                Object leftDexElements = getDexElements(dexPathList);
                Object rightDexElements = getDexElements(pathPathList);
                // 合并完成
                Object dexElements = combineArray(leftDexElements, rightDexElements);
                // 重写给PathList里面的Element[] dexElements;赋值
                Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
                setField(pathList, pathList.getClass(), "dexElements", dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 反射给对象中的属性重新赋值
     */
    private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cl.getDeclaredField(field);
        declaredField.setAccessible(true);
        declaredField.set(obj, value);
    }

    /**
     * 反射得到对象中的属性值
     */
    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }


    /**
     * 反射得到类加载器中的pathList对象
     */
    private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 反射得到pathList中的dexElements
     */
    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        return getField(pathList, pathList.getClass(), "dexElements");
    }

    /**
     * 数组合并
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> componentType = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组)
        int j = Array.getLength(arrayRhs);// 得到原dex数组长度
        int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
        Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组
        System.arraycopy(arrayLhs, 0, result, 0, i);
        System.arraycopy(arrayRhs, 0, result, i, j);
        return result;
    }

}

共具类提供的思路就是 得到补丁classes2.dex后,存放到Element数组的一地个元素,把出现Bug的类.class编译后的dex文件放到Element数组的末尾,根据ClassLoader的双亲委托模式, ClassLoader会优先加载我们提供的classes2.dex, 而不会去加载出现Bug的dex文件.

类加载方法需要重启App让ClassLoader重新加载新的类,因为类加载后无法被卸载,因此,这个类型的热修复方案无法及时生效,

采用类加载方案的主要是以腾讯系为主.Tinker等

底层替换方案

直接在Native层修改原有类 暂不介绍

动态链接库的修复

Android的动态链接库主要就是so库, 主要就是重新加载so库完成热更新

System的Load 方法

加载so主要用到了System#load和LoadLibarary方法

 @CallerSensitive
    public static void load(String filename) {
        Runtime.getRuntime().load0(VMStack.getStackClass1(), filename); //传入的参数是so的完整路径,用于加载指定的so文件
    }

synchronized void load0(Class<?> fromClass, String filename) {
        if (!(new File(filename).isAbsolute())) {
            throw new UnsatisfiedLinkError(
                "Expecting an absolute path of the library: " + filename);
        }
        if (filename == null) {
            throw new NullPointerException("filename == null");
        }
  //将加载该类的类加载器传入
        String error = nativeLoad(filename, fromClass.getClassLoader());
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }
//最终调用Native方法
 private static native String nativeLoad(String filename, ClassLoader loader);
System的LoadLibrary
 @CallerSensitive
    public static void loadLibrary(String libname) {
      //传入的参数是so的名称.APP安装完成后自动从apk包中复制到/dada/dada/packagename/lib下
        Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
    }

synchronized void loadLibrary0(ClassLoader loader, String libname) {
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        if (loader != null) {
          //通过ClassLoader的findLibrary得到,具体在baseDexClassLoader中实现
            String filename = loader.findLibrary得到,具体在baseDexClassLoader中实现(libraryName);
            if (filename == null) {
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                               System.mapLibraryName(libraryName) + "\"");
            }
          //调用Native方法
            String error = nativeLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }
				
        String filename = System.mapLibraryName(libraryName);
        List<String> candidates = new ArrayList<String>();
        String lastError = null;
  //遍历getLibPaths 得到library.path选项的路径数组
        for (String directory : getLibPaths()) {
          //拼接so路径
            String candidate = directory + filename;
            candidates.add(candidate);
            if (IoUtils.canOpenReadOnly(candidate)) {
              //调用Native方法
                String error = nativeLoad(candidate, loader);
                if (error == null) {
                    return; // We successfully loaded the library. Job done.
                }
                lastError = error;
            }
        }

        if (lastError != null) {
            throw new UnsatisfiedLinkError(lastError);
        }
        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
    }
private static native String nativeLoad(String filename, ClassLoader loader);
//主要判断so是否加载过,二次ClassLoader是否是同一个,避免重复so加载
//打开so并得到引用句柄,如果so句柄获取失败,返回false,创建新的ShareLibrary,如果传入path对应的library为空指针,就创建新的ShareLibrary赋值给library,并将library存储到libraries_中
// 查找JNI_ONLoad的函数指针,根据不同情况设置was_successful 的值,最终返回这个值
//更多详情请查看Native层的方法JVM_NativeLoad(),由于水平有限,此处不做分析

总结一下 so修复主要方案:

  • 将so补丁插入奥NativeLibraryElement数组的前部,让so补丁的路径先被返回和加载
  • 调用System的load方法来接管so的加载入口

写在最后.

Android的热修复框架太多,而且框架在不断更新迭代,只有我们明白热修复的原理.目前所有的框架都是基于这些原理去开发的.我们掌握原理才能更好的理解热修复框架和去阅读开源热修复框架的源码.

感谢 <<Android源码解密 -刘望舒>>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值