Android插件化开发探索与思考

什么是组件化和插件化?

组件化和插件化

组件化开发就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。

插件化开发组件化开发略有不用,插件化开发是将整个app拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk分开或者联合打包。

前提技术介绍

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

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

APK构成

Manifest

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

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

Application

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

四大组件

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

so

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

resource

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

安装路径

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


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

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

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

App启动流程介绍

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

IPC & Binder

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

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

AMS

Activity启动流程说个一天也说不完,过程很长,也很繁琐,不过我们只要记住了AMS就可以了。
Android系统应用框架篇:Activity启动流程

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

代码加载

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

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,提示这是两个不同的类型。

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 。

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来加载的。

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 : 父类加载器,遵从双亲委托模型

资源获取

我们知道,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());

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();
    }
}

主流插件化方案

在 Android 中实现插件化框架,需要解决的问题主要如下:

  • 资源和代码的加载
  • Android 生命周期的管理和组件的注册
  • 宿主 APK 和插件 APK 资源引用的冲突解决

下面分析几个目前主流的开源框架,看看每个框架具体实现思路和优缺点。

DL 动态加载框架 ( 2014 年底)

是基于代理的方式实现插件框架,对 App 的表层做了处理,通过在 Manifest 中注册代理组件,当启动插件组件时,首先启动一个代理组件,然后通过这个代理组件来构建,启动插件组件。 需要按照一定的规则来开发插件 APK,插件中的组件需要实现经过改造后的 Activity、FragmentActivity、Service 等的子类。

优点如下:

  • 插件需要遵循一定的规则,因此安全方面可控制。
  • 方案简单,适用于自身少量代码的插件化改造。

缺点如下:

  • 不支持通过 This 调用组件的方法,需要通过 that 去调用。
  • 由于 APK 中的 Activity 没有注册,不支持隐式调用 APK 内部的 Activity。
  • 插件编写和改造过程中,需要考虑兼容性问题比较多,联调起来会比较费时费力。

DroidPlugin ( 2015 年 8 月)

DroidPlugin 是 360 手机助手实现的一种插件化框架,它可以直接运行第三方的独立 APK 文件,完全不需要对 APK 进行修改或安装。一种新的插件机制,一种免安装的运行机制,是一个沙箱(但是不完全的沙箱。就是对于使用者来说,并不知道他会把 apk 怎么样), 是模块化的基础。

实现原理:

  • 共享进程:为android提供一个进程运行多个 apk 的机制,通过 API 欺骗机制瞒过系统。
  • 占坑:通过预先占坑的方式实现不用在 manifest 注册,通过一带多的方式实现服务管理。
  • Hook 机制:动态代理实现函数 hook ,Binder 代理绕过部分系统服务限制,IO 重定向(先获取原始 Object –> Read ,然后动态代理 Hook Object 后–> Write 回去,达到瞒天过海的目的)。

插件 Host 的程序架构:

优点如下:

  • 支持 Android 四大组件,而且插件中的组件不需要在宿主 APK 中注册。
  • 支持 Android 2.3 及以上系统,支持所有的系统 API。
  • 插件与插件之间,插件与宿主之间的代码和资源完全隔阂。
  • 实现了进程管理,插件的空进程会被及时回收,占用内存低。

缺点如下:

  • 插件 APK 中不支持自定义资源的 Notification,通知栏限制。
  • 插件 APK 中无法注册具有特殊的 IntentFilter 的四大组件。
  • 缺乏对 Native 层的 Hook 操作,对于某些带有 Native 代码的插件 APK 支持不友好,可能无法正常运行。
  • 由于插件与插件,插件与宿主之间的代码完全隔离,因此,插件与插件,插件与宿主之间的通信只能通过 Android 系统级别的通信方式。
  • 安全性担忧(可以修改,hook一些重要信息)。
  • 机型适配(不是所有机器上都能行,因为大量用反射相关,如果rom厂商深度定制了framework层,反射的方法或者类不在,容易插件运用失败)

Small ( 2015 年底)

Small 是一种实现轻巧的跨平台插件化框架,基于“轻量、透明、极小化、跨平台”的理念,实现原理有以下三点。

  • 动态加载类:我们知道插件化很多都从 DexClassLoader 类有个 DexPathList 清单,支持 dex/jar/zip/apk 文件格式,却没有支持 .so 文件格式,因此 Small 框架则是把 .so 文件包装成 zip 文件格式,插入到 DexPathList 集合中,改写动态加载的代码。
  • 资源分段:由于 Android 资源的格式是 0xPPTTNNNN ,PP 是包 ID ,00-02 是属于系统,7f 属于应用程序,03-7e 则保留,可以在这个范围内做文章 , TT 则是 Type 比如,attr 、layout 、string 等等,NNNN 则是资源全局 ID。那么这个框架则是对资源包进行重新打包,每个插件重新分配资源 ID ,这样就保证了宿主和插件的资源不冲突。
  • 动态代理注册:在 Android 中要使用四大组件,都是需要在 manifest 清单中注册,这样才可以使用,那如何在不注册情况也能使用呢,这里就是用到动态代理机制进行 Hook ,在发送 AMS 之前用占坑的组件来欺骗系统,通过认证后,再把真正要调用的组件还原回来,达到瞒天过海目的。

架构图:

优点如下:

  • 所有插件支持内置宿主包中。
  • 插件的编码和资源文件的使用与普通开发应用没有差别。
  • 通过设定 URI ,宿主以及 Native 应用插件,Web 插件,在线网页等能够方便进行通信。
  • 支持 Android 、 iOS 、和 Html5 ,三者可以通过同一套 Javascript 接口实现通信。

缺点如下:

  • 暂不支持 Service 的动态注册,不过这个可以通过将 Service 预先注册在宿主的 AndroidManifest.xml 文件中进行规避,因为 Service 的更新频率通常非常低。

与其他主流框架的区别:

DyLA  : Dynamic-load-apk          @singwhatiwanna  
DiLA  : Direct-Load-apk           @FinalLody  
APF   : Android-Plugin-Framework  @limpoxe  
ACDD  : ACDD                      @bunnyblue  
DyAPK : DynamicAPK                @TediWang  
DPG   : DroidPlugin               @cmzy, 360

  • 功能
DyLA DiLA ACDD DyAPK DPG APF Small
加载非独立插件 × x ×
加载.so后缀插件 × × ! × × ×
Activity生命周期
Service动态注册 × × × x
资源分包共享 × × ! ! × !
公共插件打包共享 × × × × × ×
支持AppCompat × × × × × ×
支持本地网页组件 × × × × × ×
支持联调插件 × x × × × ×
  • 透明度
ACDD DyAPK APF Small
插件Activity代码无需修改
插件引用外部资源无需修改name × × ×
插件模块无需修改build.gradle × x ×

VirtualAPK (2017年 6 月 )

VirtualAPK 是滴滴开源的一套插件化框架,支持几乎所有的 Android 特性,四大组件方面。

架构图:

架构图

实现思路:

VirtualAPK 对插件没有额外的约束,原生的 apk 即可作为插件。插件工程编译生成 apk后,即可通过宿主 App 加载,每个插件 apk 被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如下图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的 App 一样运行。

  • 合并宿主和插件的ClassLoader 需要注意的是,插件中的类不可以和宿主重复
  • 合并插件和宿主的资源 重设插件资源的 packageId,将插件资源和宿主资源合并
  • 去除插件包对宿主的引用 构建时通过 Gradle 插件去除插件对宿主的代码以及资源的引用
原理图

特性如下:

四大组件均不需要在宿主manifest中预注册,每个组件都有完整的生命周期。

  1. Activity:支持显示和隐式调用,支持Activity的themeLaunchMode,支持透明主题;
  2. Service:支持显示和隐式调用,支持Service的startstopbindunbind,并支持跨进程bind插件中的Service;
  3. Receiver:支持静态注册和动态注册的Receiver;
  4. ContentProvider:支持provider的所有操作,包括CRUDcall方法等,支持跨进程访问插件中的Provider。
  • 自定义View:支持自定义 View,支持自定义属性和style,支持动画;
  • PendingIntent:支持PendingIntent以及和其相关的AlarmNotificationAppWidget
  • 支持插件Application以及插件manifest中的meta-data
  • 支持插件中的so

优秀的兼容性

  • 兼容市面上几乎所有的Android手机,这一点已经在滴滴出行客户端中得到验证。
  • 资源方面适配小米、Vivo、Nubia 等,对未知机型采用自适应适配方案。
  • 极少的 Binder Hook,目前仅仅 hook了两个Binder:AMSIContentProvider,hook 过程做了充分的兼容性适配。
  • 插件运行逻辑和宿主隔离,确保框架的任何问题都不会影响宿主的正常运行。

入侵性极低

  • 插件开发等同于原生开发,四大组件无需继承特定的基类;
  • 精简的插件包,插件可以依赖宿主中的代码和资源,也可以不依赖;
  • 插件的构建过程简单,通过 Gradle 插件来完成插件的构建,整个过程对开发者透明。

如下是 VirtualAPK 和主流的插件化框架之间的对比。

特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
支持四大组件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持
组件无需在宿主manifest中预注册 ×
插件可以依赖宿主 ×
支持 PendingIntent × × ×
Android 特性支持 大部分 大部分 大部分 几乎全部 几乎全部
兼容性适配 一般 一般 中等
插件构建 部署aapt Gradle插件 Gradle插件

RePlugin (2017 年 7 月)

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

框架图:

RePlugin

主要优势有:
极其灵活:主程序无需升级(无需在Manifest中预埋组件),即可支持新增的四大组件,甚至全新的插件
非常稳定:Hook 点仅有一处(ClassLoader),无任何 Binder Hook!如此可做到其崩溃率仅为“万分之一”,并完美兼容市面上近乎所有的 Android ROM。
特性丰富:支持近乎所有在“单品”开发时的特性。包括静态 Receiver、 Task-Affinity 坑位、自定义 Theme、进程坑位、AppCompat、DataBinding等。
易于集成:无论插件还是主程序,只需“数行”就能完成接入。
管理成熟:拥有成熟稳定的“插件管理方案”,支持插件安装、升级、卸载、版本管理,甚至包括进程通讯、协议版本、安全校验等。
数亿支撑:有 360 手机卫士庞大的数亿用户做支撑,三年多的残酷验证,确保App用到的方案是最稳定、最适合使用的。

360RePlugin的使用

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

集成也非常简单,比如有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中包含哪些资源。

image.png

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

image.png

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

image.png

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可以看到修改后的结果。

image.png

源码里面也有体现

image.png

Android全面插件化RePlugin流程与源码解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容