意义

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

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

现状

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;
... @Override
public void IInit(String path, Activity context, ClassLoader classLoader) {
mProxy = true;
mProxyActivity = context; mPluginContext = new PluginContext(context, 0, path, classLoader);
attachBaseContext(mPluginContext);
} @Override
protected void onCreate(Bundle savedInstanceState) {
if (mProxy) {
mRealActivity = mProxyActivity;
} else {
super.onCreate(savedInstanceState);
mRealActivity = this;
}
} @Override
public void setContentView(int layoutResID) {
if (mProxy) {
mContentView = LayoutInflater.from(mPluginContext).inflate(layoutResID, null);
mRealActivity.setContentView(mContentView);
} else {
super.setContentView(layoutResID);
}
} ... @Override
public void IOnCreate(Bundle savedInstanceState) {
onCreate(savedInstanceState);
} @Override
public void IOnResume() {
onResume();
} @Override
public void IOnStart() {
onStart();
} @Override
public void IOnPause() {
onPause();
} @Override
public void IOnStop() {
onStop();
} @Override
public void IOnDestroy() {
onDestroy();
} @Override
public void IOnRestart() {
onRestart();
}
}
public class ProxyActivity extends Activity {
IActivity mPluginActivity;
@Override
protected 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();
}
} @Override
protected void onResume() {
super.onResume();
if(mPluginActivity != null){
mPluginActivity.IOnResume();
}
} @Override
protected 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);
} ... @Override
public 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();
} ...

接口实现很easy,仅仅是调用了和接口相应的回调函数。那这里的回调函数终于会调到哪里呢?前面提到过全部插件Activity都会继承自PluginBaseActivity。也就是说这里的回调函数终于会调到插件Activity中相应的回调,比方IOnResume运行的是插件Activity中的onResume中的代码。这也证实了之前的推測。

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

插件内资源获取

实现载入插件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实现也是不完整的,包含不少回调都没有处理。同一时候我也无法保证这套框架没有致命缺陷,本文主要是以总结、学习和交流为目的,欢迎大家一起交流。

基于Proxy思想的Android插件框架的更多相关文章

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

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

  2. 360动态加载的Android插件框架

    github地址:https://github.com/Qihoo360/DroidPlugin DroidPlugin 是360手机助手在Android系统上实现了一种新的插件机制:它可以在无需安装 ...

  3. Android插件化框架

    1.   dynamic-load-apk/DL动态加载框架 是基于代理的方式实现插件框架,对 App 的表层做了处理,通过在 Manifest 中注册代理组件,当启动插件组件时,首先启动一个代理组件 ...

  4. 有关Android插件化思考

    最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优越性,令人目不暇接.随着公司业务快速发展,项目增多,开发资源却有限,如何能在有限资源内 ...

  5. Android插件化开发

    客户端开发给人的印象往往是小巧,快速奔跑.但随着产品的发展,目前产生了大量的门户型客户端.功能模块持续集成,开发人员迅速增长.不同的开发小组开发不同的功能模块,甚至还有其他客户端集成进入.能做到功能模 ...

  6. Android插件实例——360 DroidPlugin具体解释

    在中国找到钱不难,但你的一个点子不意味着是一个创业.你谈一个再好的想法,比方我今天谈一个创意说,新浪为什么不收购GOOGLE呢?这个创意非常好.新浪一收购GOOGLE.是不是新浪就变成老大了?你从哪儿 ...

  7. 基于RxJava2+Retrofit2精心打造的Android基础框架

    代码地址如下:http://www.demodashi.com/demo/12132.html XSnow 基于RxJava2+Retrofit2精心打造的Android基础框架,包含网络.上传.下载 ...

  8. 基于Retrofit+RxJava的Android分层网络请求框架

    目前已经有不少Android客户端在使用Retrofit+RxJava实现网络请求了,相比于xUtils,Volley等网络访问框架,其具有网络访问效率高(基于OkHttp).内存占用少.代码量小以及 ...

  9. 自己动手写Android插件化框架,让老板对你刮目相看

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由达文西发表于云+社区专栏 最近在工作中接触到了Android插件内的开发,发现自己这种技术还缺乏最基本的了解,以至于在一些基本问题上浪 ...

随机推荐

  1. OpenCV学习 3:平滑过度与边缘检测

    原创文章,欢迎转载,转载请注明出处  用来记录学习的过程,这个是简单的相关函数的熟悉,内部机制和选择何种选择函数参数才能达到自己的要求还不太清楚,先学者吧..后面会慢慢清楚的.     和前面相比,主 ...

  2. 改进的延时函数Delay(使用MsgWaitForMultipleObjects等待消息或超时的到来)

    解决上一节中延时函数占CPU使用率(达50%)的第二种方法是利用消息机制,通过API函数MsgWaitForMultipleObjects等待消息或超时的到来,从而避免使用循环检测使CPU占用率过高. ...

  3. sublime text 插件开发

    前言:术语和参考资料 sublime text 2的扩展模式相当的丰富.有多种方法可以修改语法高亮模式以及所有的菜单等.它还可以创建一个新的build系统,自动补全,语言定义,代码片段,宏定义,快捷键 ...

  4. [基础]RHEL6下iSCSI客户端挂载配置

    本文根据鸟哥私房菜进行操作:http://linux.vbird.org/linux_enterprise/xen.php?thisscreen=800x600 一.侦测 [root@vhost3 ~ ...

  5. 【菜鸟学习Linux】-第一章-Linux环境搭建-安装VMware虚拟机

    本人菜鸟一个,刚毕业才上班2个月,现在用到Linux部署项目,这才开始学习Linux,以下是我在安装Linxu系统是遇到的一些问题,希望能给广大菜鸟们在学习的道路上提供帮助和指导,废话不多说!开工! ...

  6. mysql关联更新

    update tb_sdd_info a,tb_bnm_evian_info b set a.username=b.username where a.username=b.memberno and  ...

  7. SQL语句一些特殊的用法

    SQL语句一些特殊的用法 一.基础 1.说明:创建数据库 CREATE DATABASE database-name  2.说明:删除数据库 drop database dbname 3.说明:备份s ...

  8. .NET读取Project 2007 MPP项目文件

    Project文件读取: 方法1:Microsoft.Project.OLEDB.11.0 string strConn = "Provider=Microsoft.Project.OLED ...

  9. WCF Service Configuration Editor的使用

    原文:http://www.cnblogs.com/Ming8006/p/3772221.html 通过WCF Service Configuration Editor的配置修改Client端 参考 ...

  10. python定制类(以Fib类为例)

    class Fib(object): def __init__(self): self.a, self.b = 0, 1 def __iter__(self): return self def __n ...