(4.2.32)各大热补丁方案分析和比较

选自:

  1. 【腾讯bugly干货分享】微信Android热补丁实践演进之路
  2. 各大热补丁方案分析和比较

继插件化后,热补丁技术在2015年开始爆发,目前已经是非常热门的Android开发技术。其中比较著名的有淘宝的Dexposed、支付宝的AndFix以及QZone的classloader超级热补丁方案。

一、为什么需要热补丁

热补丁:让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力

从上面的定义来看,热补丁节省Android大量应用市场发布的时间。同时用户也无需重新安装,只要上线就能无感知的更新。看起来很美好,这是否可以意味我们可以尽量使用补丁来代替发布呢?事实上,热补丁技术当前依然存在它的局限性,主要表现在以下几点:

  • 补丁只能针对单一客户端版本,随着版本差异变大补丁体积也会增大;
  • 补丁不能支持所有的修改,例如AndroidManifest;
  • 补丁无论对代码还是资源的更新成功率都无法达到100%。

二、热补丁的使用场景

2.1 轻量而快速的升级

热补丁技术也可以理解为一个动态修改代码与资源的通道,它适合于修改量较少的情况。以微信的多次发布为例,补丁大小均在300K以内,它相对于传统的发布有着很大的优势。

Tables普通升级补丁升级
数据大小32.5M105K
更新速度(0-50%)10天1天(70%)
自动升级wifi移动

以Android用户的升级习惯,即使是相对活跃的微信也需要10天以上的时间去覆盖50%的用户。使用补丁技术,我们能做到1天覆盖70%以上。这也是基于补丁体积较小,可以直接使用移动网络下载更新。

正因如此,补丁技术非常适合使用在灰度阶段。在过去,我们需要在正式发布前保证所有严重的问题都已经得到修复,这通常需要我们经过三次以上的灰度过程,而且无法快速的验证这些问题在同一批用户的修复效果。利用热补丁技术,我们可以快速对同一批用户验证修复效果,这大大缩短了我们的发布流程。

若发布版本出现问题或紧急漏洞,传统方式需要单独灰度验证修改,然后重新发布新的版本。利用补丁技术,我们只需要先上线小部分用户验证修改的效果,最后再全量上线即可。但是此种发布对线上用户影响较大, 我们需要谨慎而为。本着对用户负责的态度,发布补丁等同于发布版本,它也应该严格执行完整的测试与上线流程。

总的来说,补丁技术可以降低开发成本,缩短开发周期,实现轻量而快速的升级。

2.2 远端调试

一入Android深似海,Android开发的另外一个痛是机型的碎片化。我们也许都会遇到”本地不复现”,”日志查不出”,”联系用户不鸟你”的烦恼。所以补丁机制非常适合使用在远端调试上。即我们需要具备只特定用户发送补丁的能力,这对我们查找问题非常有帮助。

2.3 数据统计

数据统计在微信中也占据着非常重要的位置,我们也非常希望将热补丁与数据统计结合的更好。事实上,热补丁无论在普通的数据统计还是ABTest都有着非常大的优势。例如若我想对同一批用户做两种test, 传统方式无法让这批用户去安装两个版本。使用补丁技术,我们可以方便的对同一批用户不停的更换补丁。

在数据统计之路,如何与补丁技术结合的更好,更加精准的控制样本人数与比例,这也是微信当前努力发展的一个方向。

2.4 其他

事实上,Android官方也使用热补丁技术实现Instant Run。它分为Hot Swap、Warm Swap与Cold Swap三种方式,大家可以参考英文介绍,也可以看参考文章中的翻译稿。最新的Instant App应该也是采用类似的原理,但是Google Play是不允许下发代码的,这个海外App需要注意一下。

三、现有的热补丁方案

热修复框架的种类繁多,按照公司团队划分主要有以下几种:

类别成员
阿里系AndFix、Dexposed、阿里百川、Sophix
腾讯系微信的Tinker、QQ空间的超级补丁、手机QQ的QFix
知名公司美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso
其他RocooFix、Nuwa、AnoleFix

虽然热修复框架很多,但热修复框架的核心技术主要涉及三个方面,分别是

  1. 代码修复
  2. 资源修复
  3. 动态链接库修复

其中每个核心技术又有很多不同的技术方案,每个技术方案又有不同的实现,另外这些热修复框架仍在不断的更新迭代中,可见热修复框架的技术实现是繁多可变的。作为开发需需要了解这些技术方案的基本原理,这样就可以以不变应万变。

由于在不同方面不同框架使用的技术是不同的,这也就导致了它们在不同方面的表现并不一致,部分热修复框架的对比如下表所示

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

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

对于即时生效,AndFix、Robust和Aceso都满足这一点,这是因为AndFix的代码修复采用了底层替换方案,而Robust和Aceso的代码修复借鉴了Instant Run原理,现在我们就来学习代码修复。

3.1 资源修复

(4.6.29.4)插件化之资源加载:使用插件中的R资源

目前市面上的很多资源热修复方案基本上都是参考了Instant Run的实现。

首先,我们简单来看一下Instant Run是怎么做到资源热修复的。

Instant Run资源热修复的核心代码就是这个monkeyPatchExistingResources方法:

  1. 构造一个新的AssetManager,并通过反射调用addAssetPath,把这个完整的新资源包加入到AssetManager中。这样就得到了一个含有所有新资源的AssetManager。
  2. 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager。
    1. 通过Resources去找
  3. hook ResourcesManager实现全部的Resources的替换,内部替换AssetManager
@com/android/tools/fd/runtime/MonkeyPatcher.java

public static void monkeyPatchExistingResources(@Nullable Context context,
                                                    @Nullable String externalResourceFile,
                                                    @Nullable Collection<Activity> activities) {

    if (externalResourceFile == null) {
        return;
    }

    try {
        // %%【 Part 1. 】创建一个新的AssetManager,并通过反射调用addAssetPath添加/sdcard上的新资源包.
        //         这样就构造出了一个带新资源的AssetManager
        // Create a new AssetManager instance and point it to the resources installed under
        // /sdcard
        AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
        mAddAssetPath.setAccessible(true);
        if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }

        // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
        // in L, so we do it unconditionally.
        Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
        mEnsureStringBlocks.setAccessible(true);
        mEnsureStringBlocks.invoke(newAssetManager);

        // %% 【Part 2. 】反射得到Activity中AssetManager的引用处,全部换成刚才新构建的newAssetManager
        if (activities != null) {
            for (Activity activity : activities) {
                Resources resources = activity.getResources();

                try {
                    Field mAssets = Resources.class.getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources);
                    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }
                    ... ...

                pruneResourceCaches(resources);
            }
        }

        // %% 【Part 3. 】得到Resources的弱引用集合,把他们的AssetManager成员替换成newAssetManager
        // Iterate over all known Resources objects
        Collection<WeakReference<Resources>> references;
        if (SDK_INT >= KITKAT) {
            // Find the singleton instance of ResourcesManager
            Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
            Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
            mGetInstance.setAccessible(true);
            Object resourcesManager = mGetInstance.invoke(null);
            try {
                Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                @SuppressWarnings("unchecked")
                ArrayMap<?, WeakReference<Resources>> arrayMap =
                        (ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
                references = arrayMap.values();
            } catch (NoSuchFieldException ignore) {
                Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                mResourceReferences.setAccessible(true);
                //noinspection unchecked
                references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
            }
        } else {
            Class<?> activityThread = Class.forName("android.app.ActivityThread");
            Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
            fMActiveResources.setAccessible(true);
            Object thread = getActivityThread(context, activityThread);
            @SuppressWarnings("unchecked")
            HashMap<?, WeakReference<Resources>> map =
                    (HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread);
            references = map.values();
        }
        for (WeakReference<Resources> wr : references) {
            Resources resources = wr.get();
            if (resources != null) {
                // Set the AssetManager of the Resources instance to our brand new one
                try {
                    Field mAssets = Resources.class.getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources);
                    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }

                resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
            }
        }
    } catch (Throwable e) {
        throw new IllegalStateException(e);
    }}

3.2 代码修复

代码修复主要有三个方案,分别是

  • 类加载方案
    • 动态改变BaseDexClassLoader对象间接引用的dexElements,将补丁的dex插入到dexElements前面,这样就可以加载到补丁类了
    • 方案中程序加载类的只有一个ClassLoader,即PathClassLoader,这样在加载过类后就不会重新加载类,进行补丁修复必须重新启动应用才可以
    • 采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、QQ空间的超级补丁、手机QQ的QFix、饿了么的Amigo和Nuwa等等
  • 底层替换方案
    • 采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。
  • Instant Run方案。
    • IncrementalClassLoader设置为默认PathClassLoader的parent,由于ClassLoader采用双亲委托模型,会先去parent查找类,而IncrementalClassLoader会在Application中去加载补丁dex,这样就可以加载补丁类了
    • Instant-Run提供了三种方式:热启动(修改方法和变量值)、温启动(资源的修改)、冷启动(增加类、修改类的继承等),instant-run中加载类是基于多ClassLoader的,采用的是双亲委托模式,IncrementalClassLoader是PathClassLoader的parent,每次更新补丁,就会新建一个ClassLoader,实现类的重新加载

3.2.1 类加载方案

(4.1.53)Android ClassLoader详解讲到了ClassLoader的加载过程,其中一个环节就是调用DexPathList的findClass的方法,如下所示。

public Class<?> findClass(String name, List<Throwable> suppressed) {
       for (Element element : dexElements) {//【1】
           Class<?> clazz = element.findClass(name, definingContext, suppressed);//【2】
           if (clazz != null) {
               return clazz;
           }
       }
       if (dexElementsSuppressedExceptions != null) {
           suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
       }
       return null;
   }
  1. Element内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element。
    多个Element组成了有序的Element数组dexElements
  2. 当要查找类时,会在注释1处遍历Element数组dexElements(相当于遍历dex文件数组)
  3. 注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。如果在Element中(dex文件)找到了该类就返回,如果没有找到就接着在下一个Element中进行查找。

根据上面的查找流程,我们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在bug的Key.class,排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案

在这里插入图片描述

类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?这是因为类是无法被卸载的,因此要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。

虽然很多热修复框架采用了类加载方案,但具体的实现细节和步骤还是有一些区别的:

  1. QQ空间的超级补丁和Nuwa是按照上面说得将补丁包放在Element数组的第一个元素得到优先加载。
  2. 微信Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素。
  3. 饿了么的Amigo则是将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element 数组

3.2.1 底层替换方案

与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类

由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况

  1. java 层的功能就是找到补丁文件,根据补丁中的注解找到将要替换的方法然后交给jni层去处理替换方法的操作
  2. 将一个不是native的方法修改成native方法,然后在 native 层进行替换,通过 dvmCallMethod_fnPtr 函数指针来调用 libdvm.so 中的 dvmCallMethod() 来加载替换后的新方法,达到替换方法的目的。
    • Jni 反射调用 java 方法时要用到一个 jmethodID 指针,这个指针在 Dalvik 里其实就是 Method 类,通过修改这个类的一些属性就可以实现在运行时将一个方法修改成 native 方法

底层替换方案和反射的原理有些关联,就拿方法替换来说,方法反射我们可以调用java.lang.Class.getDeclaredMethod,假设我们要反射Key的show方法,会调用如下所示

Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());

//Android 8.0的invoke方法,如下所示。
//libcore/ojluni/src/main/java/java/lang/reflect/Method.java
@FastNative
public native Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;

invoke方法是个native方法,对应Jni层的代码为:

//art/runtime/native/java_lang_reflect_Method.cc
static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
                             jobject javaArgs) {
  ScopedFastNativeObjectAccess soa(env);
  return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
 }
 
//Method_invoke函数中又调用了InvokeMethod函数:
//art/runtime/reflection.cc
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
                     jobject javaReceiver, jobject javaArgs, size_t num_frames) {

...
  ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
  const bool accessible = executable->IsAccessible();
  ArtMethod* m = executable->GetArtMethod();//1
...
}

注释1处获取传入的javaMethod(Key的show方法)在ART虚拟机中对应的一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等等,ArtMethod结构如下所示

class ArtMethod FINAL {
...
 protected:
  GcRoot<mirror::Class> declaring_class_;
  std::atomic<std::uint32_t> access_flags_;
  uint32_t dex_code_item_offset_;
  uint32_t dex_method_index_;
  uint16_t method_index_;
  uint16_t hotness_count_;
 struct PtrSizedFields {
    ArtMethod** dex_cache_resolved_methods_;//1
    void* data_;
    void* entry_point_from_quick_compiled_code_;//2
  } ptr_sized_fields_;
}

ArtMethod结构中比较重要的字段是注释1处的dex_cache_resolvedmethods和注释2处的entry_point_from_quick_compiledcode,它们是方法的执行入口,当我们调用某一个方法时(比如Key的show方法),就会取得show方法的执行入口,通过执行入口就可以跳过去执行show方法。

**替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。**底层替换方案直接替换了方法,可以立即生效不需要重启。

  • AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。
  • Sophix采用的是替换整个ArtMethod结构体,这样不会存在兼容问题。

采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。

3.2.3 Instant Run方案

//注释1处是一个成员变量localIncrementalChange ,它的值为$change,$change实现了IncrementalChange这个抽象接口
IncrementalChange localIncrementalChange = $change;//1
		if (localIncrementalChange != null) {//2
			localIncrementalChange.access$dispatch(
					"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
							paramBundle });
			return;
		}
  1. Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似如下的代码
  2. 当我们点击InstantRun时,如果方法没有变化则$change为null,就调用return,不做任何处理。
  3. 如果方法有变化,就生成替换类,这里我们假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity$override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity)
  4. 据列表会将MainActivity的$change设置为MainActivity$override,因此满足了注释2的条件,会执行MainActivity$override的access$dispatch方法,access$dispatch方法中会根据参数”onCreate.(Landroid/os/Bundle;)V”执行MainActivity$override的onCreate方法,从而实现了onCreate方法的修改。

借鉴Instant Run的原理的热修复框架有Robust和Aceso。

3.3 so链接库修复

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

四、 classloader QQ空间

  1. 动态改变BaseDexClassLoader对象间接引用的dexElements:

    • Classloader加载类是从DexElements依次遍历dex,如果dex中有该类则返回,没有则遍历下一个dex,所以Hotfix的解决方式就是改变dexElements中dex的顺序
    • 具体实现方式是通过反射的方式获取应用的PathdexClassloader—>PathList—>DexElements,再获取补丁dex的DexClassloader—>PathList—>DexElements,然后通过combinArray的方法将2个DexElements合并,补丁的DexElements放在前面,然后使用合并后的DexElements作为PathdexClassloader中的DexElements,这样在加载的时候就可以优先加载到补丁dex,从中可以加载到我们的补丁类。
  2. 在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志:

    • 相关类之所以会被打上CLASS_ISPREVERIFIED,是因为类和引用它的类在同一个dex文件中,那么引用它的类就会被CLASS_ISPREVERIFIED标志,这样在采用分包方案时,假如类和引用它的类不在一个dex文件中,程序就会报错。
    • 解决办法就是在每个类的构造方法中通过javassist注入代码加载辅助类,辅助类是在一个单独的类,单独会被打包成一个dex文件。另外为了实现代码在执行dex命令前注入,需要在build.gradle中新建一个task,使其在执行dex前执行。

QZone方案并没有开源,但在github上的Nuwa采用了相同的方式。这个方案使用classloader的方式,能实现更加友好的类替换。而且这与我们加载Multidex的做法相似,能基本保证稳定性与兼容性

  1. Nuwa https://github.com/jasonross/Nuwa
  2. HotFix https://github.com/dodola/HotFix
  3. DroidFix https://github.com/bunnyblue/DroidFix

本方案为了解决unexpected DEX problem异常而采用插桩的方式,从而规避问题的出现。事实上,Android系统的这些检查规则是非常有意义的,这会导致QZone方案在Dalvik与Art都会产生一些问题。

  • Dalvik; 在dexopt过程,若class verify通过会写入pre-verify标志,在经过optimize之后再写入odex文件。这里的optimize主要包括inline以及quick指令优化等。

在这里插入图片描述

若采用插桩导致所有类都非preverify,这导致verify与optimize操作会在加载类时触发。这会有一定的性能损耗,微信分别采用插桩与不插桩两种方式做过两种测试,一是连续加载700个50行左右的类,一是统计微信整个启动完成的耗时。

在这里插入图片描述

平均每个类verify+optimize(跟类的大小有关系)的耗时并不长,而且这个耗时每个类只有一次。但由于启动时会加载大量的类,在这个情况影响还是比较大的。

  • Art; Art采用了新的方式,插桩对代码的执行效率并没有什么影响。但是若补丁中的类出现修改类变量或者方法,可能会导致出现内存地址错乱的问题。为了解决这个问题我们需要将修改了变量、方法以及接口的类的父类以及调用这个类的所有类都加入到补丁包中。这可能会带来补丁包大小的急剧增加。

在这里插入图片描述

这里是因为在dex2oat时fast*已经将类能确定的各个地址写死。如果运行时补丁包的地址出现改变,原始类去调用时就会出现地址错乱。这里说的可能不够详细,事实上微信当时为了查清这两个问题,也花费了一定的时间将Dalvik跟Art的流程基本搞透。若大家对这里感兴趣,后续在单独的文章详细论述。

总的来说,Qzone方案好处在于开发透明,简单,这一套方案目前的应用成功率也是最高的,但在补丁包大小与性能损耗上有一定的局限性。特别是无论我们是否真正应用补丁,都会因为插桩导致对程序运行时的性能产生影响。微信对于性能要求较高,所以我们也没有采用这套方案。

五、 Andfix 阿里巴巴的支付宝

https://github.com/alibaba/AndFix

AndFix采用native hook的方式,这套方案直接使用dalvik_replaceMethod替换class中方法的实现。由于它并没有整体替换class, 而field在class中的相对地址在class加载时已确定,所以AndFix无法支持新增或者删除filed的情况(通过替换init与clinit只可以修改field的数值)。

在这里插入图片描述

也正因如此,Andfix可以支持的补丁场景相对有限,仅仅可以使用它来修复特定问题。结合之前的发布流程,我们更希望补丁对开发者是不感知的,即他不需要清楚这个修改是对补丁版本还是正式发布版本(事实上我们也是使用git分支管理+cherry-pick方式)。另一方面,使用native替换将会面临比较复杂的兼容性问题。

好处坏处Cool
立即生效兼容性不佳$
补丁较小开发不透明

相比其他方案,AndFix的最大优点在于立即生效。事实上,AndFix的实现与Instant Run的热插拔有点类似,但是由于使用场景的限制,微信在最初期已排除使用这一方案。

六、Instant Run

  1. 第一次编译apk:

    1. 通过transform api把instant-run.jar和instant-run-bootstrap.jar打包到主dex中,然后会替换AndroidManifest.xml中的application配置,使用的是bootstrapapplication来代理我们的application
    2. 接着使用asm在每个类中添加$change字段,在每个方法前加入一段调用逻辑,最后把源代码编译成dex,然后存放到压缩包instant-run.zip中。
  2. app运行期:

    1. bootstrapapplication提供了2个方法,attachBaseContext和onCreate方法。
    2. 在attachBaseContext方法中,首先是获取资源resource.ap_的路径,然后调用setupClassLoader方法将原有的BootClassLoader —> PathClassLoader改为BootClassLoader —> IncrementalClassLoader → PathClassLoader继承关系,
    3. 接着调用createRealApplication创建app真实的application,并获取真实application的生命周期。
    4. 在onCreate方法中,依次执行monkeyPatchApplication(反射替换ActivityThread中的各种Application成员变量)、monkeyPatchExistingResource(反射替换所有存在的AssetManager对象),最后会判断一个Server是否启动,没有启动则启动,Socket接收patch列表,并调用realApplication的onCreate方法
  3. 有代码修改时:

    1. 首先生成对应的$override类
    2. instant-run提供一个AppPatchesLoaderImpl类,记录修改的类列表,然后打包成patch,
    3. 通过socket传递给app,app的server接收到patch之后,分别按照handleColdSwapPatch、handleHotSwapPatch、handleResourcePatch对patch进行处理,restart使patch生效。

七、Dexposed 阿里巴巴的淘宝

https://github.com/alibaba/dexposed

Dexposed不支持Art模式(5.0+),且写补丁有点困难,需要反射写混淆后的代码,粒度太细,要替换的方法多的话,工作量会比较大。 暂不多加研究,带解决该问题后详细查看

八 Tinker 微信

http://blog.csdn.net/tencent_bugly/article/details/51821722

比较

AndFix支持2.3-6.0,但是不清楚是否有一些机型的坑在里面,毕竟jni层不像java曾一样标准,从实现来说,方法类似Dexposed,都是通过jni来替换方法,但是实现上更简洁直接,应用patch不需要重启。但由于从实现上直接跳过了类初始化,设置为初始化完毕,所以像是静态函数、静态成员、构造函数都会出现问题,复杂点的类Class.forname很可能直接就会挂掉。

ClassLoader方案支持2.3-6.0,会对启动速度略微有影响,只能在下一次应用启动时生效,在空间中已经有了较长时间的线上应用,如果可以接受在下次启动才应用补丁,是很好的选择。

总的来说:

  • 在兼容性稳定性上,ClassLoader方案很可靠,但需要重启
  • 如果需要应用不重启就能修复,而且方法足够简单,可以使用AndFix
  • 而Dexposed由于还不能支持art,所以只能暂时放弃,希望开发者们可以改进使它能支持art模式,毕竟xposed的种种能力还是很吸引人的(比如hook别人app的方法拿到解密后的数据,嘿嘿),还有比如无痕埋点啊线上追踪问题之类的,随时可以下掉。

参考文章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值