1.前言
在这个移动应用蓬勃发展的时代,追求新颖成为了软件开发的首要纲领,所以应用会自然而然的爆棚(方法数超过了一个 Dex 最大方法数 65535 的上限 ),然后Android插件化也就理所当然的出现了。
这并不是一篇对于插件化研究的早期文章,但是文章介绍的插件化方式的突破确是可以载入史册的:)
Android 插件化 —— 是指将一个程序划分为不同的部分,比如QQ的皮肤样式就可以看成一个插件。
Android 组件化 —— 这个概念实际跟上面相差不那么明显,组件和插件较大的区别就是:组件是指通用及复用性较高的构件,比如图片缓存就可以看成一个组件被多个 App 共用。
Android 动态加载 —— 这个实际是更高层次的概念,也有叫法是热加载或 Android 动态部署,指容器(App)在运⾏状态下动态加载某个模块,从而新增功能或改变某⼀部分行为。这也是本文所要实现的。
3.相关开源框架
(1)https://github.com/singwhatiwanna/dynamic-load-apk
这个项目的原理是把一个从ClassLoader中加载的自定义Activity类当成一个Object创建,然后使用一个代理Activity在相应的生命周期调用相应的方法。
这个项目里有几个问题没解决,一个是 FragmentActivity 或是 ActionBarActiviy 的代理方式不行,因为存在 ClassLoader 冲突问题,必须在插件和宿主中只留下一份Android.support的jar。第二个问题是必须使用that指针代替this,因为直接new的Object不具有Activity特性。
(2)https://github.com/mmin18/AndroidDynamicLoader
这个项目的插件化方式和上面有很大的不同,他不是用代理 Activity 的方式实现的,而是用 Fragment 以及 schema 的方式实现,总体上讲开发有一定的复杂性。
(3)https://github.com/houkx/android-pluginmgr
这个框架比上面两个都要牛,它不需要对插件有任何约束,可以直接启动一个apk,原理是使用DexMaker的动态热部署生成一个Activity,让这个Activity继承目标插件所在的Activity,这样类名就被固定下来,唯一的改变是继承的父类在改变。虽说使用这个框架加载插件没有约束,但是由于是基于热部署,框架的稳定性就大打折扣了, 其中的OOM问题特别突出,因此实际中能够满足加载体验的只有一些轻量级的小型APK。
4.更强大的解决方案
经过数月对Android源码的研究,一款名为Direct-load-apk的插件加载框架终于诞生,这款框架结合了Dynamic-Load-apk 和 PluginMgr 的弱点,使用了新的思路,成功实现了启动普通的apk。
<1>介绍
<2>框架解读
有了上面的理论知识,我们来开始深入探讨如何才能真正做到不安装而直接启动apk。
主要涉及以下的类:
*com.lody.plugin.LActivityProxy
*com.lody.plugin.LPluginDexLoader
*com.lody.plugin.LPluginInsrument
*com.lody.plugin.bean.LPlugin
*LActivityProxy是真正的Activity,在宿主的AndroidManifest.xml中需要声明,所有的插件事务都会转交给它,甚至囊括插件的资源,这一点有点像dynamic-load-apk。
*LPluginDexLoader负责提取和加载插件apk中的Dex文件,并加载到插件化框架中。
*LPluginInsrument 继承自android.app.Instrumentation ,它的作用极为突出,也是笔者当初克服的难题之一,可以说如果没有它,框架就不能实现插件间跳转。
*com.lody.plugin.bean.LPlugin 负责维护一个插件的信息,由com.lody.plugin.LPluginManager来管理。
以前DL的作者等人其实写过挺多文章的,有兴趣的朋友可以先阅读,对于下面的理解有很大的帮助:
http://blog.csdn.net/singwhatiwanna/article/details/40283117
http://blog.csdn.net/singwhatiwanna/article/details/22597587
http://blog.csdn.net/singwhatiwanna/article/details/39937639
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1207/2123.html
下面我们来跳过基础讲讲难点:
读者应该知道,ClassLoader加载的类只能算是一个普通的对象,不具备生命周期,因此如果自己new一个Activity,是没有任何实际意义的,那么为什么系统创建的Activity具有生命周期呢? 原因很简单,因为系统会将创建的Activity保存下来,进行管理(主要涉及ActivityThread,ActivityManagerService,ActivityManager,ActivityStack。
现在没有生命周期的原因找到了,我们来对症下药,何不使用系统创建的Activity来间接管理我们自己加载的Activity呢?
如dynamic-load-apk框架所描述的,由于自己创建的Activity并不是真正意义上的Activity,因此this不指向当前dynamic-load-apk的解决办法是让插件继承自定义的Activity,使用that指向代理的Activity,代替this指针,这就是dynamic-load-apk最失败的地方。
那么这个问题有解决办法吗?答案是有。Direct-Load-apk 就很好的解决了这个问题,我们来看看是怎么解决的:
//开始伪装插件为实体Activity proxyRef = Reflect.on(proxy); pluginRef = Reflect.on(plugin); pluginRef.set("mBase", proxy); pluginRef.set("mDecor", proxyRef.get("mDecor")); pluginRef.set("mTitleColor", proxyRef.get("mTitleColor")); pluginRef.set("mWindowManager", proxyRef.get("mWindowManager")); pluginRef.set("mWindow", proxy.getWindow()); pluginRef.set("mManagedDialogs", proxyRef.get("mManagedDialogs")); pluginRef.set("mCurrentConfig", proxyRef.get("mCurrentConfig")); pluginRef.set("mSearchManager", proxyRef.get("mSearchManager")); pluginRef.set("mMenuInflater", proxyRef.get("mMenuInflater")); pluginRef.set("mConfigChangeFlags", proxyRef.get("mConfigChangeFlags")); pluginRef.set("mIntent", proxyRef.get("mIntent")); pluginRef.set("mToken", proxyRef.get("mToken")); Instrumentation instrumentation = proxyRef.get("mInstrumentation"); pluginRef.set("mInstrumentation", new LPluginInsrument(instrumentation)); pluginRef.set("mMainThread", proxyRef.get("mMainThread")); pluginRef.set("mEmbeddedID", proxyRef.get("mEmbeddedID")); pluginRef.set("mApplication",app == null ? proxy.getApplication() : app); pluginRef.set("mComponent", proxyRef.get("mComponent")); pluginRef.set("mActivityInfo", proxyRef.get("mActivityInfo")); pluginRef.set("mAllLoaderManagers", proxyRef.get("mAllLoaderManagers")); pluginRef.set("mLoaderManager", proxyRef.get("mLoaderManager")); if (Build.VERSION.SDK_INT >= 13) { //在android 3.2 以后,Android引入了Fragment. FragmentManager mFragments = proxy.getFragmentManager(); pluginRef.set("mFragments", mFragments); pluginRef.set("mContainer", proxyRef.get("mContainer")); } if (Build.VERSION.SDK_INT >= 12) { //在android 3.0 以后,Android引入了ActionBar. pluginRef.set("mActionBar", proxyRef.get("mActionBar")); } pluginRef.set("mUiThread", proxyRef.get("mUiThread")); pluginRef.set("mHandler", proxyRef.get("mHandler")); pluginRef.set("mInstanceTracker", proxyRef.get("mInstanceTracker")); pluginRef.set("mTitle", proxyRef.get("mTitle")); pluginRef.set("mResultData", proxyRef.get("mResultData")); pluginRef.set("mDefaultKeySsb", proxyRef.get("mDefaultKeySsb")); pluginRef.call("attachBaseContext",proxy); plugin.getWindow().setCallback(plugin);
读者应该看出来了,我们自己创建的Activity之所以不具备Activity是因为它内部的数据全部为Null,如果我们把它们全部替换成代理的Activity,那么问题是不是迎刃而解了呢?
注意上面最关键的一句话,pluginRef.call("attachBaseContext",proxy);
这一句的作用尤为关键,我们知道,Activity继承自ContextThemeWarpper,ContextThemeWarpper又继承自ContextWarpper,我们不妨阅读它的代码,看透它的本质:
public class ContextWrapper extends Context { Context mBase; public ContextWrapper(Context base) { mBase = base; } /** * Set the base context for this ContextWrapper. All calls will then be * delegated to the base context. Throws * IllegalStateException if a base context has already been set. * * @param base The new base context for this wrapper. */ protected void attachBaseContext(Context base) { if (mBase != null) { throw new IllegalStateException("Base context already set"); } mBase = base; } public Context getBaseContext() { return mBase; } @Override public AssetManager getAssets() { return mBase.getAssets(); } @Override public Resources getResources() { return mBase.getResources(); } @Override public PackageManager getPackageManager() { return mBase.getPackageManager(); } @Override public ContentResolver getContentResolver() { return mBase.getContentResolver(); } @Override public Looper getMainLooper() { return mBase.getMainLooper(); } @Override public Context getApplicationContext() { return mBase.getApplicationContext(); } @Override public void setTheme(int resid) { mBase.setTheme(resid); } ...
可以看到,ContextWarpper实际上就是一个包装代理类,它的全部工作都转交其中的mBase来实现,这么做是为了把ContextImp隐藏起来。
看到这里,读者应该明白了,pluginRef.call("attachBaseContext",proxy)的作用就是把mBase指向代理的Activity,那么this就能够很好的工作了。
第二个问题:插件的跳转:
首先来看看Activity的startActivity方法: @Override public void startActivity(Intent intent) { startActivity(intent, null); } ... public void startActivity(Intent intent, Bundle options) { if (options != null) { startActivityForResult(intent, -1, options); } else { startActivityForResult(intent, -1); } } ... public void startActivityForResult(Intent intent, int requestCode) { startActivityForResult(intent, requestCode, null); } ... //真正的跳转处理类: public void startActivityForResult(Intent intent, int requestCode, Bundle options) { if (mParent == null) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options); if (ar != null) { mMainThread.sendActivityResult( mToken, mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData()); } if (requestCode >= 0) { ... } final View decor = mWindow != null ? mWindow.peekDecorView() : null; if (decor != null) { decor.cancelPendingInputEvents(); } } else { if (options != null) { mParent.startActivityFromChild(this, intent, requestCode, options); } else { mParent.startActivityFromChild(this, intent, requestCode); } } }
可以看到真正处理跳转的是mInstrumentation.execStartActivity(this,mMainThread.getApplicationThread(), mToken, this,intent, requestCode, options);
那么问题就来了,我们接触不到插件,因此无法复写startActivity转移跳转目标,最后想到了注入Instrumentation,来看看execStartActivity:
/* * {@hide} */ public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ... }
呵呵,看到了吧,这个方法被隐藏了,不能复写。
那么怎么办呢?我们来试试自定义Instrumentation,强制写一个execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options)方法:
/** * Created by lody on 2015/3/27. * * @author Lody * * 负责转移插件的跳转目标<br> * @see android.app.Activity#startActivity(android.content.Intent) */ public class LPluginInsrument extends Instrumentation { Instrumentation pluginIn; Reflect instrumentRef; public LPluginInsrument(Instrumentation pluginIn){ this.pluginIn = pluginIn; instrumentRef = Reflect.on(pluginIn); } /**@Override*/ public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { Intent gotoPluginOrHost = new Intent(); ComponentName componentName = intent.getComponent(); if(componentName == null){ return instrumentRef.call("execStartActivity",who,contextThread,token,target,intent,requestCode,options).get(); } String className = componentName.getClassName(); gotoPluginOrHost.setClass(who, LActivityProxy.class); gotoPluginOrHost.putExtra(LPluginConfig.KEY_PLUGIN_DEX_PATH, LPluginManager.finalApkPath); gotoPluginOrHost.putExtra(LPluginConfig.KEY_PLUGIN_ACT_NAME,className); gotoPluginOrHost.setAction(intent.getAction()); gotoPluginOrHost.setData(intent.getData()); gotoPluginOrHost.setType(intent.getType()); if(Build.VERSION.SDK_INT >= 16) gotoPluginOrHost.setClipData(intent.getClipData()); gotoPluginOrHost.setFlags(intent.getFlags()); return instrumentRef.call("execStartActivity",who,contextThread,token,target,gotoPluginOrHost,requestCode,options).get(); }
经过实践,这种复写方法是可行的。
到此,一切问题基本解决,开始体验Direct-Load-Apk 带给你的使用this的快感吧!
OSChina : http://git.oschina.net/lody/Direct-load-apk
Github : https://github.com/FinalLody/Direct-Load-apk/