Android基于代理的插件化思路分析

古之立大事者,不惟有超世之才,亦必有坚忍不拔之志。这篇文章主要讲述Android基于代理的插件化思路分析相关的知识,希望能为你提供帮助。
前言【Android基于代理的插件化思路分析】正常的App开发流程基本上是这样的:开发功能--> 测试---> 上线,上线后发现有大bug,紧急修复----> 发新版本----> 用户更新-----> bug修复。从发现bug到修复bug花了很长时间。我们希望bug的修复是立马生效的,用户无感知就能自动修复bug。当然,android端的动态修复bug已经有不少框架了,不过我们今天讲的是另一个话题:Android的插件化。Android插件化有很多好处:热插拔、静默升级、bug动态修复、代码解耦等。正是因为如此,才有越来越多的公司选择插件化。
分析Android插件化有很多的开源框架,基本上都是两种思路:代理和替换系统的一些关键变量,通过替换这些变量,达到欺骗系统的目的(又称Hook)。代理的思路比较简单,就是通过代理类Proxy,把主工程和插件工程的组件连接起来。代理类相当于傀儡,当主工程想要启动插件工程时,实际上会先调用代理类的相应方法,然后再通过代理类调用插件工程的组件,间接达到调用插件工程组件的目的。为何不直接调用插件工程的组件呢?因为通过DexClassLoader加载到内存的Activity等组件只是一个普通的类,没有上下文环境,意味着拿不到Context,意味着没有生命周期。
让Activity有"生命"
得益于java语言的类加载器可以动态加载类的特性,在Android中加载一个普通的类是很容易的

DexClassLoader Class< ?> mClassLaunchActivity = (Class< ?> ) classLoader.loadClass(mLaunchActivity); mPluginActivity = (IPluginActivity) mClassLaunchActivity.newInstance();

通过ClassLoader可以加载一个类,并生成类的实例。但是这个mPluginActivity只是一个普通的类,并没有Activity的生命周期,所以我们需要借助PluginProxyActivity来完成代理工作
public class PluginProxyActivity extends Activity { IPluginActivity mPluginActivity; String mPluginApkFilePath; String mLaunchActivity; private String mPluginName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle bundle = getIntent().getExtras(); if(bundle == null){ return; } mPluginName = bundle.getString(PluginUtils.PARAM_PLUGIN_NAME); mLaunchActivity = bundle.getString(PluginUtils.PARAM_LAUNCH_ACTIVITY); File pluginFile = PluginUtils.getInstallPath(PluginProxyActivity.this, mPluginName); if(!pluginFile.exists()){ return; } mPluginApkFilePath = pluginFile.getAbsolutePath(); try { initPlugin(); 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; try { PackageManager pm = getPackageManager(); packageInfo = pm.getPackageArchiveInfo(mPluginApkFilePath, PackageManager.GET_ACTIVITIES); } catch (Exception e) { throw e; }ClassLoader classLoader = PluginUtils.getOrCreateClassLoaderByPath(this, mPluginName, mPluginApkFilePath); // get default launchActivity if target Activity is null 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 = (Class< ?> ) classLoader.loadClass(mLaunchActivity); mPluginActivity = (IPluginActivity) mClassLaunchActivity.newInstance(); mPluginActivity.IInit(mPluginApkFilePath, this, classLoader, packageInfo); }protected Class< ? extends PluginProxyActivity> getProxyActivity(String pluginActivityName) { return getClass(); }@Override public void startActivityForResult(Intent intent, int requestCode) { boolean pluginActivity = intent.getBooleanExtra(PluginUtils.PARAM_IS_IN_PLUGIN, false); if (pluginActivity) { String launchActivity = null; ComponentName componentName = intent.getComponent(); if(null != componentName) { launchActivity = componentName.getClassName(); } intent.putExtra(PluginUtils.PARAM_IS_IN_PLUGIN, false); if (launchActivity != null & & launchActivity.length() > 0) { Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity)); pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_NAME, mPluginName); pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_PATH, mPluginApkFilePath); pluginIntent.putExtra(PluginUtils.PARAM_LAUNCH_ACTIVITY, launchActivity); startActivityForResult(pluginIntent, requestCode); } } else { super.startActivityForResult(intent, requestCode); } } }

每次启动新的Activity的时候,都会调用startActivityForResult,在此方法中,进行了Intent的替换,启动的新Activity还是会跳到ProxyActivity中。
Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity)); pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_NAME, mPluginName); pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_PATH, mPluginApkFilePath); pluginIntent.putExtra(PluginUtils.PARAM_LAUNCH_ACTIVITY, launchActivity); startActivityForResult(pluginIntent, requestCode);

所有的插件工程都需要继承于BasePluginActivity,其主要代码如下
public class BasePluginActivity extends Activity implements IPluginActivity {private boolean mIsRunInPlugin; private ClassLoader mDexClassLoader; private Activity mOutActivity; private String mApkFilePath; private PackageInfo mPackageInfo; private PluginContext mContext; private View mContentView; private Activity mActivity; private boolean mFinished; @Override protected void onCreate(Bundle savedInstanceState) { if (mIsRunInPlugin) { mActivity = mOutActivity; } else { super.onCreate(savedInstanceState); mActivity = this; } }@Override public void setContentView(int layoutResID) { if (mIsRunInPlugin) { mContentView = LayoutInflater.from(mContext).inflate(layoutResID, null); mActivity.setContentView(mContentView); } else { super.setContentView(layoutResID); } }@Override public void setContentView(View view) { if (mIsRunInPlugin) { mContentView = view; mActivity.setContentView(mContentView); } else { super.setContentView(view); } }@Override public View findViewById(int id) { if (mIsRunInPlugin & & mContentView != null) { View v = mContentView.findViewById(id); if (null == v) { v = super.findViewById(id); } return v; } else { return super.findViewById(id); } }@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(); }@Override public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) { mIsRunInPlugin = true; mDexClassLoader = classLoader; mOutActivity = context; mApkFilePath = path; mPackageInfo = packageInfo; mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader); attachBaseContext(mContext); }@Override protected void onResume() { if (mIsRunInPlugin) { return; } super.onResume(); }@Override protected void onPause() { if (mIsRunInPlugin) { return; } super.onPause(); }@Override protected void onStart() { if (mIsRunInPlugin) { return; } super.onStart(); }@Override protected void onRestart() { if (mIsRunInPlugin) { return; } super.onRestart(); }@Override protected void onStop() { if (mIsRunInPlugin) { return; } super.onStop(); }@Override protected void onDestroy() { if (mIsRunInPlugin) { mDexClassLoader = null; return; } super.onDestroy(); }@Override public void finish() { if (mIsRunInPlugin) { int resultCode = Activity.RESULT_CANCELED; Intent data = https://www.songbingjia.com/android/null; synchronized (this) { Field field; try { field = Activity.class.getDeclaredField("mResultCode"); field.setAccessible(true); resultCode = (Integer) field.get(this); field = Activity.class.getDeclaredField("mResultData"); field.setAccessible(true); data = https://www.songbingjia.com/android/(Intent) field.get(this); } catch (Exception e) { } } mOutActivity.setResult(resultCode, data); mOutActivity.finish(); mFinished = true; } else { super.finish(); } }@Override public boolean isFinishing() { if (mIsRunInPlugin) { return mFinished; } else { return super.isFinishing(); } }@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (mIsRunInPlugin) { return; } else { super.onActivityResult(requestCode, resultCode, data); } }@Override public LayoutInflater getLayoutInflater() { if (mContext != null) { return LayoutInflater.from(mContext); } else { return LayoutInflater.from(mActivity); } }@Override public WindowManager getWindowManager() { if (mIsRunInPlugin) { return mOutActivity.getWindowManager(); } else { return super.getWindowManager(); } }@Override public void startActivityForResult(Intent intent, int requestCode) { if (mIsRunInPlugin) { intent.putExtra(PluginUtils.PARAM_IS_IN_PLUGIN, true); mActivity.startActivityForResult(intent, requestCode); } else { super.startActivityForResult(intent, requestCode); } } }

在BasePluginActivity中,覆写了很多父类Activity的方法,用来判断当前Activity是独立运行还是作为插件运行,如果是在插件中运行,则是调用插件中设置进来的代理类ProxyActivity(mActivity)的相应方法。同时使用IPluginActivity来模拟Activity的生命周期
public interface IPluginActivity { 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); }

至此Activity已经有生命周期了!但是还有个问题,插件的资源如何获取?
插件资源的获取
这里就不卖关子了。直接通过反射调用AssetManager的addAssetPath方法把资源加载到Resource对象中,即可获取插件中的资源。为了后续方便,我们直接继承ContextWrapper类自己实现getAssets和getResources方法即可。代码如下
class PluginContext extends ContextWrapper {private AssetManager mAsset; private Resources mResources; private Theme mTheme; private int mThemeResId; private ClassLoader mClassLoader; private Context mOutContext; 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; }private Resources getSelfRes(Context ctx, AssetManager selfAsset) { DisplayMetrics metrics = ctx.getResources().getDisplayMetrics(); Configuration con = ctx.getResources().getConfiguration(); return new Resources(selfAsset, metrics, con); }private Theme getSelfTheme(Resources selfResources) { Theme theme = selfResources.newTheme(); mThemeResId = getInnerRIdValue("com.android.internal.R.style.Theme"); theme.applyStyle(mThemeResId, true); return theme; }private int getInnerRIdValue(String rStrnig) { int value = https://www.songbingjia.com/android/-1; try { int rindex = rStrnig.indexOf(".R."); String Rpath = rStrnig.substring(0, rindex + 2); int fieldIndex = rStrnig.lastIndexOf("."); String fieldName = rStrnig.substring(fieldIndex + 1, rStrnig.length()); rStrnig = rStrnig.substring(0, fieldIndex); String type = rStrnig.substring(rStrnig.lastIndexOf(".") + 1, rStrnig.length()); String className = Rpath + "$" + type; Class< ?> cls = Class.forName(className); value = https://www.songbingjia.com/android/cls.getDeclaredField(fieldName).getInt(null); } catch (Throwable e) { e.printStackTrace(); } return value; }public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) { super(base, themeres); mClassLoader = classLoader; mAsset = getSelfAssets(apkPath); mResources = getSelfRes(base, mAsset); mTheme = getSelfTheme(mResources); mOutContext = base; }@Override public Resources getResources() { return mResources; }@Override public AssetManager getAssets() { return mAsset; }@Override public Theme getTheme() { return mTheme; } }

至此,插件化的两大难题已经解决。现在插件中的Activity可以启动了!用同样的思路可以完成其他组件的代码编写。
源码下载PluginDemo
延伸阅读资源加载和activity生命周期管理
基于Proxy思想的Android插件框架


    推荐阅读