本文所有代码托管在Github:android-plugin

意义

研究插件框架的意义在于以下几点:

  • 减小安装包的体积,通过网络选择性地进行插件下发
  • 模块化升级,减小网络流量
  • 静默升级,用户无感知情况下进行升级
  • 解决低版本机型方法数超限导致无法安装的问题
  • 代码解耦

现状

Android中关于插件框架的技术已经有过不少讨论和实现,插件通常打包成apk或者dex的形式。

dex形式的插件往往提供了一些功能性的接口,这种方式类似于java中的jar形式,只是由于Android的Dalvik VM无法直接动态加载Java的Byte Code,所以需要我们提供Dalvik Byte Code,而dex就是Dalvik Byte Code形式的jar。

apk形式的插件提供了比dex形式更多的功能,例如可以将资源打包进apk,也可实现插件内的Activity或者Service等系统组件。

本文主要讨论apk形式的插件框架,对于apk形式又存在安装和不安装两种方式

  • 安装apk的方式实现相对简单,主要原理是通过将插件apk和主程序共享一个UserId,主程序通过createPackageContext构造插件的context,通过context即可访问插件apk中的资源,很多app的主题框架就是通过安装插件apk的形式实现,例如Go主题。这种方式的缺点就是需要用户手动安装,体验并不是很好。

  • 不安装apk的方式解决了用户手动安装的缺点,但实现起来比较复杂,主要通过DexClassloader的方式实现,同时要解决如何启动插件中Activity等Android系统组件,为了保证插件框架的灵活性,这些系统组件不太好在主程序中提前声明,实现插件框架真正的难点在此。

DexClassloader

这里引用《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版里对java类加载器的一段描述:

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

Android虚拟机的实现参考了java的JVM,因此在Android中加载类也用到了类加载器的概念,只是相对于JVM中加载器加载class文件而言,Android的Dalvik虚拟机加载的是Dex格式,而具体完成Dex加载的主要是PathClassloaderDexclassloader

PathClassloader默认会读取/data/dalvik-cache中缓存的dex文件,未安装的apk如果用PathClassloader来加载,那么在/data/dalvik-cache目录下找不到对应的dex,因此会抛出ClassNotFoundException

DexClassloader可以加载任意路径下包含dex和apk文件,通过指定odex生成的路径,可加载未安装的apk文件。下面一段代码展示了DexClassloader的使用方法:

final File optimizedDexOutputPath = context.getDir("odex", Context.MODE_PRIVATE);
try{DexClassLoader classloader = new DexClassLoader("apkPath",optimizedDexOutputPath.getAbsolutePath(),null, context.getClassLoader());Class<?> clazz = classloader.loadClass("com.plugindemo.test");Object obj = clazz.newInstance();Class[] param = new Class[2];param[0] = Integer.TYPE;param[1] = Integer.TYPE;Method method = clazz.getMethod("add", param);method.invoke(obj, 1, 2);
}catch(InvocationTargetException e){e.printStackTrace();
}catch(NoSuchMethodException e){e.printStackTrace();
}catch(IllegalAccessException e){e.printStackTrace();
}catch(ClassNotFoundException e){e.printStackTrace();
}catch (InstantiationException e){e.printStackTrace();
}

DexClassloader解决了类的加载问题,如果插件apk里只是一些简单的API调用,那么上面的代码已经能满足需求,不过这里讨论的插件框架还需要解决资源访问和Android系统组件的调用。

插件内系统组件的调用

Android Framework中包含ActivityServiceContent Provider以及BroadcastReceiver等四大系统组件,这里主要讨论如何在主程序中启动插件中的Activity,其它3种组件的调用方式类似。

大家都知道Activity需要在AndroidManifest.xml中进行声明,apk在安装的时候PackageManagerService会解析apk中的AndroidManifest.xml文件,这时候就决定了程序包含的哪些Activity,启动未声明的Activity会报ActivityNotFound异常,相信大部分Android开发者曾经都遇到过这个异常。

启动插件里的Activity必然会面对如何在主程序中的AndroidManifest.xml中声明这个Activity,然而为了保证插件框架的灵活性,我们是无法预知插件中有哪些Activity,所以也无法提前声明。

为了解决上述问题,这里介绍一种基于Proxy思想的解决方法,大致原理是在主程序的AndroidManifest.xml中声明一些ProxyActivity,启动插件中的Activity会转为启动主程序中的一个ProxyActivityProxyActivity中所有系统回调都会调用插件Activity中对应的实现,最后的效果就是启动的这个Activity实际上是主程序中已经声明的一个Activity,但是相关代码执行的却是插件Activity中的代码。这就解决了插件Activity未声明情况下无法启动的问题,从上层来看启动的就是插件中的Activity。下面具体分析整个过程。

PluginSDK

所有的插件和主程序需要依赖PluginSDK进行开发,所有插件中的Activity继承自PluginSDK中的PluginBaseActivityPluginBaseActivity继承自Activity并实现了IActivity接口。

public interface IActivity {public void IOnCreate(Bundle savedInstanceState);public void IOnResume();public void IOnStart();public void IOnPause();public void IOnStop();public void IOnDestroy();public void IOnRestart();public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo);
}

public class PluginBaseActivity extends Activity implements IActivity {...private Activity mProxyActivity;...@Overridepublic void IInit(String path, Activity context, ClassLoader classLoader) {mProxy = true;mProxyActivity = context;mPluginContext = new PluginContext(context, 0, path, classLoader);attachBaseContext(mPluginContext);}@Overrideprotected void onCreate(Bundle savedInstanceState) {if (mProxy) {mRealActivity = mProxyActivity;} else {super.onCreate(savedInstanceState);mRealActivity = this;}}@Overridepublic void setContentView(int layoutResID) {if (mProxy) {mContentView = LayoutInflater.from(mPluginContext).inflate(layoutResID, null);mRealActivity.setContentView(mContentView);} else {super.setContentView(layoutResID);}}...@Overridepublic void IOnCreate(Bundle savedInstanceState) {onCreate(savedInstanceState);}@Overridepublic void IOnResume() {onResume();}@Overridepublic void IOnStart() {onStart();}@Overridepublic void IOnPause() {onPause();}@Overridepublic void IOnStop() {onStop();}@Overridepublic void IOnDestroy() {onDestroy();}@Overridepublic void IOnRestart() {onRestart();}
}

public class ProxyActivity extends Activity {IActivity mPluginActivity;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);Bundle bundle = getIntent().getExtras();if(bundle == null){return;}mPluginName = bundle.getString(PluginConstants.PLUGIN_NAME);mLaunchActivity = bundle.getString(PluginConstants.LAUNCH_ACTIVITY);File pluginFile = PluginUtils.getInstallPath(ProxyActivity.this, mPluginName);if(!pluginFile.exists()){return;}mPluginApkFilePath = pluginFile.getAbsolutePath();try {initPlugin();super.onCreate(savedInstanceState);mPluginActivity.IOnCreate(savedInstanceState);} catch (Exception e) {mPluginActivity = null;e.printStackTrace();}}@Overrideprotected void onResume() {super.onResume();if(mPluginActivity != null){mPluginActivity.IOnResume();}}@Overrideprotected void onStart() {super.onStart();if(mPluginActivity != null) {mPluginActivity.IOnStart();}}...private void initPlugin() throws Exception {PackageInfo packageInfo = PluginUtils.getPackgeInfo(this, mPluginApkFilePath);if (mLaunchActivity == null || mLaunchActivity.length() == 0) {mLaunchActivity = packageInfo.activities[0].name;}ClassLoader classLoader = PluginUtils.getClassLoader(this, mPluginName, mPluginApkFilePath);if (mLaunchActivity == null || mLaunchActivity.length() == 0) {if (packageInfo == null || (packageInfo.activities == null) || (packageInfo.activities.length == 0)) {throw new ClassNotFoundException("Launch Activity not found");}mLaunchActivity = packageInfo.activities[0].name;}Class<?> mClassLaunchActivity = classLoader.loadClass(mLaunchActivity);getIntent().setExtrasClassLoader(classLoader);mPluginActivity = (IActivity) mClassLaunchActivity.newInstance();mPluginActivity.IInit(mPluginApkFilePath, this, classLoader);}...@Overridepublic void startActivityForResult(Intent intent, int requestCode) {boolean pluginActivity = intent.getBooleanExtra(PluginConstants.IS_IN_PLUGIN, false);if (pluginActivity) {String launchActivity = null;ComponentName componentName = intent.getComponent();if(null != componentName) {launchActivity = componentName.getClassName();}intent.putExtra(PluginConstants.IS_IN_PLUGIN, false);if (launchActivity != null && launchActivity.length() > 0) {Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));pluginIntent.putExtra(PluginConstants.PLUGIN_NAME, mPluginName);pluginIntent.putExtra(PluginConstants.PLUGIN_PATH, mPluginApkFilePath);pluginIntent.putExtra(PluginConstants.LAUNCH_ACTIVITY, launchActivity);startActivityForResult(pluginIntent, requestCode);}} else {super.startActivityForResult(intent, requestCode);}}

PluginBaseActivityProxyActivity在整个插件框架的核心,下面简单分析一下代码:

首先看一下ProxyActivity#onResume

@Override
protected void onResume() {super.onResume();if(mPluginActivity != null){mPluginActivity.IOnResume();}
}

变量mPluginActivity的类型是IActivity,由于插件Activity实现了IActivity接口,因此可以猜测mPluginActivity.IOnResume()最终执行的是插件Activity的onResume中的代码,下面我们来证实这种猜测。

PluginBaseActivity实现了IActivity接口,那么这些接口具体是怎么实现的呢?看代码:

@Override
public void IOnCreate(Bundle savedInstanceState) {onCreate(savedInstanceState);
}@Override
public void IOnResume() {onResume();
}@Override
public void IOnStart() {onStart();
}@Override
public void IOnPause() {onPause();
}...

接口实现非常简单,只是调用了和接口对应的回调函数,那这里的回调函数最终会调到哪里呢?前面提到过所有插件Activity都会继承自PluginBaseActivity,也就是说这里的回调函数最终会调到插件Activity中对应的回调,比如IOnResume执行的是插件Activity中的onResume中的代码,这也证实了之前的猜测。

上面的一些代码片段揭示了插件框架的核心逻辑,其它的代码更多的是为实现这种逻辑服务的,后面会提供整个工程的源码,大家可自行分析理解。

插件内资源获取

实现加载插件apk中的资源的一种思路是将插件apk的路径加入主程序资源查找的路径中,下面的代码展示了这种方法:

private AssetManager getSelfAssets(String apkPath) {AssetManager instance = null;try {instance = AssetManager.class.newInstance();Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);addAssetPathMethod.invoke(instance, apkPath);} catch (Throwable e) {e.printStackTrace();}return instance;
}

为了让插件Activity访问资源时使用我们自定义的Context,我们需要在PluginBaseActivity的初始化中做一些处理:

public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {mProxy = true;mProxyActivity = context;mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);attachBaseContext(mContext);
}

PluginContext中通过重载getAssets来实现包含插件apk查找路径的Context:

public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) {super(base, themeres);mClassLoader = classLoader;mAsset = getPluginAssets(pluginFilePath);mResources = getPluginResources(base, mAsset);mTheme = getPluginTheme(mResources);
}private AssetManager getPluginAssets(String apkPath) {AssetManager instance = null;try {instance = AssetManager.class.newInstance();Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);addAssetPathMethod.invoke(instance, apkPath);} catch (Throwable e) {e.printStackTrace();}return instance;
}private Resources getPluginAssets(Context ctx, AssetManager selfAsset)  {DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();Configuration con = ctx.getResources().getConfiguration();return new Resources(selfAsset, metrics, con);
}private Theme getPluginTheme(Resources selfResources) {Theme theme = selfResources.newTheme();mThemeResId = getInnerRIdValue("com.android.internal.R.style.Theme");theme.applyStyle(mThemeResId, true);return theme;
}@Override
public Resources getResources() {return mResources;
}@Override
public AssetManager getAssets() {return mAsset;
}...

总结

本文介绍了一种基于Proxy思想的插件框架,所有的代码都在Github中,代码只是抽取了整个框架的核心部分,如果要用在生产环境中还需要完善,比如Content ProviderBroadcastReceiver组件的Proxy类未实现,Activity的Proxy实现也是不完整的,包括不少回调都没有处理。同时我也无法保证这套框架没有致命缺陷,本文主要是以总结、学习和交流为目的,欢迎大家一起交流。

原文地址: http://zjmdp.github.io/2014/07/22/a-plugin-framework-for-android/

基于Proxy思想的Android插件框架相关推荐

  1. 腾讯零反射全动态Android插件框架Shadow解析

    简介 最近几年,腾讯对于开源事业也是越来越支持,今天要说的就是在腾讯被广泛使用的Shadow框架,一个经过线上亿级用户量检验的反射全动态Android插件框架. 首先,让我们来看一下官方对于Shado ...

  2. Android 插件框架机制之Small

    Android 插件框架机制系列文章: Android 插件框架机制之预热篇 Android 插件框架机制之DroidPlugin 引言 上一篇文章提到过Small,这次就简单说一下Small,这只是 ...

  3. Android加密通信防抓包,[原创]基于Taintdroid思想的android ssl\tsl保密通信抓包研究(未成功,分享一下思路)...

    [旧帖] [原创]基于Taintdroid思想的android ssl\tsl保密通信抓包研究(未成功,分享一下思路) 0.00元 2014-5-12 22:07 1565 [旧帖] [原创]基于Ta ...

  4. android 日志框架封装,FLog: 一个基于函数组合的Android日志框架,拥有极简的结构和极高的灵活性、扩展性...

    FLog 一个基于函数组合的Android日志框架,拥有极简的结构和极高的灵活性.扩展性 下载 在根目录下的build.gradle中添加jitpack.io的maven地址 allprojects ...

  5. android插件框架机制的选择,Android插件开发初探——基础篇

    Android插件开发初探 对于Android的插件化其实已经讨论已久了,但是市面上还没有非常靠谱成熟的插件框架供我们使用.这里我们就尝试性的对比一下Java中,我们使用插件化该是一个怎么样的流程,且 ...

  6. Android插件框架VirtualAPK

    VirtualAPK是滴滴出行自研的一款优秀的插件化框架,主要有如下几个特性. 功能完备 支持几乎所有的Android特性: 四大组件方面 四大组件均不需要在宿主manifest中预注册,每个组件都有 ...

  7. 滴滴开源Android插件框架

     登录 | 注册 收藏成功 确定 收藏失败,请重新收藏 确定 查看所有私信查看所有通知 暂没有新通知 返回通知列表 下一条 上一条 分享资讯传PPT/文档提问题写博客传资源创建项目创建代码片 wz ...

  8. Android 插件框架实现思路及原理

    插件框架实现思路及原理 一.技术可行性 a) apk的安装处理流程 i. apk会copy到/data/app: ii. 解压apk中的class.dex,并对其进行优化,获得odex(即JIT).最 ...

  9. 基于QProbe创建基本Android图像处理框架

    先来看一个GIF 这个GIF中有以下几个值得注意的地方 这个界面是基本的主要界面所应该在的地方.其右下角有一个"+"号,点击后,打开图像采集界面 在这个界面最上面的地方,显示的是当 ...

最新文章

  1. 修改android studio中的avd sdk路径、avd sdk找不到的解决方案
  2. Android中ActivityManager学习笔记
  3. Python学习 - 之 数据封装和私有属性
  4. Gitlab代码托管服务器安装
  5. 2014年第五届蓝桥杯C/C++ A组国赛 —— 第三题:日期差
  6. 小程序 wx.getBackgroundAudioManager() 手机黑屏后,让控制音频停止
  7. JS实例操作QQ空间自动点赞方法
  8. Java中如何实现代理机制(JDK、CGLIB)
  9. 飞鸽传书2011绿色版简单性
  10. 【高校宿舍管理系统】第一章 建立数据库以及项目框架搭建
  11. java对象和字符串转换_java中字符串和JSON对象、Bean之间的相互转换
  12. python限制输入长度_textFiled限制输入长度.
  13. 第一章 架构 1.4 编译 amp; 1.5总结
  14. 清晨晓叙:matlab中的矩阵拼接
  15. Oracle 数据库用户表大小分析
  16. 对话系统 | (8) 任务型对话系统概述
  17. SSH工具连接谷歌云VPS实例
  18. 加权最小二乘法matlab,加权最小二乘法matlab
  19. Covetrus宣布首席执行官和董事长过渡
  20. 中文数字阿拉伯数字相互转换(Java版本)

热门文章

  1. C/C++/动态链接库DLL中函数的调用约定与名称修饰
  2. (转)工作了一个星期各位一定累了吧,那我们一起来表单验证一番吧!
  3. C++ unique and erase问题处理
  4. jQuery UI Download
  5. 加载NMGameX.dll时出错?
  6. tensorboard出现OSError: [Errno 22] Invalid argument问题解决
  7. 【Python】随机划分数据集并生成VOC格式列表
  8. 在表格中批量显示图片
  9. 云炬创业政策学习笔记20210111
  10. [MATLAB学习笔记]textread读取文本文件中的数据;写入多个输出