做者:kaedeajava
项目:android-dynamical-loadingandroid
简单模式中,使用ClassLoader加载外部的Dex或Apk文件,能够加载一些本地APP不存在的类,从而执行一些新的代码逻辑。可是使用这种方法却不能直接启动插件里的Activity。git
Activity等组件是须要在Manifest中注册后才能以标准Intent的方式启动的(若是有兴趣强烈推荐你了解下Activity生命周期实现的机制及源码),经过ClassLoader加载并实例化的Activity实例只是一个普通的Java对象,能调用对象的方法,可是它没有生命周期,并且Activity等系统组件是须要Android的上下文环境的(Context等资源),没有这些东西Activity根本没法工做。github
使用插件APK里的Activity须要解决两个问题:segmentfault
如何使插件APK里的Activity具备生命周期;安全
如何使插件APK里的Activity具备上下文环境(使用R资源);服务器
代理Activity模式为解决这两个问题提供了一种思路。cookie
这种模式也是咱们项目中,继“简单动态加载模式”以后,第二种投入实际生产项目的开发方式。app
其主要特色是:主项目APK注册一个代理Activity(命名为ProxyActivity),ProxyActivity是一个普通的Activity,但只是一个空壳,自身并无什么业务逻辑。每次打开插件APK里的某一个Activity的时候,都是在主项目里使用标准的方式启动ProxyActivity,再在ProxyActivity的生命周期里同步调用插件中的Activity实例的生命周期方法,从而执行插件APK的业务逻辑。框架
ProxyActivity + 没注册的Activity = 标准的Activity
下面谈谈代理模式是怎么处理上面提到的两个问题的。
目前还真的没什么办法可以处理这个问题,一个Activity的启动,若是不采用标准的Intent方式,没有经历过Android系统Framework层级的一系列初始化和注册过程,它的生命周期方法是不会被系统调用的(除非你可以修改Android系统的一些代码,而这已是另外一个领域的话题了,这里不展开)。
那把插件APK里全部Activity都注册到主项目的Manifest里,再以标准Intent方式启动。可是事先主项目并不知道插件Activity里会新增哪些Activity,若是每次有新加的Activity都须要升级主项目的版本,那不是本末倒置了,不如把插件的逻辑直接写到主项目里来得方便。
那就绕绕弯吧,生命周期不就是系统对Activity一些特定方法的调用嘛,那咱们能够在主项目里建立一个ProxyActivity,再由它去代理调用插件Activity的生命周期方法(这也是代理模式叫法的由来)。用ProxyActivity(一个标准的Activity实例)的生命周期同步控制插件Activity(普通类的实例)的生命周期,同步的方式能够有下面两种:
在ProxyActivity生命周期里用反射调用插件Activity相应生命周期的方法,简单粗暴。
把插件Activity的生命周期抽象成接口,在ProxyActivity的生命周期里调用。另外,多了这一层接口,也方便主项目控制插件Activity。
这里补充说明下,Fragment自带生命周期,用Fragment来代替Activity开发能够省去大部分生命周期的控制工做,可是会使得界面跳转比较麻烦,并且Honeycomb之前没有Fragment,没法在API11之前的系统使用。
使用代理的方式同步调用生命周期的作法容易理解,也没什么问题,可是要使用插件里面的res资源就有点麻烦了。简单的说,res里的每个资源都会在R.java里生成一个对应的Integer类型的id,APP启动时会先把R.java注册到当前的上下文环境,咱们在代码里以R文件的方式使用资源时正是经过使用这些id访问res资源,然而插件的R.java并无注册到当前的上下文环境,因此插件的res资源也就没法经过id使用了。
这个问题困扰了咱们好久,一开始的项目急于投入生产,因此咱们索性抛开res资源,插件里须要用到的新资源都经过纯Java代码的方式建立(包括XML布局、动画、点九图等),蛋疼但有效。知道网上出现了解决这一个问题的有效方法(一开始貌似是在手机QQ项目中出现的,可是没有开源因此不清楚,在这里真的佩服这些对技术这么有追求的开发者)。
记得咱们平时怎么使用res资源的吗,就是“getResources().getXXX(resid)”,看看“getResources()”
@Override public Resources getResources() { if (mResources != null) { return mResources; } if (mOverrideConfiguration == null) { mResources = super.getResources(); return mResources; } else { Context resc = createConfigurationContext(mOverrideConfiguration); mResources = resc.getResources(); return mResources; } }
看起来像是经过mResources实例获取res资源的,在找找mResources实例是怎么初始化的,看看上面的代码发现是使用了super类ContextThemeWrapper里的“getResources()”方法,看进去
Context mBase; public ContextWrapper(Context base) { mBase = base; } @Override public Resources getResources() { return mBase.getResources(); }
看样子又调用了Context的“getResources()”方法,看到这里,咱们知道Context只是个抽象类,其实际工做都是在ContextImpl完成的,赶忙去ContextImpl里看看“getResources()”方法吧
@Override public Resources getResources() { return mResources; }
…………
……
你TM在逗我么,仍是没有mResources的建立过程啊!啊,不对,mResources是ContextImpl的成员变量,多是在构造方法中建立的,赶忙去看看构造方法(这里只给出关键代码)。
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(), packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo); mResources = resources;
看样子是在ResourcesManager的“getTopLevelResources”方法中建立的,看进去
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo) { Resources r; AssetManager assets = new AssetManager(); if (libDirs != null) { for (String libDir : libDirs) { if (libDir.endsWith(".apk")) { if (assets.addAssetPath(libDir) == 0) { Log.w(TAG, "Asset path '" + libDir + "' does not exist or contains no resources."); } } } } DisplayMetrics dm = getDisplayMetricsLocked(displayId); Configuration config ……; r = new Resources(assets, dm, config, compatInfo); return r; }
看来这里是关键了,看样子就是经过这些代码从一个APK文件加载res资源并建立Resources实例,通过这些逻辑后就可使用R文件访问资源了。具体过程是,获取一个AssetManager实例,使用其“addAssetPath”方法加载APK(里的资源),再使用DisplayMetrics、Configuration、CompatibilityInfo实例一块儿建立咱们想要的Resources实例。
最终访问插件APK里res资源的关键代码以下
try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, mDexPath); mAssetManager = assetManager; } catch (Exception e) { e.printStackTrace(); } Resources superRes = super.getResources(); mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
注意,有的人担忧从插件APK加载进来的res资源的ID可能与主项目里现有的资源ID冲突,其实这种方式加载进来的res资源并非融入到主项目里面来,主项目里的res资源是保存在ContextImpl里面的Resources实例,整个项目共有,而新加进来的res资源是保存在新建立的Resources实例的,也就是说ProxyActivity其实有两套res资源,并非把新的res资源和原有的res资源合并了(因此不怕R.id重复),对两个res资源的访问都须要用对应的Resources实例,这也是开发时要处理的问题。(其实应该有3套,Android系统会加载一套framework-res.apk资源,里面存放系统默认Theme等资源)
额外补充下,这里你可能注意到了咱们采用了反射的方法调用AssetManager的“addAssetPath”方法,而在上面ResourcesManager中调用AssetManager的“addAssetPath”方法是直接调用的,不用反射啊,并且看看SDK里AssetManager的“addAssetPath”方法的源码(这里也能看到具体APK资源的提取过程是在Native里完成的),发现它也是public类型的,外部能够直接调用,为何还要用反射呢?
/** * 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) { synchronized (this) { int res = addAssetPathNative(path); makeStringBlocks(mStringBlocks); return res; } }
这里有个误区,SDK的源码只是给咱们参考用的,APP实际上运行的代码逻辑在android.jar里面(位于android-sdk\platforms\android-XX),反编译android.jar并找到ResourcesManager类就能够发现这些接口都是对应用层隐藏的。
public final class AssetManager{ AssetManager(){throw new RuntimeException("Stub!"); } public void close() { throw new RuntimeException("Stub!"); } public final InputStream open(String fileName) throws IOException { throw new RuntimeException("Stub!"); } public final InputStream open(String fileName, int accessMode) throws IOException { throw new RuntimeException("Stub!"); } public final AssetFileDescriptor openFd(String fileName) throws IOException { throw new RuntimeException("Stub!"); } public final native String[] list(String paramString) throws IOException; public final AssetFileDescriptor openNonAssetFd(String fileName) throws IOException { throw new RuntimeException("Stub!"); } public final AssetFileDescriptor openNonAssetFd(int cookie, String fileName) throws IOException { throw new RuntimeException("Stub!"); } public final XmlResourceParser openXmlResourceParser(String fileName) throws IOException { throw new RuntimeException("Stub!"); } public final XmlResourceParser openXmlResourceParser(int cookie, String fileName) throws IOException { throw new RuntimeException("Stub!"); } protected void finalize() throws Throwable { throw new RuntimeException("Stub!"); } public final native String[] getLocales(); }
到此,启动插件里的Activity的两大问题都有解决的方案了。
上面只是分析了代理模式的关键技术点,若是运用到具体项目中去的话,除了两个关键的问题外,还有许多繁琐的细节须要处理,咱们须要设计一个框架,规范插件APK项目的开发,也方便之后功能的扩展。这里,dynamic-load-apk向咱们展现了许多优秀的处理方法,好比:
把Activity关键的生命周期方法抽象成DLPlugin接口,ProxyActivity经过DLPlugin代理调用插件Activity的生命周期;
设计一个基础的BasePluginActivity类,插件项目里使用这些基类进行开发,能够以接近常规Android开发的方式开发插件项目;
以相似的方式处理Service的问题;
处理了大量常见的兼容性问题(好比使用Theme资源时出现的问题);
处理了插件项目里的so库的加载问题;
使用PluginPackage管理插件APK,从而能够方便地管理多个插件项目;
这里须要把插件APK里面的SO库文件解压释放出来,在根据当前设备CPU的型号选择对应的SO库,并使用System.load方法加载到当前内存中来,具体分析请参考 Android动态加载补充 加载SD卡的SO库。
动态加载一个插件APK须要三个对应的DexClassLoader
、AssetManager
、Resources
实例,能够用组合的方式建立一个PluginPackage
类存放这三个变量,再建立一个管理类PluginManager
,用成员变量HashMap<dexPath,pluginPackage>
的方式保存PluginPackage
实例。
具体的代码请参考原项目的文档、源码以及Sample里面的示例代码,在这里感谢singwhatiwanna的开源精神。
使用动态加载的目的,就是但愿能够绕过APK的安装过程升级应用的功能,若是插件APK是打包在主项目内部的那动态加载纯粹是屡次一举。更多的时候咱们但愿能够在线下载插件APK,而且在插件APK有新版本的时候,主项目要从服务器下载最新的插件替换本地已经存在的旧插件。为此,咱们应该有一个管理后台,它大概有如下功能:
上传不一样版本的插件APK,并向APP主项目提供插件APK信息查询功能和下载功能;
管理在线的插件APK,并能向不一样版本号的APP主项目提供最合适的插件APK;
万一最新的插件APK出现紧急BUG,要提供旧版本回滚功能;
出于安全考虑应该对APP项目的请求信息作一些安全性校验;
加载外部的可执行代码,一个逃不开的问题就是要确保外部代码的安全性,咱们可不但愿加载一些来历不明的插件APK,由于这些插件有的时候能访问主项目的关键数据。
最简单可靠的作法就是校验插件APK的MD5值,若是插件APK的MD5与咱们服务器预置的数值不一样,就认为插件被改动过,弃用。
这一部分做为补充说明,若是不太熟悉动态加载的使用姿式,可能不是那么容易理解。
谈到动态加载的时候咱们常常说到“热部署”和“插件化”这些名词,它们虽然都和动态加载有关,可是仍是有一点区别,这个问题涉及到主项目与插件项目的交互方式。前面咱们说到,动态加载方式,能够在“项目层级”作到代码分离,按道理咱们但愿是主项目和插件项目不要有任何交互行为,实际上也应该如此!这样作不只能确保项目的安全性,也能简化开发工做,因此通常的作法是
主项目仍是像常规Android项目那样开发,只有用户使用插件APK的功能时才动态加载插件并运行,插件一旦运行后,与主项目没有任何交互逻辑,只有在主项目启动插件的时候才触发一次调用插件的行为。好比,咱们的主项目里有几款推广的游戏,平时在用户使用主项目的功能时,能够先静默把游戏(其实就是一个插件APK)下载好,当用户点击游戏入口时,以动态加载的方式启动游戏,游戏只运行插件APK里的代码逻辑,结束后返回主项目界面。
另一种彻底相反的情形是,主项目只提供一个启动的入口,以及从服务器下载最新插件的更新逻辑,这两部分的代码都是长期保持不变的,应用一启动就动态加载插件,全部业务逻辑的代码都在插件里实现。好比如今一些游戏市场都要求开发者接入其SDK项目,若是SDK项目采用这种开发方式,先提供一个空壳的SDK给开发者,空壳SDK能从服务器下载最新的插件再运行插件里的逻辑,就能保证开发者开发的游戏每次启动的时候都能运行最新的代码逻辑,而不用让开发者在SDK有新版本的时候从新更换SDK并构建新的游戏APK。
明明,说了不要交互的,恰恰,Android开发者就是这么执着于技术。
有些时候,好比,主项目里有一个成熟的图片加载框架ImageLoader,而插件里也有一个ImageLoader。若是一个应用同时运行两套ImageLoader,那会有许多额外的性能开销,若是能让插件也用主项目的ImageLoader就行了。另外,若是在插件里须要用到用户登陆功能,咱们总不但愿用户使用主项目时进行一次登陆,进入插件时由来一次登陆,若是能在插件里使用主项目的登陆状态就行了。
所以,有些时候咱们但愿插件项目能调用主项目的功能。怎么处理好呢,因为插件项目与主项目是分开的,咱们在开发插件的时候,怎么调用主项目的代码啊?这里须要稍微了解一下Android项目间的依赖方式。
想一想一个普通的APK是怎么构建和运行的,Android SDK提供了许多系统类(如Activity、Fragment等,通常咱们也喜欢在这里查看源码),咱们的Android项目依赖Android SDK项目并使用这些类进行开发,那构建APK的时候会把这些类打包进来吗?不会,要是每一个APK都打包一份,那得有多少冗余啊。因此Android项目至少有两种依赖的方式,一种构建时会把被依赖的项目(Library)的类打包进来,一种不会。
在Android Studio打开项目的Project Structure,找到具体Module的Dependencies选项卡
能够看到Library项目有个Scope属性,这里的Compile模式就是会把Library的类打包进来,而Provided模式就不会。
注意,使用Provided模式的Library只能是jar文件,而不能是一个Android Library项目,由于后者可能自带了一些res资源,这些资源没法一并塞进标准的jar文件里面。到这里咱们明白,Android SDK的代码实际上是打包进系统ROM(俗称Framework层级)里面的,咱们开发Android项目的时候,只是以Provided模式引用android.jar,从这个角度也佐证了上面谈到的“为何APP实际运行时AssetManager类的逻辑会与Android SDK里的源码不同”。
如今好办了,若是要在插件里使用主项目的ImageLoader,咱们能够把ImageLoader的相关代码抽离成一个Android Libary项目,主项目以Compile模式引用这个Libary,而插件项目以Provided模式引用这个Library(编译出来的jar),这样能实现二者之间的交互了,固然代价也是明显的。
咱们应该只给插件开放一些必要的接口,否则会有安全性问题;
做为通用模块的Library应该保持不变(起码接口不变),否则主项目与插件项目的版本同步会复杂许多;
由于插件项目已经严重依赖主项目了,因此插件项目不能独立运行,由于缺乏必要的环境;
最后咱们再说说“热部署”和“插件化”的区别,通常咱们把独立运行的插件APK叫热部署,而须要依赖主项目的环境运行的插件APK叫作插件化。