随着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卡路径下即可。