前言
正常的App开发流程基本上是这样的:开发功能–>测试—>上线,上线后发现有大bug,紧急修复—->发新版本—->用户更新—–>bug修复。从发现bug到修复bug花了很长时间。我们希望bug的修复是立马生效的,用户无感知就能自动修复bug。当然,Android端的动态修复bug已经有不少框架了,不过我们今天讲的是另一个话题:Android的插件化。Android插件化有很多好处:热插拔、静默升级、bug动态修复、代码解耦等。正是因为如此,才有越来越多的公司选择插件化。
分析
Android插件化有很多的开源框架,基本上都是两种思路:代理和替换系统的一些关键变量,通过替换这些变量,达到欺骗系统的目的(又称Hook)。代理的思路比较简单,就是通过代理类Proxy,把主工程和插件工程的组件连接起来。代理类相当于傀儡,当主工程想要启动插件工程时,实际上会先调用代理类的相应方法,然后再通过代理类调用插件工程的组件,间接达到调用插件工程组件的目的。为何不直接调用插件工程的组件呢?因为通过DexClassLoader加载到内存的Activity等组件只是一个普通的类,没有上下文环境,意味着拿不到Context,意味着没有生命周期。
让Activity有”生命”
得益于Java语言的类加载器可以动态加载类的特性,在Android中加载一个普通的类是很容易的
1 2 3 4 5
| DexClassLoader Class<?> mClassLaunchActivity = (Class<?>) classLoader.loadClass(mLaunchActivity); mPluginActivity = (IPluginActivity) mClassLaunchActivity.newInstance();
|
通过ClassLoader可以加载一个类,并生成类的实例。但是这个mPluginActivity只是一个普通的类,并没有Activity的生命周期,所以我们需要借助PluginProxyActivity来完成代理工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| 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); 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中。
1 2 3 4 5
| 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,其主要代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
| 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 = 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 = (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的生命周期
1 2 3 4 5 6 7 8 9 10
| 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方法即可。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| 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 = -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 = 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
延伸阅读