android插件化 動態加載apk

隨着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中,有好想法的小夥伴可以留言或者私聊我哦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章