Anroid插件化/模块化/热修复知识梳理

今天跟着大神们的blog来梳理一下Android 关于插件化 热修复等的知识。

关于模块化、插件化、组件化

单工程模式
就是我们单纯的直接开发项目时,就二话不说的开new Project->分包 然后开怼。写的就很爽。这种模式不涉及到花里胡哨的东西,上手快,开发快(当然如果你的demo其实很小,那也没有必有用到别的模式)。一般项目刚起步都比较小,一些附加业务都没有绑定到App上面。

模块化
AS出来之后,多了一个概念,Project Module模块,Module中包含两种格式:application、library。也就是说一个Module是一个小的项目,也是AS概念中的模块。因此我们开始设计common等模块,对于包来说,模块更加灵活,耦合度更低,随意插拔,就跟引入第三方框架一样,想用哪一个就用哪一个。模块化就是将一个项目根据可以共享的部分抽取出来形成一个module,这就是模块化。

组件化
组件化和模块化有着比较小的差别,其核心是 模块角色的可转化性
比如说在打包的时候是library,在调试时就是application。
Module分为application和library。library就是引用库,如我抽取的common。application就是一个apk,是一个完整的项目。
当我们在调试自己的代码的时候就相当于调试一个小的App。就相当一个上百M的大项目分成了多个只有10几M的小项目,这个时候只需要关注自己的代码调试,不用在意别的代码怎么样就行了。

插件化
插件化严格意义也是模块化的进阶概念。将一个完整的工程,按业务分成不同的插件,这是分治的一种体现。也是相当于将一个大项目分成许多小项目,化整为零。那这样又和之前讲到的组件化有什么区别呢?

组件化的单位是Module,插件化的单位是一整个apk
组件化实现的是解耦和快编译,隔离不需要关注的部分
插件化也能实现组件化的东西,同时也能实现热插拔(热更新)
组件化的灵活性在加载Apk,比如说动态下载、在线更新
按照朋友圈来举例子,组件化就是已经存在了朋友圈,我想单独调试和维护,和别人不耦合,但是和整个微信有关联。插件化就是朋友圈就是个app,这个时候需要把它整合到微信这个更大的app中去。

其实就是字面上的意思:
组件化:就是将应用里面的内容分成一块一块的组件。
插件化:就是将应用作为一个插件,去和别的插件进行组装。

插件化和热修复的关系

从技术角度来说,他们都是从系统加载器的角度出发,无论是采用hook方式,亦或者是代理方式或者是其它底层实现。(之前说过,热修复就是插件化思想的一种实现) 都是通过“欺骗”Android系统的方式让宿主正常加载和运行插件(补丁)中的内容。
接下来了解一下称谓约定:

  • 宿主:就是当前运行的app
  • 插件:相对于插件化技术来说,就是要加载运行的apk文件
  • 补丁:对于热修复来说,就是要加载运行的 .patch,.dex,*.apk等一系列包含dex修复内容的文件。

插件化技术的典型应用

关于动态加载,早期的一些换肤功能就是通过下载皮肤的apk文件、还有特定节日的促销活动,逃避审核机制的动态广告加载都是Android插件技术可以考虑实现的。

类加载原理

热修复原理和类加载原理相关,和java类似,Android中类的加载也是通过classload来完成,具体来说就是PathClassLoader和DexClassLoader两个加载器,他们的区别是:

  • PathClassLoader:只能加载到已经安装到Android系统的apk文件,是android使用的默认的类加载器。
  • DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,也就是补丁啦。

这两个类都是继承自BaseDexClassLoader,我们可以看一下BaseDexClassLoader的构造函数

   public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

它做的事就是对传入进来的参数初始化了一个DexPathList对象。
DexPathList的构造函数就是将传递进来的程序文件(就是补丁文件)封装成一个Element对象,并将这些对象添加到Element的数组集合dexElement中去。

classloader的加载机制是双亲委派机制,这种机制下一个Class只会被加载一次。
这里对于一个ClassLoader来说,一个Class加载到内存中其实是有虚拟机完成的,对于开发者来说,我们的关注点应该是如何去找到这个需要加载的类

假设我们现在要找一个名为name的class,那么DexClassLoader将会走下面几个步骤:

  • 在DexClassLoader的findClass中通过DexPathList的findClass()方法来获取一个class。
  • 在DexPathListh的findClass方法里面去遍历之前说的集合dexElement,如果找到类名和name相同,则直接返回这个class,否则返回null。

总的来说通过DexClassLoader查找一个类最终就是在dexElement中查找特定类的操作。

综上所述我们可以描述出一个热修复的方案:当我们发现我们代码中某个类或者某几个类是有bug的,那我们在修复完这个类后,打包成一个补丁文件,然后通过这个补丁文件打包封装成一个Elemt对象,并将这个Element对象插入到原有dexElements数组的最前端。这样当DexClassLoader去类时,优先会从我们这个Element中找到对应的类,然后加载了一个没有bug的类,虽然原有的bug类还存在于数组的后面,但是由于双亲委派机制,这个类已经没有机会再加载了,这样一个bug就在没有重新安装应用的情况下修复了

现在市面上有许多热修复框架,按照公司团队可以分为以下几种:

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

修复框架很多,但是核心修复技术只有三类:代码修复、资源修复和动态链接库修复
而每个修复框架也有自己的特点,我们根据需要去使用修复框架。

代码修复

代码修复主要有三个方案,分别是底层替换方案、类加载方案和Instant Run方法。

类加载方案
类加载方案基于Dex分包方案,什么是Dex分包方案呢?得先从65536和LinearAlloc限制说起。

  • 65536限制:就是指一个应用中应用的方法数超过了65536个,因为DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多只能引用65535个方法。
  • LinearAlloc限制:在安装时会提示INSTALL_FAILED_DEXOPT。产生的原因就是DVM中LinearAlloc是一个缓存区,当方法数过多超出了缓存区的大小就会报错。

为了解决上述两个问题,从而产生了Dex分包方案。方案主要是在打包的过程中将代码分成多个dex,将应用启动时和重要的类放在主dex中,其他代码放在次dex包中。当应用加载时,先加载主dex,等到应用加载好之后再加载次dex,从而缓解了上述两个限制。

没错啦,之前提介绍过的,通过DexPathList中的dexElement查找就是上述的实现。我们来看看DexPathList中的findClass方法:

public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;

Element封装了DexFile,DexFile终于加载dex文件,所以每个dex文件对应一个Element。element的findClass方法会调用DexFile的loadClassBinaryName方法查找类。我们将修改好的类打包成含dex的补丁包 Patch.jar,放在Element数组中的第一个,然后接下来的之前有介绍过~~~~~

底层替换方案
底层替换方案不会再加载新类,而是在Native层修改原有的类,由于是在原有的类进行修改,所以限制比较多,不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时无法通过索引找到正确的方法,同样字段也是类似的情况。
底层替换方案和反射的原理也有些关联,就拿替换方法来说,方法我们可以调用java.lang.Class.getDeclaredMethod,假设我们要反射Key的show方法,会调用如下:

   Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());
  
   //invoke方法
     @FastNative
    public native Object invoke(Object obj, Object... args)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;

    //invoke是native方法,对应的jni代码为:

static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
                             jobject javaArgs) {
  ScopedFastNativeObjectAccess soa(env);
  return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);}

    //而InvkeMethod又调用了InvokeMethod函数:
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方法的所有信息,包括执行入口、访问权限、所属类和代码执行的地址等等。ArtMehod的结构体如下:


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_;

最重要的就是注释1中的dex_cache_resolved_methods和注释2的entry_point_from_quick_compiled_code,它们是方法的执行入口,当我们调用某一个方法时(比如Key的show)就会取得show方法的执行入口,通过执行入口就可以跳过去执行show方法。我们通过替换ArtMethod的字段者替换整个ArtMethod结构体,这就是底层替换方案。
AndFix采用的是替换ArtMethod中的字段,这样会有兼容问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。
Sophix采用的是替换整个ArtMethod结构体,就不会存在兼容问题。
底层替换方案直接替换了方法,可以立即生效不需要重启。

Instant Run方案
Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似的代码:


IncrementalChange localIncrementalChange = $change;//1
        if (localIncrementalChange != null) {//2
            localIncrementalChange.access$dispatch(
                    "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                            paramBundle });
            return;
}

注释1是一个localIncrementalChange,值为$change,它实现了IncrementalChange这个接口。当我们点击InstantRun时,如果方法没有变化则change为null,就直接return,否则就生成替换类,这里我们假设MainActivity的onCreate做了修改,就会生成替换的类MainActivity.override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的change设置成MainActivity.override,因此满足了注释2的条件,汇之星override的access.dispatch方法,这个方法会根据参数"onCreate.(Landroid/os/Bundle;)V"执行MainActivity.override的onCreate方法,从而实现了onCreate方法的修改。
借鉴Instant Run的原理修复框架还有Robust和Aceso。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值