在 Android 中实现插件化框架,需要解决的问题主要如下:
资源和代码的加载
Android 生命周期的管理和组件的注册
宿主 APK 和插件 APK 资源引用的冲突解决
是基于代理的方式实现插件框架; 对 App 的表层做了处理,通过在 Manifest 中注册代理组件,当启动插件组件时,首先启动一个代理组件,然后通过这个代理组件来构建,启动插件组件;需要按照一定的规则来开发插件 APK,插件中的组件需要实现经过改造后的 Activity、FragmentActivity、Service 等的子类
插件需要遵循一定的规则,因此安全方面可控制
方案简单,适用于自身少量代码的插件化改造
不支持通过 This 调用组件的方法,需要通过 that 去调用
由于 APK 中的 Activity 没有注册,不支持隐式调用 APK 内部的 Activity
插件编写和改造过程中,需要考虑兼容性问题比较多,联调起来会比较费时费力
DroidPlugin 是 360 手机助手实现的一种插件化框架; 它可以直接运行第三方的独立 APK 文件,完全不需要对 APK 进行修改或安装;一种新的插件机制,一种免安装的运行机制,是一个沙箱(但是不完全的沙箱;就是对于使用者来说,并不知道他会把 apk 怎么样), 是模块化的基础
共享进程: 为android提供一个进程运行多个 apk 的机制,通过 API 欺骗机制瞒过系统
占坑: 通过预先占坑的方式实现不用在 manifest 注册,通过一带多的方式实现服务管理
Hook 机制:
动态代理实现函数 hook ,Binder 代理绕过部分系统服务限制,IO 重定向(先获取原始 Object –> Read ,然后动态代理 Hook Object 后–> Write 回去,达到瞒天过海的目的)
支持 Android 四大组件,而且插件中的组件不需要在宿主 APK 中注册
支持 Android 2.3 及以上系统,支持所有的系统 API
插件与插件之间,插件与宿主之间的代码和资源完全隔阂
实现了进程管理,插件的空进程会被及时回收,占用内存低
插件 APK 中不支持自定义资源的 Notification,通知栏限制
插件 APK 中无法注册具有特殊的 IntentFilter 的四大组件
缺乏对 Native 层的 Hook 操作,对于某些带有 Native 代码的插件 APK 支持不友好,可能无法正常运行
由于插件与插件,插件与宿主之间的代码完全隔离,因此,插件与插件,插件与宿主之间的通信只能通过 Android 系统级别的通信方式
安全性担忧(可以修改,hook一些重要信息)
机型适配(不是所有机器上都能行,因为大量用反射相关,如果rom厂商深度定制了framework层,反射的方法或者类不在,容易插件运用失败)
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
功能
透明度
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 插件来完成插件的构建,整个过程对开发者透明
speed-tools 是一款基于代理模式的动态部署apk热更新框架、插件化开发框架;
speed-tools这个名字主要指的快速迭×××发工具集的意思
1、支持Android 2.3 以上版本
2、支持R文件资源直接调用
3、开发过程中无发射调用
4、apk无需安装直接调用
5、代理模式对代码侵入性少
6、使用简单,只需要继承简单的类即可
添加依赖:
compile 'com.liyihangjson:speed_tools:1.0.3'
首先看看项目结构:
lib_speed_tools: 插件化核心功能library module_host_main:宿主工程主工程,负责加载部署apk module_client_one:测试业务apk 1 module_client_two:测试业务apk 2 lib_img_utils:测试imageloader图片框架
注意:需要使用speed tools 只需要依赖lib_speed_tools即可,然后开始配置插件化步骤:
首先在module_client_one中创建业务逻辑类:TestClass.java
/**
* by liyihang
*/
public class TestClass extends SpeedBaseInterfaceImp {
private Activity activity;
@Override
public void onCreate(Bundle savedInstanceState, final Activity activity) {
this.activity=activity;
activity.setContentView(R.layout.activity_client_main);
activity.findViewById(R.id.jump).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SpeedUtils.goActivity(activity,"first_apk", "two_class");
}
});
ImageView imageView= (ImageView) activity.findViewById(R.id.img_view);
imageView.setVisibility(View.VISIBLE);
ImgUtils.getInstance(activity).showImg("http://img.my.csdn.net/uploads/201309/01/1378037235_3453.jpg", imageView);
}
}SpeedBaseInterfaceImp业务组件中业务activity代理类,他是实现了主要的生命周期方法,相当于组件的activity类
然后创建hock类每个业务组件中只创建一个:ClientMainActivity.java
public class ClientMainActivity extends SpeedClientBaseActivity {
@Override
public SpeedBaseInterface getProxyBase() {
return new TestClass();
}
}这个类在组件中是唯一的,作用就是hock在独立测试时候使用
接下来配置配置组件的AndroidManifest.xml
组件意图写死保持一直,root_class 是调用死后使用对于配置的com.example.clientdome.TestClass业务类,这样业务组件配置完成
接下来配置宿主工程module_host_main;
创建宿主工程唯一hock类:ApkActivity.java
/**
* by liyihang
* blog http://sijienet.com/
*/
public class ApkActivity extends SpeedHostBaseActivity {
@Override
public String getApkKeyName() {
return HostMainActivity.FIRST_APK_KEY;
}
@Override
public String getClassTag() {
return null;
}
}整个宿主工程创建一个类即可,用户是hock activity;然后创建一个开屏页apk第一次加载时候需要一些时间,放入开屏等待页面是非常合适的
HostMainActivity.java
/**
* by liyihang
* blog http://sijienet.com/
*/
public class HostMainActivity extends AppCompatActivity implements Runnable,Handler.Callback, View.OnClickListener {
public static final String FIRST_APK_KEY="first_apk";
public static final String TWO_APK_KEY="other_apk";
private Handler handler;
private TextView showFont;
private ProgressBar progressBar;
private Button openOneApk;
private Button openTwoApk;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_host_main);
showFont= (TextView) findViewById(R.id.show_font);
progressBar= (ProgressBar) findViewById(R.id.progressbar);
openOneApk= (Button) findViewById(R.id.open_one_apk);
openTwoApk= (Button) findViewById(R.id.open_two_apk);
handler=new Handler(this);
new Thread(this).start();
}
@Override
public void run() {
String s = "module_client_one-release.apk";
String dexOutPath="dex_output2";
File nativeApkPath = SpeedUtils.getNativeApkPath(getApplicationContext(), s);
SpeedApkManager.getInstance().loadApk(FIRST_APK_KEY, nativeApkPath.getAbsolutePath(), dexOutPath, this);
String s2 = "module_client_two-release.apk";
String dexOutPath3="dex_output3";
File nativeApkPath2 = SpeedUtils.getNativeApkPath(getApplicationContext(), s2);
SpeedApkManager.getInstance().loadApk(TWO_APK_KEY, nativeApkPath2.getAbsolutePath(), dexOutPath3, this);
handler.sendEmptyMessage(0x78);
}
@Override
public boolean handleMessage(Message message) {
showFont.setText("当前是主宿主apk
插件apk完毕");
progressBar.setVisibility(View.GONE);
openOneApk.setVisibility(View.VISIBLE);
openTwoApk.setVisibility(View.VISIBLE);
openOneApk.setOnClickListener(this);
openTwoApk.setOnClickListener(this);
return false;
}
@Override
public void onClick(View v) {
if (v.getId()==R.id.open_one_apk)
{
SpeedUtils.goActivity(this, FIRST_APK_KEY, null);
}
if (v.getId()==R.id.open_two_apk)
{
SpeedUtils.goActivity(this, TWO_APK_KEY, null);
}
}
}加载apk核心代码是:
String s = "module_client_one-release.apk";
String dexOutPath="dex_output2";
File nativeApkPath = SpeedUtils.getNativeApkPath(getApplicationContext(), s);
SpeedApkManager.getInstance().loadApk(FIRST_APK_KEY, nativeApkPath.getAbsolutePath(), dexOutPath, this);
业务apk都是放在assets目录中
最后配置AndroidManifest.xml文件:
<?xml version="1.0" encoding="utf-8"?>
这样所有配置结束,插件化实现
有需要完整代码的同学 私信 发送 “底层源码” 即可 免费获取
现在点击还可以获得更多《Android 学习笔记+源码解析+面试视频》
正如开头所说,要实现插件化的框架,无非就是解决那典型的三个问题:插件代码如何加载、插件中的组件生命周期如何管理、插件资源和宿主资源冲突怎么办;
每个框架针对这三个问题,都有不同的解决方案,同时呢,根据时间顺序,后出来的框架往往都会吸收已经出的框架精髓,进而修复那些比较有里程碑意义框架的不足;
但这些框架的核心思想都是用到了代理模式,有的在表面层进行代理,有的则在系统应用层进行代理,通过代理达到替换和瞒天过海,最终让 Android 系统误以为调用插件功能和调用原生开发的功能是一样的,进而达到插件化和原生兼容编程的目的
有需要完整代码的同学 私信 发送 “底层源码” 即可 免费获取
现在点击还可以获得更多《Android 学习笔记+源码解析+面试视频》
技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面
Android 架构师之路还很漫长,与君共勉
PS:有问题欢迎指正,可以在评论区留下你的建议和感受;
欢迎大家点赞评论,觉得内容可以的话,可以转发分享一下
| 留言与评论(共有 0 条评论) “” |