基于Proxy思想的Android插件框架
本文所有代碼托管在Github:android-plugin
意義
研究插件框架的意義在于以下幾點(diǎn):
- 減小安裝包的體積,通過網(wǎng)絡(luò)選擇性地進(jìn)行插件下發(fā)
- 模塊化升級(jí),減小網(wǎng)絡(luò)流量
- 靜默升級(jí),用戶無感知情況下進(jìn)行升級(jí)
- 解決低版本機(jī)型方法數(shù)超限導(dǎo)致無法安裝的問題
- 代碼解耦
現(xiàn)狀
Android中關(guān)于插件框架的技術(shù)已經(jīng)有過不少討論和實(shí)現(xiàn),插件通常打包成apk或者dex的形式。
dex形式的插件往往提供了一些功能性的接口,這種方式類似于java中的jar形式,只是由于Android的Dalvik VM無法直接動(dòng)態(tài)加載Java的Byte Code,所以需要我們提供Dalvik Byte Code,而dex就是Dalvik Byte Code形式的jar。
apk形式的插件提供了比dex形式更多的功能,例如可以將資源打包進(jìn)apk,也可實(shí)現(xiàn)插件內(nèi)的Activity或者Service等系統(tǒng)組件。
本文主要討論apk形式的插件框架,對(duì)于apk形式又存在安裝和不安裝兩種方式
-
安裝apk的方式實(shí)現(xiàn)相對(duì)簡單,主要原理是通過將插件apk和主程序共享一個(gè)UserId,主程序通過createPackageContext構(gòu)造插件的context,通過context即可訪問插件apk中的資源,很多app的主題框架就是通過安裝插件apk的形式實(shí)現(xiàn),例如Go主題。這種方式的缺點(diǎn)就是需要用戶手動(dòng)安裝,體驗(yàn)并不是很好。
-
不安裝apk的方式解決了用戶手動(dòng)安裝的缺點(diǎn),但實(shí)現(xiàn)起來比較復(fù)雜,主要通過DexClassloader的方式實(shí)現(xiàn),同時(shí)要解決如何啟動(dòng)插件中Activity等Android系統(tǒng)組件,為了保證插件框架的靈活性,這些系統(tǒng)組件不太好在主程序中提前聲明,實(shí)現(xiàn)插件框架真正的難點(diǎn)在此。
DexClassloader
這里引用《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》第二版里對(duì)java類加載器的一段描述:
虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類加載階段中的“通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為“類加載器”。
Android虛擬機(jī)的實(shí)現(xiàn)參考了java的JVM,因此在Android中加載類也用到了類加載器的概念,只是相對(duì)于JVM中加載器加載class文件而言,Android的Dalvik虛擬機(jī)加載的是Dex格式,而具體完成Dex加載的主要是PathClassloader和Dexclassloader。
PathClassloader默認(rèn)會(huì)讀取/data/dalvik-cache中緩存的dex文件,未安裝的apk如果用PathClassloader來加載,那么在/data/dalvik-cache目錄下找不到對(duì)應(yīng)的dex,因此會(huì)拋出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調(diào)用,那么上面的代碼已經(jīng)能滿足需求,不過這里討論的插件框架還需要解決資源訪問和Android系統(tǒng)組件的調(diào)用。
插件內(nèi)系統(tǒng)組件的調(diào)用
Android Framework中包含Activity,Service,Content Provider以及BroadcastReceiver等四大系統(tǒng)組件,這里主要討論如何在主程序中啟動(dòng)插件中的Activity,其它3種組件的調(diào)用方式類似。
大家都知道Activity需要在AndroidManifest.xml中進(jìn)行聲明,apk在安裝的時(shí)候PackageManagerService會(huì)解析apk中的AndroidManifest.xml文件,這時(shí)候就決定了程序包含的哪些Activity,啟動(dòng)未聲明的Activity會(huì)報(bào)ActivityNotFound異常,相信大部分Android開發(fā)者曾經(jīng)都遇到過這個(gè)異常。
啟動(dòng)插件里的Activity必然會(huì)面對(duì)如何在主程序中的AndroidManifest.xml中聲明這個(gè)Activity,然而為了保證插件框架的靈活性,我們是無法預(yù)知插件中有哪些Activity,所以也無法提前聲明。
為了解決上述問題,這里介紹一種基于Proxy思想的解決方法,大致原理是在主程序的AndroidManifest.xml中聲明一些ProxyActivity,啟動(dòng)插件中的Activity會(huì)轉(zhuǎn)為啟動(dòng)主程序中的一個(gè)ProxyActivity,ProxyActivity中所有系統(tǒng)回調(diào)都會(huì)調(diào)用插件Activity中對(duì)應(yīng)的實(shí)現(xiàn),最后的效果就是啟動(dòng)的這個(gè)Activity實(shí)際上是主程序中已經(jīng)聲明的一個(gè)Activity,但是相關(guān)代碼執(zhí)行的卻是插件Activity中的代碼。這就解決了插件Activity未聲明情況下無法啟動(dòng)的問題,從上層來看啟動(dòng)的就是插件中的Activity。下面具體分析整個(gè)過程。
PluginSDK
所有的插件和主程序需要依賴PluginSDK進(jìn)行開發(fā),所有插件中的Activity繼承自PluginSDK中的PluginBaseActivity,PluginBaseActivity繼承自Activity并實(shí)現(xiàn)了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;...@Overridepublic void IInit(String path, Activity context, ClassLoader classLoader) {mProxy = true;mProxyActivity = context;mPluginContext = new PluginContext(context, 0, path, classLoader);attachBaseContext(mPluginContext);}@Overrideprotected void onCreate(Bundle savedInstanceState) {if (mProxy) {mRealActivity = mProxyActivity;} else {super.onCreate(savedInstanceState);mRealActivity = this;}}@Overridepublic void setContentView(int layoutResID) {if (mProxy) {mContentView = LayoutInflater.from(mPluginContext).inflate(layoutResID, null);mRealActivity.setContentView(mContentView);} else {super.setContentView(layoutResID);}}...@Overridepublic void IOnCreate(Bundle savedInstanceState) {onCreate(savedInstanceState);}@Overridepublic void IOnResume() {onResume();}@Overridepublic void IOnStart() {onStart();}@Overridepublic void IOnPause() {onPause();}@Overridepublic void IOnStop() {onStop();}@Overridepublic void IOnDestroy() {onDestroy();}@Overridepublic void IOnRestart() {onRestart();} } public class ProxyActivity extends Activity {IActivity mPluginActivity;@Overrideprotected 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();}}@Overrideprotected void onResume() {super.onResume();if(mPluginActivity != null){mPluginActivity.IOnResume();}}@Overrideprotected 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);}...@Overridepublic 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在整個(gè)插件框架的核心,下面簡單分析一下代碼:
首先看一下ProxyActivity#onResume:
@Override protected void onResume() {super.onResume();if(mPluginActivity != null){mPluginActivity.IOnResume();} }變量mPluginActivity的類型是IActivity,由于插件Activity實(shí)現(xiàn)了IActivity接口,因此可以猜測mPluginActivity.IOnResume()最終執(zhí)行的是插件Activity的onResume中的代碼,下面我們來證實(shí)這種猜測。
PluginBaseActivity實(shí)現(xiàn)了IActivity接口,那么這些接口具體是怎么實(shí)現(xiàn)的呢?看代碼:
@Override public void IOnCreate(Bundle savedInstanceState) {onCreate(savedInstanceState); }@Override public void IOnResume() {onResume(); }@Override public void IOnStart() {onStart(); }@Override public void IOnPause() {onPause(); }...接口實(shí)現(xiàn)非常簡單,只是調(diào)用了和接口對(duì)應(yīng)的回調(diào)函數(shù),那這里的回調(diào)函數(shù)最終會(huì)調(diào)到哪里呢?前面提到過所有插件Activity都會(huì)繼承自PluginBaseActivity,也就是說這里的回調(diào)函數(shù)最終會(huì)調(diào)到插件Activity中對(duì)應(yīng)的回調(diào),比如IOnResume執(zhí)行的是插件Activity中的onResume中的代碼,這也證實(shí)了之前的猜測。
上面的一些代碼片段揭示了插件框架的核心邏輯,其它的代碼更多的是為實(shí)現(xiàn)這種邏輯服務(wù)的,后面會(huì)提供整個(gè)工程的源碼,大家可自行分析理解。
插件內(nèi)資源獲取
實(shí)現(xiàn)加載插件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訪問資源時(shí)使用我們自定義的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來實(shí)現(xiàn)包含插件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; }...總結(jié)
本文介紹了一種基于Proxy思想的插件框架,所有的代碼都在Github中,代碼只是抽取了整個(gè)框架的核心部分,如果要用在生產(chǎn)環(huán)境中還需要完善,比如Content Provider和BroadcastReceiver組件的Proxy類未實(shí)現(xiàn),Activity的Proxy實(shí)現(xiàn)也是不完整的,包括不少回調(diào)都沒有處理。同時(shí)我也無法保證這套框架沒有致命缺陷,本文主要是以總結(jié)、學(xué)習(xí)和交流為目的,歡迎大家一起交流。
原文地址: http://zjmdp.github.io/2014/07/22/a-plugin-framework-for-android/
總結(jié)
以上是生活随笔為你收集整理的基于Proxy思想的Android插件框架的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OS X 内核研究 准备知识
- 下一篇: Android apk动态加载机制的研究