808 views
# Android 插件框架的原理与实现 ###### tags: `blog` [toc] ## 前言 所谓插件,想想自己平时实用 Chrome 浏览器的时候所安装和使用的插件。Chrome 插件的一个重要的特点是,当你下载 Chrome 的时候,并不用一起下载这些插件,而是在日后你突然想 Chrome 为你提供某项功能的时候,再自由地添加插件;不想用的时候自由移除插件,而并不需要卸载 Chrome 本身。 那么问题来了,Android 的应用程序是否也可以和 Chrome 浏览器这样,提供一种随时添加和删除“插件”的特性呢?比如一个工具集应用,提供词典、天气预报、手电筒、指南针等等功能。但是可能一些用户只需要这一堆功能里的一部分,如果应用下载时就打包好了这一大堆功能,这就造成,下载的时候因安装包过大而浪费了流量,而且占用了手机空间。可不可以这样:用户一开始下载的时候,只是下载这个工具集的一个“壳”,提供了下载各个工具的入口,用户进入该应用的时候,按需下载自己需要的功能的“插件”?并且当插件有更新的时候,用户不需要更新整个应用,而仅仅需要下载更新这个插件?甚至不用用户手动操作,应用自动就把插件更新了,就像web页面那样呢? 上述问题当然是可以解决的,方法就是我们这里要研究的“Android 插件框架”。 ## 基本术语 在继续往下之前,有必要先和读者协商好某些概念术语。 - 宿主:宿主程序,指的是用来装载、调起插件的主体程序。比如上面所举的 Chrome 浏览器的例子中,Chrome 浏览器就是我们所说的“宿主”。 - 插件:这个概念应该不用过多解释,指的就是被宿主程序动态装载和调起的程序,比如 Chrome 浏览器中的有道词典等插件。 ## 基础:Java 的动态加载技术 实现 Android 插件框架的技术基础是 Java 的动态加载技术,即通过 ClassLoader 动态装载外部的类。 我们平时编写 Java 程序时,需要使用到一个类的时候,通常是简单地使用"import"语句将其引入即可。"import"引入的类要求该类必须参与编译,即编译时便知道该类的存在,然后在运行时由内部的类装载器自动装载。如果编译时该类不存在本地,则无法编译通过。而 ClassLoader 则是在真正需要装载该类的时候才知道该类的存在,通过存入类文件的路径等参数去装载该类。 普通的 Java 虚拟机使用的是 .class 格式的类文件,而 Android Dalvik 虚拟机使用的类文件是经过优化的 .dex 格式。相应地,Android 中的类加载器是一个从 ClassLoader 派生出来的 DexClassLoader,通过 DexClassLoader 可加载 .jar(dex处理后) 和 .apk 文件内部的 classes.dex 文件,classes.dex 文件便包含了优化后的适合 Android 程序使用的类。 这里说的 DexClassLoader 便是我们实现 Android 的插件框架的基础。我们编译宿主时,并不需要插件的参与。即宿主在被编译的时候,并不需要知道插件的存在。宿主在编译的时候,插件可以还没开发完成甚至还没开始开发。手机上安装了宿主程序,当插件程序开发完成后,可通过网络下载等手段将插件程序存放到本地,然后宿主通过 DexClassLoader 将其装载到 Dalvik 虚拟上运行。这就是所谓的“动态装载插件”的过程。以下代码示范了如何简单地使用 DexClassLoader 装载一个类。 ```clike= /** * @param dexPath * 被装载的 .jar或.apk 文件的路径列表,多个之间用 File.pathSeparator 分隔 * @param optimizedDirectory * 优化后的 dex 文件存放路径 * @param libraryPath * 被装载的类使用的 native 库的存放路径列表,多个之间用 File.pathSeparator 分隔 * @param parent * 当前类的类装载器 */ ClassLoader dexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, libraryPath, parent); /** * @param className * 被装载的类的完整类名 */ Class clazz = dexClassLoader.loadClass(className); ``` ## 实现方案 对于 Android 插件的实现,目前比较流行的方案有两种:1. 基于代理的思想;2. 基于 Android 的 Fragment。 ### 1. 基于代理的思想: Android 应用是基于四大组件(Activity, Service, ContentProvider 和 BroadcastReceiver)进行开发的,而众所周知,Android 应用需要使用上述组件时,需要先在 AndroidManifest.xml 中声明注册(ContentProvider 和 BroadcastReceiver 也可动态注册),应用在启动前,系统就需要根据 AndroidManifest.xml 中的信息对应用进行各种初始化。而对于动态加载的插件程序,它的装载和启动是由宿主程序主导的,系统无法把它当做一个独立的应用对其进行各种上下文初始化。因此,插件程序中声明的各种 Activity 或 Service 等,和普通的类没什么两样,系统无法像管理正常的组件那样去管理它们了,比如无法通过 Context.startActivity() 去启动插件中的一个 Activity,也无法自动管理它们的生命周期。基本的原理是在宿主程序中创建一个代理的 Activity(以下称 ProxyActivity),ProxyActivity 是常规的 Activity,在 AndroidManifest.xml 下有声明;插件中的 Activity 称为 PluginActivity。ProxyActivity 唤起 PluginActivity 的时候,把自己作为参数传递给 PluginActivity,使得 PluginActivity 需要调用某些系统接口,比如 setContentView 的时候,直接调用 ProxyActivity 的 setContentView;并且,当 ProxyActivity 的生命周期发生切换时,把切换同步到 PluginActivity。博主实现的插件框架不会采用这种思路,因此这里不再赘述,有兴趣的朋友可以自行研究,并且具体的实现也很容易在互联网上找到。 ### 2. 基于 Fragment: Fragment 是 Android 3.0(2.x 版本通过引入 "android-support-v4.jar" 实现支持) 之后引入的,可以嵌套在 Activity 中,负责应用界面的展示,具有和 Activity 类似的生命周期。Fragment 的使用不需要在 AndroidManifest.xml 文件下声明,并且可以在同一个 Activity 下进行多个 Fragment 之间的切换。这正好拟补了上一种思路所说的无法像普通应用那样随便使用 Activity 组件去展示 UI 的缺陷(因为 Activity 需要在 AndroidManifest.xml 里静态声明才能被系统理解)。因此,我们可以根据这样的思路来实现一个插件框架: 在宿主中声明一个 Activity,用于承载插件,这里称之为 HostActivity。宿主启动时,启动 HostActivity,然后 HostActivity 去动态加载插件的初始 Fragment。实际上,HostActivity 只需要知道插件的初始 Fragment 的存在,插件中如果还有其他的界面需要跳转和展示,这些完全可以交给插件自己去管理,因为 Fragment 不需要像 Activity 那样在 AndroidManifest.xml 里去声明,是可以像普通的 Java 类那样去被使用的。 博主实现的插件框架,正是基于这一种思路的。示范代码如下: ```clike= /** * @file: com.tigaliang.host.HostActivity.java 宿主中,用来承载插件的 Activity, * 动态加载插件的初始 Fragment */ public class HostActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { /** 动态加载 Fragment */ ClassLoader dexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, libraryPath, parent); Class apFragmentClass = dexClassLoader .loadClass("com.tigaliang.plugin.PluginFragment"); Fragment fragment = apFragmentClass.newInstance(); /** 把 Fragment 嵌入 Activity 中 */ if (fragment != null) { fragment.setArguments(getIntent().getExtras()); getSupportFragmentManager().beginTransaction() .add(android.R.id.content, fragment).commit(); } } } } /** * file: com.tigaliang.plugin.PluginFragment.java * 插件的初始 Fragment */ public class PluginFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { /** 创建插件的主界面 */ TextView textView = new TextView(getActivity()); textView.setText("Hello world!"); return textView; } } ``` ## 难点 ### 1. 资源访问 我们通过 Java 的动态加载技术来加载调起插件,显然我们加载 Java 类的时候不会加载 apk 中资源(如 R.string, R.layout 等等)。一种访问插件资源的方法是,我们自己实现通过文件访问去获取插件 apk 中的所有资源。但在 Android 中,系统明明给我们提供了 API,通过 Context.getResources() 接口就可以简单地通过 R 类中定义的资源 id 去访问资源,我们肯定不会蛋疼到选用自己去读取文件那种方法。但是,动态加载起来的插件甚至没有自己 apk 的 Context,怎么用 Context 去访问资源呢?方法是,实现自己的 Context,实现自己的 Context 的 getResources() 方法。getRescources() 方法返回一个 Resources 对象,查看 Android 源码,Resources 有一个这样的构造方法: ```clike= /** * Create a new Resources object on top of an existing set of assets in an * AssetManager. * * @param assets * Previously created AssetManager. * @param metrics * Current display metrics to consider when selecting/computing resource values. * @param config * Desired device configuration to consider when selecting/computing resource * values (optional). */ public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); } ``` 看得出,第一个参数 AssetManager 就是资源相关的,因此需要插件自己的 AssetManager。看 AssetManager 源码,看到: ```clike= /** * Add an additional set of assets to the asset manager. This can be * either a directory or ZIP file. Not for use by applications. Returns * the cookie of the added asset, or 0 on failure. * {@hide} */ public final int addAssetPath(String path) { int res = addAssetPathNative(path); return res; } ``` 于是,我们只要调用这个 AssetManager 的这个接口,把插件 apk 的路径传入,系统就可以构造插件自己的 Rescources 对象,我们就可以和普通的 Android 程序那样,通过 R 里的 id 去简便地访问应用资源了。需要提醒的是,因为 addAssetPath 方法是加了 "{@hide}" 的隐藏 API,因此需要反射调用。另外,除了实现 getResources() 和 getAssets() 获取资源,还有就是在使用 LayoutInflater 去解释 xml 文件生成 Android 的 View 对象时,也许要使用到 Context。获得应用的 LayoutInflater 的方法是 LayoutInflater.from(Context),查看这个方法的源码,发现这里是通过 Context 的 getSystemService 方法实现的,因此我们也要重写这个方法,获取插件自己的 LayoutInflater。下面是一个插件自己的 Context 的参考实现: ```clike= public class APContext extends ContextWrapper { private AssetManager mAssetManager; private Resources mResources; public APContext(Context base, String srcDexPath) { super(base); if (!TextUtils.isEmpty(srcDexPath)) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath", String.class); addAssetPath.invoke(assetManager, srcDexPath); mAssetManager = assetManager; Resources superResources = super.getResources(); mResources = new Resources(mAssetManager, superResources.getDisplayMetrics(), superResources.getConfiguration()); } catch (Throwable e) { e.printStackTrace(); } } } @Override public Resources getResources() { return mResources == null ? super.getResources() : mResources; } @Override public AssetManager getAssets() { return mAssetManager == null ? super.getAssets() : mAssetManager; } @Override public Object getSystemService(String name) { if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) { LayoutInflater layoutInflater = (LayoutInflater) super.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (layoutInflater == null) { throw new AssertionError("LayoutInflater not found."); } /** 关键一步:使用当前 Context 构造 LayoutInflater */ return layoutInflater.cloneInContext(this); } return super.getSystemService(name); } } ``` ### 2. 插件与宿主之间的通信 (未完待续)