什么是组件化和插件化?
组件化开发就是将一个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中预注册,每个组件都有完整的生命周期。
- Activity:支持显示和隐式调用,支持Activity的
theme
和LaunchMode
,支持透明主题; - Service:支持显示和隐式调用,支持Service的
start
、stop
、bind
和unbind
,并支持跨进程bind插件中的Service; - Receiver:支持静态注册和动态注册的Receiver;
- ContentProvider:支持provider的所有操作,包括
CRUD
和call
方法等,支持跨进程访问插件中的Provider。
- 自定义View:支持
自定义 View
,支持自定义属性和style
,支持动画; - PendingIntent:支持
PendingIntent
以及和其相关的Alarm
、Notification
和AppWidget
; - 支持插件
Application
以及插件manifest中的meta-data
; - 支持插件中的
so
。
优秀的兼容性
- 兼容市面上几乎所有的Android手机,这一点已经在滴滴出行客户端中得到验证。
- 资源方面适配小米、Vivo、Nubia 等,对未知机型采用自适应适配方案。
- 极少的 Binder Hook,目前仅仅 hook了两个Binder:
AMS
和IContentProvider
,hook 过程做了充分的兼容性适配。 - 插件运行逻辑和宿主隔离,确保框架的任何问题都不会影响宿主的正常运行。
入侵性极低
- 插件开发等同于原生开发,四大组件无需继承特定的基类;
- 精简的插件包,插件可以依赖宿主中的代码和资源,也可以不依赖;
- 插件的构建过程简单,通过 Gradle 插件来完成插件的构建,整个过程对开发者透明。
如下是 VirtualAPK 和主流的插件化框架之间的对比。
特性 | DynamicLoadApk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
组件无需在宿主manifest中预注册 | √ | × | √ | √ | √ |
插件可以依赖宿主 | √ | √ | √ | × | √ |
支持 PendingIntent | × | × | × | √ | √ |
Android 特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 |
兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 |
插件构建 | 无 | 部署aapt | Gradle插件 | 无 | Gradle插件 |
RePlugin (2017 年 7 月)
RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。
框架图:
主要优势有:
极其灵活:主程序无需升级(无需在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中包含哪些资源。
插件自动生成了plugin-builtin.json文件
同时也在Manifest中插入很多坑位。
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可以看到修改后的结果。
源码里面也有体现