美文网首页
热修复技术框架对比

热修复技术框架对比

作者: ModestStorm | 来源:发表于2022-02-19 21:15 被阅读0次

    热修复技术框架对比:

    PathClassLoader和DexClassLoader都是Android提供给我们的ClassLoader,都能加载dex
    PathClassLoader只能加载已经被系统安装过的apk,DexClassLoader无此限制,两者都继承自BaseDexClassLoader,最终都会创建一个DexFile,不同点是一个关键的参数:optimizedDirectory,PathClassLoader为null,DexClassLoader则使用传递进来的
    然后会根据optimizedDirectory判断对应的oat文件是否已经生成(null则使用/data/dalvik-cache/),如果有且该oat对应的dex正确则直接加载,否则触发dex2oat(就是这家伙耗了我们宝贵的时间!!),成功则用生成的oat,失败则走解释执行
    ps:貌似Q版做了优化,不会再卡死在dex2oat里了
    根据加载外部dex的实验,DexClassLoader会触发dex2oat,而PathClassLoader不会

    AndroidManifest出现Bug是无法修复的,因为它是由系统进行解析的,系统会直接获取安装包里唯一的AndroidMainfest.xml文件,在解析过程不会访问补丁包信息。

    代码修复:任何的热修复方案,想要改变代码逻辑,都需要在补丁包里包含一个新逻辑的dex文件。

    资源修复:有些资源,比如桌面图标,通知栏图标资源,是由系统直接解析安装包里的资源得到的,因此对于这类资源,任何热修复方案都无法进行资源替换和修复。
    构造一个package id为0x66的资源包,这个包里面只需要包含需要改变的资源项,然后直接在原有AssetManger中通过addAssetPath函数添加这个包就可以了。由于补丁包的package id为0x66,不与目前的已经加载的地址为0x7f的包冲突,因此直接加载到已有的AssetManager中就可以直接使用了。
    补丁包里面的只有新增的资源和需要替换的资源。并且,我们采用了更加优雅的替换方式,直接在原有的AssetManager对象上进行解析和重构,这样所有原有对AssetManager对象的引用是没有发生改变的

    so库修复:so库的修复思路应该是最明确的。在Android系统中,所有的so库都是由System.load进行加载的,因此只要找到办法在加载的时候优先加载补丁包的so库,而不是加载原有安装包的so库,就能够进行完整的底层代码替换了。
    把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库的目录,从而达到修复bug的目的

    代码修复热修复方案对比:

    阿里系:AndFix:底层替换方案,时效性最好,加载轻快,立即生效,兼容性差。依赖直接修改虚拟机方法实体的具体字段实现的。不同厂商/版本对ArtMethod结构体的结构修改带来的问题。每个Java方法在art中都对应着一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括所属类,访问权限,代码执行地址等等。不支持新增方法,新增、修改成员变量等

    腾讯系:QQ空间和tinker 冷启动时生效

    QQ空间:不支持资源修复,在编译字节码阶段使用插件修改所有类的class字节码,构造函数中插入System.out.println(AntilazyLoad.class),其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作,然后在应用启动的时候加载进来.AntilazyLoad类所在的dex包必须被先加载进来,不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dex包,那么他也是不存在的。
    所以Application作为应用的入口不能插入这段代码。(因为载入hack.dex的代码是在Application中onCreate中执行的,如果在Application的构造函数里面插入了这段代码,那么就是在hack.dex加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志。

    最后加载补丁patch.dex得到dexFile对象作为参数构建一个Element对象插入到dex Elements数组的最前面达到热修复的目的,对代码侵入性较大。

    之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。
    空间使用的是在字节码插入代码,而不是源代码插入,使用的是javaassist库来进行字节码插入的。

    但是插桩会给类加载效率带来比较严重的影响,
    如果类没被打上CLASS ISPREVERIFIED/CLASS ISOPTIMIZED 的标志,那么类的校验和优化都在类的初始化阶段进行。那么类的校验和优化都将在类的初始化阶段进行。正常情况下类的校验和优化都仅在 APK 第一次安装执行 dexopt 操作的时候进行 , 类的校验任务实际上是很重的,因为会对类的所有方法中的所有指令都进行校验,单个类加载时类校验耗时并不多,但是如果是在同一时间点加载大量类的情况下,这种耗时就会被放大。

    • static方法
    • private方法
    • 构造函数
    • override方法
      概括一下就是如果以上方法中直接引用到的类(第一层级关系,不会递归进行搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED
      Apk 安装的时候虚拟机会对dex进行odex优化,优化的过程中会进行class类的校验,给每一个class打上了一个CLASS_ISPREVERIFIED的标签,在调用的时候会根据该标签判断所在的class是否是同一个dex如果不是会抛出异常导致程序停止。所以我们需要防止类被打上CLASS_ISPREVERIFIED。

    Tinker:支持资源修复,微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,
    区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX文件,以达到修复的目的。

    根据newApk和oldApk生成dex差量包,整体替换dex的方案。差量的方式给出patch.dex,然后将patch.dex与应用的class.dex合并成一个完整的dex,完整的dex加载得到dexFile对象作为参数构建一个Element对象然后整体替换掉旧的dex Element数组。合成全量的dex文件,这样所有类都在全量dex中解决,从而消除类重复而带来的冲突。

    假设应用有5个DEX文件,分别修改了这5个DEX,产生5个patch.dex文件,就要进行5次的patch合并动作,假设每个补丁1M,那么就要多占用7.5M的磁盘空间。
    dex合并内存消耗在jvm heap上,容易导致OOM,最后导致dex合成失败

    Tinker的dex merge操作是在Java层面进行的,所有对象的分配都是在java heap上完成的,可能发生申请的java heap超过vm heap规定的大小,进程发生OOM导致进程被杀死合成失败,如果在JNI层面进行C++ new/malloc申请的内存,分配在native heap,native heap的增长并不受vm heap大小的限制,只受限于RAM,如果RAM不足,也会导致进程被杀死导致闪退。在JNI层面进行dex merge,从而避免OOM提高合并成功率。

    美团:Robust 代码层级的javahook,兼容性好,稳定性高,支持方法级别的修复,缺点是代码是侵入式的,会在原有的类中加入相关代码,不支持so库和资源的替换。会增大apk的体积,平均一个函数会比原来增加17个字节。
    Robust的方案虽然有侵入性,增加代码体积等的缺点,但是感觉这种方式是兼容率最好的方案。也不会受Android版本的更新影响。

    在打基础包时插桩,在每一个方法前插入一段ChangeQuickRedirect静态变量的逻辑,插入过程对业务开发完全透明。加载补丁时,从补丁包中读取要替换的类以及具体替换的方法实现,新建ClassLoader加载补丁dex。当changeQuickRedirect不为null的时候,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。

    public static ChangeQuickRedirect u;
    protected void onCreate(Bundle bundle) {
            //为每个方法自动插入修复逻辑代码,如果ChangeQuickRedirect为空则不执行
            if (u != null) {
                if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
                    PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
                    return;
                }
            }
            super.onCreate(bundle);
            ...
        }
    

    Robust的核心修复源码如下:

    public class PatchExecutor extends Thread {
        @Override
        public void run() {
            ...
            applyPatchList(patches);
            ...
        }
        /**
         * 应用补丁列表
         */
        protected void applyPatchList(List<Patch> patches) {
            ...
            for (Patch p : patches) {
                ...
                currentPatchResult = patch(context, p);
                ...
                }
        }
         /**
         * 核心修复源码
         */
        protected boolean patch(Context context, Patch patch) {
            ...
            //新建ClassLoader
            DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
                    null, PatchExecutor.class.getClassLoader());
            patch.delete(patch.getTempPath());
            ...
            try {
                patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
                patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
                } catch (Throwable t) {
                 ...
            }
            ...
            //通过遍历其中的类信息进而反射修改其中 ChangeQuickRedirect 对象的值
            for (PatchedClassInfo patchedClassInfo : patchedClasses) {
                ...
                try {
                    oldClass = classLoader.loadClass(patchedClassName.trim());
                    Field[] fields = oldClass.getDeclaredFields();
                    for (Field field : fields) {
                        if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
                            changeQuickRedirectField = field;
                            break;
                        }
                    }
                    ...
                    try {
                        patchClass = classLoader.loadClass(patchClassName);
                        Object patchObject = patchClass.newInstance();
                        changeQuickRedirectField.setAccessible(true);
                        changeQuickRedirectField.set(null, patchObject);
                        } catch (Throwable t) {
                        ...
                    }
                } catch (Throwable t) {
                     ...
                }
            }
            return true;
        }
    }
    

    相关文章

      网友评论

          本文标题:热修复技术框架对比

          本文链接:https://www.haomeiwen.com/subject/sunplrtx.html