图片 16

是时候来一波Android插件化了

1. 功能介绍

一:模块

DynamicLoadApk 主要分为四大模块:
(1) DLPluginManager
插件管理模块,负责插件的加载、管理以及启动插件组件。
(2) Proxy:代理组件模块,目前包括
DLProxyActivity(代理Activity)、DLProxyFragmentActivity(代理 FragmentActivity)、DLProxyService(代理 Service)
(3) Proxy Impl:代理组件公用逻辑模块,与(2)中的 Proxy
不同的是,这部分并不是一个组件,而是负责构建、加载插件组件
的管理器。这些 Proxy Impl 通过反射得到插件组件,然后将插件与 Proxy
组件建立关联,最后调用插件组件的 onCreate 函数进行启动。
(4) Base Plugin:插件组件的基类模块,目前包括
DLBasePluginActivity(插件 Activity
的基类)、DLBasePluginFragmentActivity(插件 FragmentActivity
的基类)、DLBasePluginService(插件 Service 的基类)。

图片 1

DynamicLoadApk

是时候来一波Android插件化了


  • 是时候来一波Android插件化了
    • 前言
    • Android开发演进
    • 模块化介绍
    • 插件化介绍
    • 前提技术介绍
      • APK构成
        • Manifest
        • Application
        • 四大组件
        • so
        • resource
        • 安装路径
      • App启动流程介绍
        • IPC &
          Binder
        • AMS
      • 插件化技术问题与解决方案
        • 代码加载
          • Java
            ClassLoader
          • Android
            ClassLoader

            • PathClassLoader
            • DexClassLoader
        • 资源获取
        • Hook
    • 主流框架方案
      • Fragment加载
      • Activity代理
      • Activity占坑
    • 360RePlugin介绍
      • 主要优势
      • 集成与Demo演示
      • 原理介绍
        • host
          lib
        • host
          gradle
        • plugin
          lib
        • plugin
          gradle
    • 其他插件化方案
      • Instant
        App
      • 淘宝Atlas
      • 滴滴VirtualAPK
      • Small
    • 总结
    • 相关资料

1.1 简介

DynamicLoadApk 是一个开源的 Android 插件化框架。

插件化的优点包括:(1) 模块解耦,(2) 动态升级,(3)
高效并行开发(编译速度更快) (4) 按需加载,内存占用更低等等。

DynamicLoadApk 提供了 3
种开发方式,让开发者在无需理解其工作原理的情况下快速的集成插件化功能。

  1. 宿主程序与插件完全独立
  2. 宿主程序开放部分接口供插件与之通信
  3. 宿主程序耦合插件的部分业务逻辑

三种开发模式都可以在 demo 中看到。

二:原理

DynamicLoadApk 原理: 核心思想可以总结为两个字:代理。通过在
Manifest
中注册代理组件,当启动插件组件时首先启动一个代理组件,然后通过这个代理组件来构建、启动插件组件。
总体原理图

图片 2

加载插件并启动代理组件

(1) 首先通过 DLPluginManager 的 loadApk
函数加载插件,这步每个插件只需调用一次。
流程解析:

图片 3

loadApk流程

(2) 通过 DLPluginManager 的 startPluginActivity 函数启动代理 Activity。

图片 4

startPluginActivity启动代理组件流程

(3) 代理 Activity 启动过程中构建、启动插件 Activity。

图片 5

构建,启动插件组件流程

前言

今年(2017年)6月时候,有幸参加了在北京举行的GMTC大会,恰巧360的张炅轩大神分享了360的插件化方案——
RePlugin
,听了以后,受益匪浅。

因为是公司组织参加大会的,参会后需要技术分享,所以就选择介绍RePlugin以及Android插件化相关内容,本文也是主要介绍RePlugin以及自己对插件化的理解。

因为插件化涉及到的东西比较多,由于篇幅的限制,很多知识点只是简单介绍一下,同时会给出相关链接,读者可以点击作参考。

这几年,世面上就已经出现了不少几款插件化方案,同时热更新技术也是遍地开花。当时是比较抵触这类技术的,个人觉的这样会破坏Android的生态圈,但是毕竟出现了这么多的插件化方案,出现总是有道理的。本着学习的态度,还是要学习下插件化相关技术。

1.2 核心概念

(1) 宿主:主 App,可以加载插件,也称 Host。
(2) 插件:插件 App,被宿主加载的 App,也称 Plugin,可以是跟普通 App
一样的 Apk 文件。

(3) 组件:指 Android
中的Activity、Service、BroadcastReceiver、ContentProvider,目前 DL
支持Activity、Service以及动态的BroadcastReceiver。

(4) 插件组件:插件中的组件。

(5) 代理组件:在宿主的 Manifest
中注册,启动插件组件时首先被启动的组件。目前包括 DLProxyActivity(代理
Activity)、DLProxyFragmentActivity(代理
FragmentActivity)、DLProxyService(代理 Service)。

(6) Base 组件:插件组件的基类,目前包括 DLBasePluginActivity(插件
Activity 的基类)、DLBasePluginFragmentActivity(插件 FragmentActivity
的基类)、DLBasePluginService(插件 Service 的基类)。

DynamicLoadApk 原理的核心思想可以总结为两个字:代理。通过在 Manifest
中注册代理组件,当启动插件组件时首先启动一个代理组件,然后通过这个代理组件来构建、启动插件组件。

Android开发演进

Android开发初期,基本上没有什么框架的,什么东西都往Activity里面塞,最后Activity就变得很大。后面有些人借鉴了Java后端的思想,使用MVC模式,一定程度上解决了代码乱堆的问题,
使用了一段时间MVC后,Activity依旧变的很大,因为Activity里面不光有UI的逻辑,还有数据的逻辑。

MVC

再后来有了MVP,MVP解决了UI逻辑和数据逻辑在一起的问题,同时也解决了Android代码测试困难问题。

MVP

随着业务的增多,架构中有了Domain的概念,Domain从Data中获取数据,Data可能会是Net,File,Cache各种IO等,然后项目架构变成了这样。

MVP2

2. 总体设计

图片 6

<img
src=”
width=”759″ height=”581″ alt=”DynamicLoadApk 源码解析”
/>

图片 7

上面是 DynamicLoadApk 的总体设计图,DynamicLoadApk 主要分为四大模块:

(1) DLPluginManager

插件管理模块,负责插件的加载、管理以及启动插件组件。

(2) Proxy

代理组件模块,目前包括 DLProxyActivity(代理
Activity)、DLProxyFragmentActivity(代理
FragmentActivity)、DLProxyService(代理 Service)。

(3) Proxy Impl

代理组件公用逻辑模块,与(2)中的 Proxy
不同的是,这部分并不是一个组件,而是负责构建、加载插件组件的管理器。这些
Proxy Impl 通过反射得到插件组件,然后将插件与 Proxy
组件建立关联,最后调用插件组件的 onCreate 函数进行启动。

(4) Base Plugin

插件组件的基类模块,目前包括 DLBasePluginActivity(插件 Activity
的基类)、DLBasePluginFragmentActivity(插件 FragmentActivity
的基类)、DLBasePluginService(插件 Service 的基类)。

模块化介绍

MVP升级版用了一段时间以后,新问题又出现了。随着业务的增多,代码变的越来越复杂,每个模块之间的代码耦合变得越来越严重,解耦问题急需解决,同时编译时间也会越来越长。

开发人员增多,每个业务的组件各自实现一套,导致同一个App的UI风格不一样,技术实现也不一样,团队技术也无法得到沉淀,重复早轮子严重。

Modular

然后模块化(组件化)解决方案就出现了。

Modular2

3. 流程图

图片 8

<img
src=”
alt=”DynamicLoadApk 源码解析” />

图片 9

上面是调用插件 Activity 的流程图,其他组件调用流程类似。

(1) 首先通过 DLPluginManager 的 loadApk
函数加载插件,这步每个插件只需调用一次。

(2) 通过 DLPluginManager 的 startPluginActivity 函数启动代理 Activity。

(3) 代理 Activity 启动过程中构建、启动插件 Activity。

插件化介绍

讲道理,模块化已经是最终完美的解决方案了,为啥还需要插件化呢?

还是得从业务说起,如果一个公司有很多业务,并且每个业务可以汇总成一个大的App,又或者某一个小业务又需要单独做成一个小的App。

按照上面的说的模块化解决方案,需要把这个业务设计成一个模块,代码最终打包成一个aar,主App和业务App设计成一个运行壳子,编译打包时候使用Gradle做maven依赖即可。

举例说明美团和猫眼电影。

美团和猫眼

实际上这样做比较麻烦,主App和业务模块会或多或少依赖一点公共代码,如果公共代码出现变动,则需要对应做出修改。
同时业务代码会设计成Android Lib
project,开发、编译、调试也有点麻烦,那么能不能这样设计,某个业务模块单独做出一个Apk,主App直接使用插件的方式,如果需要某种功能,那么直接加载某一个apk,而不是直接依赖代码的形式。

4. 详细设计

前提技术介绍

通过上面的业务演进,最终我们需要做的就是一个Apk调用另外一个Apk文件,这也就是我们今天的主题——插件化。

一个常识,大家都知道,Apk只有在安装的情况下,才可以被运行调用。如果一个Apk只是一个文件,放置在存储卡上,我们如何才能调用起来呢?

对于这个问题,先保留,后面会做讲解,当然了已经有几种方案是可以这样做的。但是为了了解插件化的原理,先回顾一下基础知识。

4.1 类关系图

图片 10

<img
src=”
width=”749″ height=”664″ alt=”DynamicLoadApk 源码解析”
/>

图片 11

以上是 DynamicLoadApk
主要类的关系图,跟总体设计中介绍的一样大致分为三部分。

(1) 对于 Proxy 部分,每个组件都存在 DLAttachable
接口,方便统一该组件不同类,如
Activity、FragmentActivity。每个组件的公共实现部分都统一放到了对应的
DLProxyImpl 中。

(2) 对于 Base Plugin 部分,每个组件都存在 DLPlugin
接口,同样是方便统一该组件不同类。

APK构成

Apk是App代码最终编译打包生成的文件,主要包含代码(dex、so)、配置文件、资源问题、签名校验等。

4.2 类功能介绍

Manifest

App中系统组件配置文件,包括Application、Activity、Service、Receiver、Provider等。

App中所有可运行的Activity必须要在这里定义,否则就不能运行,也包括其他组件,Receiver也可以动态注册。(敲黑板,这里很重要,记住这句话。)

4.2.1 DLPluginManager.java

DynamicLoadApk 框架的核心类,主要功能包括:

(1) 插件的加载和管理;

(2) 启动插件的组件,目前包括 Activity、Service。

主要属性:

mNativeLibDir为插件 Native Library 拷贝到宿主中后的存放目录路径。

mPackagesHolderHashMap,key 为包名,value
为表示插件信息的DLPluginPackage,存储已经加载过的插件信息。

主要函数:

(1) getInstance(Context context)
获取 DLPluginManager 对象的单例。
在私有构造函数中将mNativeLibDir变量赋值为宿主 App
应用程序数据目录下名为pluginlib子目录的全路径。

(2) loadApk(String dexPath)
加载插件。参数 dexPath 为插件的文件路径。
这个函数直接调用 loadApk(final String dexPath, boolean hasSoLib)。

(3) loadApk(final String dexPath, boolean hasSoLib)
加载插件 Apk。参数 dexPath 为插件的文件路径,hasSoLib 表示插件是否含有
so 库。

注意:在启动插件的组件前,必须先调用上面两个函数之一加载插件,并且只能在宿主中调用。

流程图如下:

图片 12
图片 13

<img
src=”
alt=”DynamicLoadApk 源码解析” />

loadApk 函数调用 preparePluginEnv 函数加载插件,图中虚线框为
preparePluginEnv 的流程图。

(4) preparePluginEnv(PackageInfo packageInfo, String dexPath)
加载插件及其资源。流程图如上图。
调用createDexClassLoader(…)、createAssetManager(…)、createResources(…)函数完成相应初始化部分。

(5) createDexClassLoader(String dexPath)
利用DexClassLoader加载插件,DexClassLoader 初始化函数如下:

public DexClassLoader (String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

其中dexPath为插件的路径。
optimizedDirectory优化后的dex存放路径。这里将路径设置为当前 App
应用程序数据目录下名为dex的子目录中。
libraryPath为 Native Library
存放的路径。这里将路径设置为mNativeLibDir属性,其在getInstance(Context)函数中已经初始化。
parent父 ClassLoader,ClassLoader
采用双亲委托模式查找类,具体加载方式可见 ClassLoader
基础。

(6) createAssetManager(String dexPath)
创建 AssetManager,加载插件资源。
在 Android 中,资源是通过 R.java 中的 id
来调用访问的。但是实现插件化之后,宿主是无法通过 R
文件访问插件的资源,所以这里使用反射来生成属于插件的AssetManager,并利用addAssetPath函数加载插件资源。

    private AssetManager createAssetManager(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexPath);
            return assetManager;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

    }

AssetManager
的无参构造函数以及addAssetPath函数都被hide了,通过反射调用。

(7) createResources(AssetManager assetManager)
利用AssetManager中已经加载的资源创建Resources,代理组件中会从这个Resources中读取资源。
关于AssetManager、Resources深入的信息可参考:Android
应用程序资源的查找过程分析。

(8) copySoLib(String dexPath)
调用SoLibManager拷贝 so 库到 Native Library 目录。

(9) startPluginActivity(Context context, DLIntent dlIntent)
启动插件 Activity,会直接调用startPluginActivityForResult(…)函数。
插件自己内部 Activity 启动依然是调用Context#startActivity(…)方法。

(10) startPluginActivityForResult(Context context, DLIntent dlIntent,
int requestCode)

启动插件 Activity,流程图如下:

图片 14
图片 15

<img
src=”
alt=”DynamicLoadApk 源码解析” />

(11) startPluginService(final Context context, final DLIntent
dlIntent)

启动插件 Service。
主要逻辑在函数fetchProxyServiceClass(…)中,流程与startPluginActivity(…)类似,只是换成了回调的方式,在各种条件成立后调用原生方式启动代理
Service,不再赘述。

(12) bindPluginService(…) unBindPluginService(…)
bind 或是 unBind 插件
Service。逻辑与startPluginService(…)类似,不再赘述。

Application

App启动,代码中可以获取到被运行调用的第一个类,常用来做一些初始化操作。

4.2.2 DLPluginPackage

插件信息对应的实体类,主要属性如下:

    public String packageName;

    public String defaultActivity;

    public DexClassLoader classLoader;

    public AssetManager assetManager;

    public Resources resources;

    public PackageInfo packageInfo;

packageName为插件的包名;
defaultActivity为插件的 Launcher Main Activity;
classLoader为加载插件的 ClassLoader;
assetManager为加载插件资源的 AssetManager;
resources利用assetManager中已经加载的资源创建的Resources,代理组件中会从这个Resources中读取资源。
packageInfo被PackageManager解析后的插件信息。
这些信息都会在DLPluginManager#loadApk(…)时初始化。

四大组件

四大系统组件Activity、Service、Receiver、Provider,代码中继承系统中的父类。如上面所说,必须要在manifest中配置定义,否则不可以被调用。

4.2.3 DLAttachable.java/DLServiceAttachable.java

DLServiceAttachable 与 DLAttachable 类似,下面先分析 DLAttachable.java。

DLAttachable 是一个接口,主要作用是以统一所有不同类型的代理
Activity,如DLProxyActivity、DLProxyFragmentActivity,方便作为同一接口统一处理。
DLProxyActivity和DLProxyFragmentActivity都实现了这个类。

DLAttachable 目前只有一个

attach(DLPlugin pluginActivity, DLPluginManager pluginManager)

抽象函数,表示将插件Activity和代理Activity绑定在一起,其中的pluginActivity参数就是指插件Activity。

同样 DLServiceAttachable 类似,作用是统一所有不同类型的代理
Service,实现插件Service和代理Service的绑定。虽然目前只有DLProxyService。

so

App中C、C++代码编译生成的二进制文件,与手机的CPU架构相关,不同CPU架构生成的文件有些不同。开发中常常会生成多份文件,然后打包到Apk中,不同CPU类型,会调用不同的文件。

4.2.4 DLPlugin.java/DLServicePlugin.java

DLPlugin 与 DLServicePlugin 类似,下面先分析 DLPlugin.java。

DLPlugin 是一个接口,包含Activity生命周期、触摸、菜单等抽象函数。
DLBase*Activity 都实现了这个类,这样插件的 Activity 间接实现了此类。
主要作用是统一所有不同类型的插件
Activity,如Activity、FragmentActivity,方便作为同一接口统一处理,所以这个类叫DLPluginActivity更合适。

同样 DLServicePlugin 主要作用是统一所有不同类型的插件
Service,方便作为统一接口统一处理,目前包含Service生命周期等抽象函数。

resource

Android中资源文件比较多,通常放在res和assets文件夹下面。常见的有布局、图片、字符、样式、主题等。

4.2.5 DLProxyActivity.java/DLProxyFragmentActivity.java

代理 Activity,他们是在宿主 Manifest 中注册的组件,也是启动插件 Activity
时,真正被启动的 Activity,他们的内部会完成插件 Activity
的初始化和启动。

这两个类大同小异,所以这里只分析DLProxyActivity。

首先来看下它的成员变量。

(1). DLPlugin mRemoteActivity
表示真正需要启动的插件Activity。这个属性名应该叫做pluginActivity更合适。

上面我们已经介绍了,DLPlugin是所有插件Activity都间接实现了的接口。

接下来在代理Activity的生命周期、触摸、菜单等函数中我们都会同时调用
mRemoteActivity 的相关函数,模拟插件Activity的相关功能。

(2). DLProxyImpl impl

主要封装了插件Activity的公用逻辑,如初始化插件 Activity 并和代理
Activity 绑定、获取资源等。

安装路径

上面的介绍的Apk结构,那么Apk安装以后,它的安装位置在哪,资源和数据又放在哪里呢?

安装路径

/data/app/{package}/主要放置Apk文件,同时Cpu对应的so文件也会被解压到对应的文件夹中,Android高级版本中还会对dex做优化,生成odex文件也在这个文件夹中。

data/data/{package}/主要存放App生成的数据,比如SharedPreferences、cache等其他文件。

那么问题来了,如果调用为安装的Apk,假设能够运行,那么他们的运行文件放在哪里?代码中生成的数据文件又要放在哪里?

4.2.6 DLProxyImpl.java/DLServiceProxyImpl.java

DLProxyImpl 与 DLServiceProxyImpl 类似,下面先分析 DLProxyImpl.java。

DLProxyImpl 主要封装了插件Activity的公用逻辑,如初始化插件 Activity
并和代理 Activity
绑定、获取资源等,相当于把DLProxyActivity和DLProxyFragmentActivity的公共实现部分提出出来,核心逻辑位于下面介绍的
onCreate() 函数。

主要函数:

(1) DLProxyImpl(Activity activity)
构造函数,参数为代理 Activity。

(2) public void onCreate(Intent intent)
onCreate 函数,会在代理 Activity onCreate 函数中被调用,流程图如下:

图片 16
图片 17

<img
src=”
alt=”DynamicLoadApk 源码解析” />

其中第一步设置 intent 的 ClassLoader是用于 unparcel Parcelable
数据的,可见介绍:
android.os.BadParcelableException。

(3) protected void launchTargetActivity()
加载待启动插件 Activity
完成初始化流程,并通过DLPlugin和DLAttachable接口的 attach 函数实现和代理
Activity 的双向绑定。流程图见上图虚线框部分。

(4) private void initializeActivityInfo()
获得待启动插件的 ActivityInfo。

(5) private void handleActivityInfo()
设置代理 Activity 的主题等信息。

其他的 get* 函数都是获取一些插件相关信息,会被代理 Activity 调用。

同样 DLServiceProxyImpl 主要封装了插件Service的公用逻辑,如初始化插件
Service 并和代理 Activity 绑定。

App启动流程介绍

App的二进制文件Apk安装以后,就可以直接启动了,直接点击Launcher上面的图片即可,但是我们需要的是一个App启动另外一个apk文件,所以有必要了解下App的启动流程。

4.2.7 DLBasePluginActivity.java/DLBasePluginFragmentActivity.java

插件 Activity 基类,插件中的Activity都要继承
DLBasePluginActivity/DLBasePluginFragmentActivity 之一(目前尚不支持
ActionBarActivity)。

主要作用是根据是否被代理,确定一些函数直接走父类逻辑还是代理 Activity
或是空逻辑。

DLBasePluginActivity继承自Activity,同时实现了DLPlugin接口。这两个类大同小异,所以这里只分析DLProxyActivity。
主要变量:

    protected Activity mProxyActivity;

    protected Activity that;

    protected DLPluginManager mPluginManager;

    protected DLPluginPackage mPluginPackage;

mProxyActivity为代理 Activity,通过attach(…)函数绑定。
that与mProxyActivity等同,只是为了和this指针区分,表示真实的Context,这里真实指的是被代理情况下为代理
Activity,未被代理情况下等同于 this。

IPC & Binder

在Android系统中,每一个应用程序都是由一些Activity和Service组成的,这些Activity和Service有可能运行在同一个进程中,也有可能运行在不同的进程中。那么,不在同一个进程的Activity或者Service是如何通信的呢?

Android系统提供一种Binder机制,能够使进程之间相互通信。

Android进程间通信资料

4.2.8 DLBasePluginService.java

插件 Service 基类,插件中的 Service
要继承这个基类,主要作用是根据是否被代理,确定一些函数直接走父类逻辑还是代理
Service 或是空逻辑。

主要变量含义与DLBasePluginActivity类似,不重复介绍。
PS:截止目前这个类还是不完善的,至少和DLBasePluginActivity对比,还不支持非代理的情况

AMS

Activity启动流程说个一天也说不完,过程很长,也很繁琐,不过我们只要记住了AMS就可以了。

Android系统应用框架篇:Activity启动流程

盗一张图

AMS

4.2.9 DLIntent.java

继承自 Intent,封装了待启动组件的 PackageName 和 ClassName。

插件化技术问题与解决方案

4.2.10 SoLibManager.java

调用SoLibManager拷贝 so 库到 Native Library 目录。

主要函数:

(1) copyPluginSoLib(Context context, String dexPath, String
nativeLibDir)

函数中以ZipFile形式加载插件,循环读取其中的文件,如果为.so结尾文件、符合当前平台
CPU 类型且尚未拷贝过最新版,则新建Runnable拷贝 so 文件。

代码加载

按照正常思路,如果一个主Apk需要运行一个插件Apk,那么怎么样才能把里面的代码加载过来呢?

4.2.11 DLUtils.java

这个类中大都是无用或是不该放在这里的函数,也许是大版本升级及维护人过多后对工具函数的维护不够所致。

Java ClassLoader

Java中提供了ClassLoader方式来加载代码,然后就可以运行其中的代码了。这里有一份资料(深入分析Java
ClassLoader原理)
,可以简单了解下。

  • 原理介绍

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap
ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。
当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap
ClassLoader试图加载,
如果没加载到,则把任务转交给Extension
ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader
进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。
如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

  • 为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,
因为String已经在启动时就被引导类加载器(Bootstrcp
ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

  • 但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,
并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,
就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。

5. 杂谈

Android ClassLoader

Android 的 Dalvik/ART 虚拟机如同标准 Java 的 JVM
虚拟机一样,也是同样需要加载 class 文件到内存中来使用,但是在
ClassLoader 的加载细节上会有略微的差别。

热修复入门:Android 中的
ClassLoader比较详细介绍了Android中ClassLoader。

在Android开发者官网上的ClassLoader的文档说明中我们可以看到,
ClassLoader是个抽象类,其具体实现的子类有
BaseDexClassLoader和SecureClassLoader。

SecureClassLoader的子类是URLClassLoader,其只能用来加载jar文件,这在Android的
Dalvik/ART 上没法使用的。

BaseDexClassLoader的子类是PathClassLoader和DexClassLoader 。

5.1 插件不能打包 dl-lib.jar

原因是插件和宿主属于不同的 ClassLoader,如果同时打包 dl-lib.jar,会因为
ClassLoader 隔离导致类型转换错误,具体可见:ClassLoader
隔离

Eclipse 打包解决方式见项目主页;
Android Studio 打包解决方式见 5.2;
Ant 打包需要修改 build.xml 中 dex target 引用到的 compileclasspath
属性。

PathClassLoader

PathClassLoader 在应用启动时创建,从/data/app/{package}安装目录下加载
apk 文件。

有2个构造函数,如下所示,这里遵从之前提到的双亲委托模型:

public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
    super(dexPath, null, libraryPath, parent);
}
  • dexPath :
    包含dex的jar文件或apk文件的路径集,多个以文件分隔符分隔,默认是“:”

  • libraryPath : 包含 C/C++
    库的路径集,多个同样以文件分隔符分隔,可以为空

PathClassLoader
里面除了这2个构造方法以外就没有其他的代码了,具体的实现都是在
BaseDexClassLoader 里面,其dexPath比较受限制,一般是已经安装应用的 apk
文件路径。

在Android中,App安装到手机后,apk里面的class.dex中的class均是通过PathClassLoader来加载的。

5.2 在 Android Studio 下使用 DynamicLoadApk

在使用 DynamicLoadApk 时有个地方要注意,就是插件 Apk 在打包的时候不能把
dl-lib.jar 文件打包进去,不然会报错(java.lang.IllegalAccessError: Class
ref in pre-verified class resolved to unexpected
implementation)。换句话说,dl-lib.jar
要参与编译,但不参与打包。该框架作者已经给出了 Eclipse
下的解决方案。我这里再说下怎么在 Android Studio 里使用。

dependencies {
        provided fileTree(dir: 'dl-lib', include: ['*.jar'])
    }
DexClassLoader

介绍 DexClassLoader 之前,先来看看其官方描述:

A class loader that loads classes from .jar and .apk filescontaining a
classes.dex entry. This can be used to execute code notinstalled as
part of an application.

很明显,对比 PathClassLoader
只能加载已经安装应用的dex或apk文件,DexClassLoader则没有此限制,可以从SD卡上加载包含class.dex的.jar和.apk
文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的dex的加载。

DexClassLoader 的源码里面只有一个构造方法,这里也是遵从双亲委托模型:

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

参数说明:

  • String dexPath : 包含 class.dex 的 apk、jar 文件路径
    ,多个用文件分隔符(默认是 :)分隔

  • String optimizedDirectory : 用来缓存优化的 dex 文件的路径,即从 apk
    或 jar 文件中提取出来的 dex
    文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径(实际上也可以使用外部存储空间

  • String libraryPath : 存储 C/C++ 库文件的路径集

  • ClassLoader parent : 父类加载器,遵从双亲委托模型

5.3 DynamicLoadApk 待完善的问题

(1) 还未支持广播;
(2) Base Plugin 中的 that 还未去掉,需要覆写 Activity 的相关方法;
(3) 插件和宿主资源 id 可能重复的问题没有解决,需要修改 aapt 中资源 id
的生成规则;
(4) 不支持自定义主题,不支持系统透明主题;
(5) 插件中的 so 处理有异常;
(6) 不支持静态 Receiver;
(7) 不支持 Provider;
(8) 插件不能直接用 this;

资源获取

我们知道,Android
Apk里面除了代码,剩下的就是资源,而且资源占了很大一部分空间,我们可以利用ClassLoader来加载代码,那么如何来加载apk中的资源,而且Android中的资源种类又可以分为很多种,比如布局、图片,字符、样式、主题等。

在组件中获取资源时使用getResource获得Resource对象,通过这个对象我们可以访问相关资源,比如文本、图片、颜色等。

通过跟踪源码发现,其实getResource方法是Context的一个抽象方法,getResource的实现是在ContextImp中实现的。
获取的Resource对象是应用的全局变量,然后继续跟踪源码,发现
Resource中有一个AssetManager的全局变量,在Resource的构造函数中传入的,所以最终获取资源都是通过AssetManager获取的,于是我们把注意力放到AssetManager上。

我们要解决下面两个问题。

一、如何获取AssetManager对象。

二、如何通过AssetManager对象获取插件中apk的资源。

通过对AssetManager的相关源码跟踪,我们找到答案。

一、AssetManager的构造函数没有对api公开,不能使用new创建;context.getAssets()可用获取当前上下文环境的
AssetManager;利用反射
AssetManager.class.newInstance()这样可用获取对象。

二、如何获取插件apk中的资源。我们发现AssetManager中有个重要的方法。

/**
 * Add an additional set of assets to the asset manager.  This can be
 * either a directory or ZIP file.  Not for use by applications.  Returns
 * the cookie of the added asset, or 0 on failure.
 * {@hide}
 */
public final int addAssetPath(String path) {
    return  addAssetPathInternal(path, false);
}

我们可以把一个包含资源的文件包添加到assets中。这就是AssetManager查找资源的第一个路径。这个方法是一个隐藏方法,我们可以通过反射调用。

AssetManager assetManager = AssetManager.class.newInstance() ; // context .getAssets()?
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);
Resources pluginResources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

5.4 其他插件化方案

除了 DynamicLoadApk 用代理的方式实现外,目前还有两种插件化方案:
(1) 用 Fragment 以及 schema 的方式实现。
(2) 利用字节码库动态生成一个插件类 A 继承自待启动插件 Activity,启动插件
A。这个插件 A 名称固定且已经在 Manifest 中注册。
具体可见:Android 插件化

最后 H5
框架越来越多,也能解决插件化解决的自动升级这部分功能,硬件、网络也在改善,未来何如?

Hook

Hook就是可以修改函数的调用,通常可以通过代理模式就可以达到修改的目的。

比如有个Java示例代码

public interface IService {

    void fun();
}
public class ServiceImpl implements IService {

    private static final String TAG = "ServiceImpl";

    @Override
    public void fun() {
        Log.i(TAG, "fun: ");
    }
}

正常调用直接这样就可以了。

public class MainActivity extends AppCompatActivity {

    private IService iService;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        iService = new ServiceImpl();
        callService();
    }

    void callService() {
        iService.fun();
    }
}

上面代码中MainActivity中含有iService字段,可以利用反射机制来替换它,然后当有其他地方调用iService的时候,就可以对调用方法进拦截和处理。

可以先实现自己的代理类,对需要Hook的地方添加下代码。

public class ServiceProxy implements IService {

    private static final String TAG = "ServiceProxy";

    @NonNull
    private IService base;

    public ServiceProxy(@NonNull IService base) {
        this.base = base;
    }

    @Override
    public void fun() {
        Log.i(TAG, "fun: before");
        base.fun();
        Log.i(TAG, "fun: after");
    }
}

然后再修改MainActivity中的iService的值,首先获取iService字段的值,传给自己定义的Proxy对象,然后把Proxy对象再赋值给原先的iService字段,这样调用iService中方法的时候,就会执行Proxy的方法,然后由Proxy再进行处理。

void reflectHock() {
    try {
        Class<? extends MainActivity> aClass = MainActivity.class;
        Field field = aClass.getDeclaredField("iService");
        field.setAccessible(true);
        IService service = (IService) field.get(this);
        IService proxy = new ServiceProxy(service);
        field.set(this, proxy);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

当然有时候,实现自己的Proxy类是很麻烦的,可以利用Java的动态代理技术来搞定。

public class MyInvocationHandler implements InvocationHandler {

    private static final String TAG = "MyInvocationHandler";

    @NonNull
    private IService service;

    public MyInvocationHandler(@NonNull IService service) {
        this.service = service;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        Log.i(TAG, "invoke: before");
        Object result = method.invoke(service, objects);
        Log.i(TAG, "invoke: after");
        return result;
    }
}

void proxyHook() {
    try {
        Class<? extends MainActivity> aClass = MainActivity.class;
        Field field = aClass.getDeclaredField("iService");
        field.setAccessible(true);
        IService value = (IService) field.get(this);

        InvocationHandler handler = new MyInvocationHandler(value);
        ClassLoader classLoader = value.getClass().getClassLoader();
        Object instance = Proxy.newProxyInstance(classLoader, value.getClass().getInterfaces(), handler);

        field.set(this, instance);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

主流框架方案

Fragment加载

早在2012年时候,出现了一个简单的Android插件化方案,原理大致这样的。

我们知道Android基本的页面元素是Activity,如果要动态加载一个界面,那么需要动态加载加载一个Activity,但是Activity是需要注册在Manifest中的。

所以就把目标瞄向了Fragment,首先Fragment是不需要注册的,使用的时候直接new出一个对象即可,然后放到了Activity容器中即可,那么能否从一个apk中加载出来一个FragmentClass,然后使用反射实例化,然后放入到Activity中呢?

答案是可以的,首先在Manifest中定义个容器HostContainerActivity,然后页面跳转的时候通过intent,把目标的页面的fragment的class写成路径,
当 HostContainerActivity
页面启动,从intent中获取Fragment的路径,然后利用反射,动态new出一个示例放入到布局中即可。

AndroidDynamicLoader就是这样一个解决方案,但是这个方案是有限制的,所有的页面必须是Fragment,这样肯定不符合要求,所以这个方案就没有流行起来。

Activity代理

上面说道了使用Fragment加载的形式,来显示插件中的页面,但是这个解决方案是有限制的,界面全部只能用Fragment,不能用Activity,不能称的上是一种完美的插件化解决方案。

那到底能不能用到Activity的方式,答案是肯定的。

可以这样,上面介绍了Fragment动态加载原理,我们把Fragment的路径换成Activity的路径,然后用原先的那个容器Activity,做为一个代理Activity,当HostContainerActivity启动时候,
初始化将要显示的Activity,然后当容器Activity依次执行对应的生命周期时候,容器Activity做一个代理Activity,也要相应执行动态加载的Activity。

大致代码示例如下:

public class HostContainerActivity extends BaseActivity {

    public static final String EXTRA_BASE_ACTIVITY = "extra_base_activity";
    private BaseActivity remote;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        String clazz = getIntent().getStringExtra(EXTRA_BASE_ACTIVITY);
        try {
            remote = (BaseActivity) Class.forName(clazz).newInstance();
            remote.onCreate(savedInstanceState);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onStart() {
        super.onStart();

        remote.onStart();
    }

    @Override
    public void onResume() {
        super.onResume();

        remote.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();

        remote.onPause();
    }

    @Override
    public void onStop() {
        super.onStop();

        remote.onStop();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        remote.onDestroy();
    }
}

dynamic-load-apk
这个动态化框架就是利用这个原理来实现的。

但是这个方案还是有限制的,因为插件中的Activity并不是系统直接运行的,而是由另外一个Activity作为代理运行的,这个Activity不是一个真正的Activity,
很多的功能是限制的,比如需要在Activity弹出一个Toast,则是不行的,因为当前的Activity没有context,所以dynamic-load-apk提出了1个关键字——that,
java中this表示对象本身,但是本对象不能当做context使用,因为当前的Activity只是一个Java对象,而that是真正运行的Activity对象。

Activity占坑

上面介绍Activity代理的方法,虽然插件中可以正常使用Activity,但是限制还是很多,用起来很不方便。

那到底有没有最优解,既可以不需要注册Activity,又可以动态的加载Activity,答案是肯定的。我们可以来一个偷梁换柱,既然要注册咱们就先注册一个,然后启动的时候,
把需要的运行的Activity当做参数传递过去,让系统启动那个替身Activity,当时机恰当的时候,我们再把那个Activity的对象给换回来即可,这个叫做瞒天过海。

这里有一篇文章详细记载了Activity占坑方案是怎么运行的以及方案的原理。

360RePlugin介绍

Ok,上面说了这么多,全部都是引子,下面着重介绍今天的主角——RePlugin。

RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin
Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。

主要优势

  • 极其灵活:

主程序无需升级(无需在Manifest中预埋组件),即可支持新增的四大组件,甚至全新的插件

  • 非常稳定:

Hook点仅有一处(ClassLoader),无任何Binder
Hook!如此可做到其崩溃率仅为“万分之一”,并完美兼容市面上近乎所有的Android
ROM

  • 特性丰富:

支持近乎所有在“单品”开发时的特性。包括静态Receiver、Task-Affinity坑位、自定义Theme、进程坑位、AppCompat、DataBinding等

  • 易于集成:

无论插件还是主程序,只需“数行”就能完成接入

  • 管理成熟:

拥有成熟稳定的“插件管理方案”,支持插件安装、升级、卸载、版本管理,甚至包括进程通讯、协议版本、安全校验等

  • 数亿支撑:

有360手机卫士庞大的数亿用户做支撑,三年多的残酷验证,确保App用到的方案是最稳定、最适合使用的

集成与Demo演示

集成也非常简单,比如有2个工程,一个是主工程host,一个是插件工程sub。

本人写作的时候,RePlugin版本为2.1.5,可能会与最新版本不一致。

  • 添加Host根目录Gradle依赖

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        classpath 'com.qihoo360.replugin:replugin-host-gradle:2.1.5'
    }
}
  • 添加Host项目Gradle依赖

apply plugin: 'com.android.application'
apply plugin: 'replugin-host-gradle'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.0"
    defaultConfig {
        applicationId "cn.mycommons.replugindemo"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

repluginHostConfig {
    useAppCompat = true
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.qihoo360.replugin:replugin-host-lib:2.1.5'

    testCompile 'junit:junit:4.12'
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
}
  • 添加Sub根目录Gradle依赖

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.1.5'
    }
}
  • 添加Sub项目Gradle依赖

apply plugin: 'com.android.application'
apply plugin: 'replugin-plugin-gradle'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.0"

    defaultConfig {
        applicationId "cn.mycommons.repluginsdemo.sub"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

repluginPluginConfig {
    //插件名
    pluginName = "app"
    //宿主app的包名
    hostApplicationId = "cn.mycommons.replugindemo"
    //宿主app的启动activity
    hostAppLauncherActivity = "cn.mycommons.replugindemo.MainActivity"

    // Name of 'App Module',use '' if root dir is 'App Module'. ':app' as default.
    appModule = ':app'

    // Injectors ignored
    // LoaderActivityInjector: Replace Activity to LoaderActivity
    // ProviderInjector: Inject provider method call.
    // ignoredInjectors = ['LoaderActivityInjector']
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.qihoo360.replugin:replugin-plugin-lib:2.1.5'

    testCompile 'junit:junit:4.12'
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
}

原理介绍

RePlugin源码主要分为4部分,对比其他插件化,它的强大和特色,在于它只Hook住了ClassLoader。One
Hook这个坚持,最大程度保证了稳定性、兼容性和可维护性。

host lib

插件宿主库,主要是对插件的管理,以及对ClassLoader的Hook,具体原理和管理逻辑不做详细解释。

host gradle

对插件宿主代码编译过程进行处理,主要有config.json文件生成、RePluginHostConfig.java代码生成、以及Activity坑位代码插入到Manifest中。

比如我们内置一个插件,按照官方文档,这样操作的。

  • 将APK改名为:[插件名].jar

  • 放入主程序的assets/plugins目录

我们可以看看Host apk中包含哪些资源。

插件自动生成了plugin-builtin.json文件

同时也在Manifest中插入很多坑位。

[图片上传失败…(image-469f3a-1513305916950)]

RePluginHostConfig.java代码生成逻辑。

plugin lib

同宿主库一样,这个是给插件App提供基本的支持。

plugin gradle

对插件App代码编译过程进行处理,主要修改插件中四大组建的父类,没错,就是这样。

比如有个LoginActivity,它是继承Activity的,那么会修改它的父类为PluginActivity,如果是AppCompatActivity,那么会替换成PluginAppCompatActivity

如:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
    }
}

反编译Apk可以看到修改后的结果。

源码里面也有体现

其他插件化方案

上次大致是RePlugin的原理,当然除了RePlugin的解决方案以外,还有其他几家厂商的解决方案。

Instant App

Android Instant App
官网

16年IO的时候,Google提出了Instant
App特性,在17年IO正式发布这项技术,不过这项技术在我写这篇文章的时候,还是beta版本。

它的使用方式很简单,你在 Android
手机上,朋友给你发来一个链接,比方说一家外卖店面。而恰好外卖App应用也支持了
Instant
Apps。你点击了这个链接,就直接进入了外卖应用,即便手机并没有安装它。

实现原理大致是利用App linker唤起打开app的intent,Google
Play检测到支持该intent,而且没有安装后,直接通过类似Android插件化的原理,打开相关页面。

但是这个Instant App必须发布在Google Play上, 国内暂时没有办法使用。

淘宝Atlas

淘宝Atlas

Atlas是伴随着手机淘宝的不断发展而衍生出来的一个运行于Android系统上的一个容器化框架,我们也叫动态组件化(Dynamic
Bundle)框架。它主要提供了解耦化、组件化、动态性的支持。覆盖了工程师的工程编码期、Apk运行期以及后续运维期的各种问题。

在工程期,实现工程独立开发,调试的功能,工程模块可以独立。

在运行期,实现完整的组件生命周期的映射,类隔离等机制。

在运维期,提供快速增量的更新修复能力,快速升级。

Atlas是工程期和运行期共同起作用的框架,我们尽量将一些工作放到工程期,这样保证运行期更简单,更稳定。

相比multidex,atlas在解决了方法数限制的同时以OSGI为参考,明确了业务开发的边界,使得业务在满足并行迭代,快速开发的同时,能够进行灵活发布,动态更新以及提供了线上故障快速修复的能力。

与外界某些插件框架不同的是,atlas是一个组件框架,atlas不是一个多进程的框架,他主要完成的就是在运行环境中按需地去完成各个bundle的安装,加载类和资源。

滴滴VirtualAPK

VirtualAPK

VirtualAPK介绍

VirtualAPK是滴滴17年开源出来的一款插件化方案。

Small

Small

世界那么大,组件那么小。Small,做最轻巧的跨平台插件化框架。 ——Galenlin

这是Small作者,林光亮老师,给Small一句概括。

总结

本文只是简单的介绍下插件化相关内容,很多内容也是参照大神的博客的,感觉80%都是从别人那边复制过来的,同时插件不只是简单的加载界面和资源,包括BroadCastReceiver、Service等组件使用。

RePlugin使用方法还是蛮简单的,大部分情况下,插件的开发,相当于单独的一个App开发。

相对于其他厂商的方案,个人比较偏向于RePlugin,主要是因为开发简单,比较稳定,Hook点少,支持特性较多等。

相关资料

关于Android模块化我有一些话不知当讲不当讲

Android插件化原理解析——Hook机制之动态代理

APK文件结构和安装过程

Android进程间通信资料

Android系统应用框架篇:Activity启动流程

Android
插件化原理解析——Hook机制之AMS&PMS

深入分析Java
ClassLoader原理

热修复入门:Android中的ClassLoader

ANDROID应用程序插件化研究之ASSETMANAGER

DroidPlugin

DynamicAPK

AndroidDynamicLoader,利用动态加载Fragment来解决

dynamic-load-apk

android-pluginmgr

Small

DynamicAPK

淘宝Atlas

VirtualAPK

VirtualAPK介绍

Android Instant App
官网

发表评论

电子邮件地址不会被公开。 必填项已用*标注