随着android技术不断更新,app插件化也逐渐成为焦点。本人在上海某公司做物流产品,用到很多扫描驱动。近期应老大需求,要求我们把扫描做成插件化,让用户下载并动态加载。上网上看了一番,发现都是通过classloader通过反射机制去加载jar/dex/apk中类的方法。类加载器(class loader)把类的字节代码加载到Java虚拟机中。虽然这种方法可以很轻松的加载任意代码,但是我们发现如果要是去启动一个activity就会报错,我们发现了俩个问题,一是res资源文件是无法正常加载的,二是activity的重中之重,它没有自己的context上下文对象,同时它对于加载器来说也只是一个class,没有自己的生命周期。所以衍生出代理activity。
本文主要讲动态加载插件apk,下面是android插件化的基本原理:
- 利用DexClassLoader来实现动态加载插件中的class。
- 通过反射替换ContextImpl中的mResources,mPackageInfo,并替换插件Activity中的相关属性,来实现加载插件中的资源文件。
- 通过启动代理Activity(ProxyActivity)来代替插件Activity,也就是说一个ProxyActivity对应一个插件Activity。
- 插件中的Activity的生命周期反射到宿主中的代理Activity。
直接上源码来看是如何插件化的
首先是在主程序(宿主程序)mainfest.xml文件中添加一个代理Activity
<activity android:name="com.example.host.activity.PlugProxyActivity" >
<action android:name="com.example.host.activity.Intent" />
<category android:name="android.intent.category.DEFAULT"></category>
</activity>
在需要插件化的地方跳转到我们的代理Activity
Intent intent = new Intent(this, PlugProxyActivity.class);
intent.putExtra(PlugProxyActivity.KEY_APK,getFilesPath() + File.separator +"plug.apk");
startActivity(intent);
下面是我的代理Activity的代码
public class PlugProxyActivity extends BaseRes {
private Object pluginActivity;
private Class<?> pluginClass;
private HashMap<String, Method> methodMap = new HashMap<String,Method>();
private SharedPreferences sharedPreferences;
private static PCallback callback;
private final int FROM_EXTERNAL = 0;
private final String FROM = "extra.from";
private final String VIEW_ACTION="com.example.dynamic.activity.Intent";
public static final String KEY_APK = "path.apk";
public static final String KEY_CLASS = "name.class";
public static final String KEY_SCAN_MSG = "scanMsg";
private String className;
private String apkPath;
private String dexOutputPath;
private final String TAG="PlugProxy";
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
apkPath =getIntent().getStringExtra(KEY_APK);
className=getIntent().getStringExtra(KEY_CLASS);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
try {
DexClassLoader loader = initClassLoader();
pluginClass = loader.loadClass(className);
Constructor<?> localConstructor = pluginClass.getConstructor(new Class[] {});
pluginActivity = localConstructor.newInstance(new Object[] {});
transmitMsg();
Bundle bundle = new Bundle();
bundle.putInt(FROM, FROM_EXTERNAL);
executeMethod("onCreate",Bundle.class,bundle );
} catch (Exception e) {
Log.i(TAG, "load activity error:"+Log.getStackTraceString(e));
}
}
public static void scan(PCallback pCallback){
callback=pCallback;
}
private void transmitMsg(){
executeMethod("setProxy",Activity.class,this );
executeMethod("setDexPath",String.class,dexOutputPath );
executeMethod("setProxyViewAction",String.class,VIEW_ACTION );
executeMethod("setKeyScanMsg",String.class,KEY_SCAN_MSG );
}
@SuppressLint("NewApi")
private DexClassLoader initClassLoader(){
File dexOutputDir = this.getDir("dex", 0);
dexOutputPath = dexOutputDir.getAbsolutePath();
loadResources(apkPath);
DexClassLoader loader = new DexClassLoader(apkPath, dexOutputPath,null , getClass().getClassLoader());
return loader;
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i(TAG, "proxy onDestroy");
executeMethod("onDestroy");
callback.scan(sharedPreferences.getString("scan",null));
}
@Override
protected void onPause() {
super.onPause();
Log.i(TAG, "proxy onPause");
executeMethod("onPause");
}
@Override
protected void onResume() {
super.onResume();
Log.i(TAG, "proxy onResume");
executeMethod("onResume");
}
@Override
protected void onStart() {
super.onStart();
Log.i(TAG, "proxy onStart");
executeMethod("onStart");
}
@Override
protected void onStop() {
super.onStop();
Log.i(TAG, "proxy onStop");
executeMethod("onStop");
}
/**
* 无formalParameter
* @param methodName
*/
private void executeMethod(String methodName){
executeMethod(methodName,new Class[]{},new Object[]{});
}
/**
* 单formalParameter
* @param methodName
* @param formalParameter
* @param actualParameter
*/
private void executeMethod(String methodName,Class formalParameter,Object actualParameter){
executeMethod(methodName,new Class[]{formalParameter},new Object[]{actualParameter});
}
/**
* 多formalParameter
* @param methodName
* @param formalParameter
* @param actualParameter
*/
private void executeMethod(String methodName,Class[] formalParameter,Object[] actualParameter){
Method method = null;
try {
method = pluginClass.getMethod(methodName,formalParameter);
method.setAccessible(true);
method.invoke(pluginActivity, actualParameter);
} catch (NoSuchMethodException e) {
Log.d(TAG, "NoSuchMethodException:"+Log.getStackTraceString(e));
} catch (InvocationTargetException e) {
Log.d(TAG, "InvocationTargetException:"+Log.getStackTraceString(e));
} catch (IllegalAccessException e) {
Log.d(TAG, "IllegalAccessException:"+Log.getStackTraceString(e));
}
}
先将对应的路径下的插件APK通过实例DexLodaerClass导出dex文件,再从dex文件中通过loadClass()方法找到编译好的class,我封装了一个executeMethod()的方法用来反射查找class中的函数。看代码可知有一个反射函数setProxy(),这个方法就是把代理activity的context对象传传入插件中,我又通过反射在代理Activity的生命周期去变相调用插件的Activity。
Ok,到这基本class已经完全反射到代理上了,那么如何获得apk中的资源文件呢。加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。下面是资源代码
protected void loadResources(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
mAssetManager = assetManager;
} catch (Exception e) {
Log.i("inject", "loadResource error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
Resources superRes = super.getResources();
superRes.getDisplayMetrics();
superRes.getConfiguration();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}
@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
@Override
public Theme getTheme() {
return mTheme == null ? super.getTheme() : mTheme;
}
我们知道,activity的工作主要是由ContextImpl来完成的, 它在activity中是一个叫做mBase的成员变量。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上context就是通过它们来获取资源的,这两个抽象方法的真正实现在ContextImpl中。也即是说,只要我们自己实现这两个方法,就可以解决资源问题了。到这里基本主程序代码完成了。
public class BaseActivity extends Activity {
protected Activity mProxyActivity;
public static final String FROM = "extra.from";
public static final String EXTRA_DEX_PATH = "extra.dex.path";
public static final String EXTRA_CLASS = "extra.class";
private String PROXY_VIEW_ACTION;
private String DEX_PATH;
protected String KEY_SCAN_MSG;
public static final int FROM_EXTERNAL = 0;
public static final int FROM_INTERNAL = 1;
protected int mFrom = FROM_INTERNAL;
protected boolean isProxy=true;
public void setProxy(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
public void setDexPath(String DEX_PATH){this.DEX_PATH=DEX_PATH;}
public void setProxyViewAction(String PROXY_VIEW_ACTION){this.PROXY_VIEW_ACTION=PROXY_VIEW_ACTION;}
public void setKeyScanMsg(String KEY_SCAN_MSG){this.KEY_SCAN_MSG=KEY_SCAN_MSG;}
@Override
protected void onCreate(Bundle savedInstanceState) {
// if (savedInstanceState != null) {
// mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);
// }
// if (mFrom != FROM_EXTERNAL) {
// super.onCreate(savedInstanceState);
// mProxyActivity = this;
// isProxy=false;
// }
if (!isProxy){
mProxyActivity=this;
super.onCreate(savedInstanceState);
}
}
@Override
public void setContentView(int layoutResID) {
if (!isProxy){
super.setContentView(layoutResID);
}else {
if (mProxyActivity != null && mProxyActivity instanceof Activity)
mProxyActivity.setContentView(layoutResID);
}
}
protected void startActivityByProxy(String className) {
if (mProxyActivity == this) {
Intent intent = new Intent();
intent.setClassName(this, className);
this.startActivity(intent);
} else {
Intent intent = new Intent(PROXY_VIEW_ACTION);
intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);
intent.putExtra(EXTRA_CLASS, className);
mProxyActivity.startActivity(intent);
}
}
@Override
protected void onResume() {
if (!isProxy)
super.onResume();
}
@Override
protected void onStart() {
if (!isProxy)
super.onStart();
}
@Override
protected void onPause() {
if (!isProxy)
super.onPause();
}
@Override
protected void onRestart() {
if (!isProxy)
super.onRestart();
}
@Override
protected void onStop() {
if (!isProxy)
super.onStop();
}
@Override
protected void onDestroy() {
if (!isProxy)
super.onDestroy();
}
}
因为插件中的Activity在实际中已经没有生命周期的意义,如果编译成APK让宿主程序去加载会报错,原因是插件中不能有super方法,它对于程序来说只是一个简单的类不能调用activity的任何方法,所以我加了一个判断isProxy,想直接运行插件APK的小伙伴可以把isProxy改成false即可,打包成插件APK时再改成true。打包好的插件APK放在手机对应的SD卡路径下即可。