基于Proxy思想的Android插件框架
本文所有代码托管在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加载的主要是PathClassloader
和Dexclassloader
。
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中包含Activity
,Service
,Content 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会转为启动主程序中的一个ProxyActivity
,ProxyActivity
中所有系统回调都会调用插件Activity中对应的实现,最后的效果就是启动的这个Activity实际上是主程序中已经声明的一个Activity,但是相关代码执行的却是插件Activity中的代码。这就解决了插件Activity未声明情况下无法启动的问题,从上层来看启动的就是插件中的Activity。下面具体分析整个过程。
PluginSDK
所有的插件和主程序需要依赖PluginSDK进行开发,所有插件中的Activity继承自PluginSDK中的PluginBaseActivity
,PluginBaseActivity
继承自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);}}
PluginBaseActivity
和ProxyActivity
在整个插件框架的核心,下面简单分析一下代码:
首先看一下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 Provider
和BroadcastReceiver
组件的Proxy类未实现,Activity的Proxy实现也是不完整的,包括不少回调都没有处理。同时我也无法保证这套框架没有致命缺陷,本文主要是以总结、学习和交流为目的,欢迎大家一起交流。
原文地址: http://zjmdp.github.io/2014/07/22/a-plugin-framework-for-android/
基于Proxy思想的Android插件框架相关推荐
- 腾讯零反射全动态Android插件框架Shadow解析
简介 最近几年,腾讯对于开源事业也是越来越支持,今天要说的就是在腾讯被广泛使用的Shadow框架,一个经过线上亿级用户量检验的反射全动态Android插件框架. 首先,让我们来看一下官方对于Shado ...
- Android 插件框架机制之Small
Android 插件框架机制系列文章: Android 插件框架机制之预热篇 Android 插件框架机制之DroidPlugin 引言 上一篇文章提到过Small,这次就简单说一下Small,这只是 ...
- Android加密通信防抓包,[原创]基于Taintdroid思想的android ssl\tsl保密通信抓包研究(未成功,分享一下思路)...
[旧帖] [原创]基于Taintdroid思想的android ssl\tsl保密通信抓包研究(未成功,分享一下思路) 0.00元 2014-5-12 22:07 1565 [旧帖] [原创]基于Ta ...
- android 日志框架封装,FLog: 一个基于函数组合的Android日志框架,拥有极简的结构和极高的灵活性、扩展性...
FLog 一个基于函数组合的Android日志框架,拥有极简的结构和极高的灵活性.扩展性 下载 在根目录下的build.gradle中添加jitpack.io的maven地址 allprojects ...
- android插件框架机制的选择,Android插件开发初探——基础篇
Android插件开发初探 对于Android的插件化其实已经讨论已久了,但是市面上还没有非常靠谱成熟的插件框架供我们使用.这里我们就尝试性的对比一下Java中,我们使用插件化该是一个怎么样的流程,且 ...
- Android插件框架VirtualAPK
VirtualAPK是滴滴出行自研的一款优秀的插件化框架,主要有如下几个特性. 功能完备 支持几乎所有的Android特性: 四大组件方面 四大组件均不需要在宿主manifest中预注册,每个组件都有 ...
- 滴滴开源Android插件框架
登录 | 注册 收藏成功 确定 收藏失败,请重新收藏 确定 查看所有私信查看所有通知 暂没有新通知 返回通知列表 下一条 上一条 分享资讯传PPT/文档提问题写博客传资源创建项目创建代码片 wz ...
- Android 插件框架实现思路及原理
插件框架实现思路及原理 一.技术可行性 a) apk的安装处理流程 i. apk会copy到/data/app: ii. 解压apk中的class.dex,并对其进行优化,获得odex(即JIT).最 ...
- 基于QProbe创建基本Android图像处理框架
先来看一个GIF 这个GIF中有以下几个值得注意的地方 这个界面是基本的主要界面所应该在的地方.其右下角有一个"+"号,点击后,打开图像采集界面 在这个界面最上面的地方,显示的是当 ...
最新文章
- 修改android studio中的avd sdk路径、avd sdk找不到的解决方案
- Android中ActivityManager学习笔记
- Python学习 - 之 数据封装和私有属性
- Gitlab代码托管服务器安装
- 2014年第五届蓝桥杯C/C++ A组国赛 —— 第三题:日期差
- 小程序 wx.getBackgroundAudioManager() 手机黑屏后,让控制音频停止
- JS实例操作QQ空间自动点赞方法
- Java中如何实现代理机制(JDK、CGLIB)
- 飞鸽传书2011绿色版简单性
- 【高校宿舍管理系统】第一章 建立数据库以及项目框架搭建
- java对象和字符串转换_java中字符串和JSON对象、Bean之间的相互转换
- python限制输入长度_textFiled限制输入长度.
- 第一章 架构 1.4 编译 amp; 1.5总结
- 清晨晓叙:matlab中的矩阵拼接
- Oracle 数据库用户表大小分析
- 对话系统 | (8) 任务型对话系统概述
- SSH工具连接谷歌云VPS实例
- 加权最小二乘法matlab,加权最小二乘法matlab
- Covetrus宣布首席执行官和董事长过渡
- 中文数字阿拉伯数字相互转换(Java版本)
热门文章
- C/C++/动态链接库DLL中函数的调用约定与名称修饰
- (转)工作了一个星期各位一定累了吧,那我们一起来表单验证一番吧!
- C++ unique and erase问题处理
- jQuery UI Download
- 加载NMGameX.dll时出错?
- tensorboard出现OSError: [Errno 22] Invalid argument问题解决
- 【Python】随机划分数据集并生成VOC格式列表
- 在表格中批量显示图片
- 云炬创业政策学习笔记20210111
- [MATLAB学习笔记]textread读取文本文件中的数据;写入多个输出