基于Proxy思想的Android插件框架
2016-02-17 18:15
429 查看
意义
研究插件框架的意义在于下面几点:减小安装包的体积,通过网络选择性地进行插件下发
模块化升级。减小网络流量
静默升级,用户无感知情况下进行升级
解决低版本号机型方法数超限导致无法安装的问题
代码解耦
现状
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; ... @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); } }
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(); } ...
接口实现很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实现也是不完整的,包含不少回调都没有处理。同一时候我也无法保证这套框架没有致命缺陷,本文主要是以总结、学习和交流为目的,欢迎大家一起交流。
相关文章推荐
- ANDROID_MARS学习笔记_S01原始版_022_MP3PLAYER002_本地及remote标签
- android 快速创建一个新的线程
- Android利用ant将多个jar包合并成一个jar包
- Android播放本地语音
- MPAndroidChart开源图表库(一)之饼状图
- Android EventBus框架
- 【从零之六&完结】android口语对话系统(RavenClaw java版 含所有源代码)
- Android串口操作,简化android-serialport-api的demo的问题
- 监听android home键的实现方式
- # android 开发小问题 button
- Android内存泄漏原因
- 初步试用android studio
- android setCookie 免登录
- 薪酬浅析Android开发者比iOS开发者工资低吗
- Android studio -SVN 使用笔记
- 上百个Android开源项目分享
- 2016.2 Android 项目开发总结
- facebook的Android调试工具Stetho介绍
- 新浪微博 分享功能 handleWeiboResponse(intent, response); 不回调的解决办法
- Android Studio 视频教程分享