隨着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中。也即是說,只要我們自己實現這兩個方法,就可以解決資源問題了。到這裏基本主程序代碼完成了。
這是主程序的代碼地址,裏面我寫了一個插件APK下載地址,想自己加載插件的小夥伴可以把apkPath改了即可點擊打開鏈接
插件Activity我寫了一個基類供大家參考
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卡路徑下即可。插件下載地址
由於加載的插件是掃描驅動,所以需要把插件中的掃描結果傳到主程序中,我想過用一個service去傳遞,但是時間不夠,暫時用存在sharedpreferences中,有好想法的小夥伴可以留言或者私聊我哦。