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

时间:2023-03-10 02:50:06
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 = 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的生命周期

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 = -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

延伸阅读

资源加载和activity生命周期管理

基于Proxy思想的Android插件框架