最近在学习android下的插件式开发,做个笔记整理一下。
参考链接:
http://blog.csdn.net/singwhatiwanna/article/details/22597587
http://blog.csdn.net/singwhatiwanna/article/details/23387079
http://blog.csdn.net/bboyfeiyu/article/details/11710497
http://blog.csdn.net/com360/article/details/14125683
http://www.cnblogs.com/LittleRedPoint/p/3429709.html
http://blog.csdn.net/wenchao126/article/details/41216513
http://blog.zhourunsheng.com/2011/09/%E6%8E%A2%E7%A7%98%E8%85%BE%E8%AE%AFandroid%E6%89%8B%E6%9C%BA%E6%B8%B8%E6%88%8F%E5%B9%B3%E5%8F%B0%E4%B9%8B%E4%B8%8D%E5%AE%89%E8%A3%85%E6%B8%B8%E6%88%8Fapk%E7%9B%B4%E6%8E%A5%E5%90%AF%E5%8A%A8%E6%B3%95/
概念
许多平台化的应用,诸如支付宝钱包和微信,为了方便的扩展功能,都采用了插件式开发的思想。
主要包含如下几个意思:
- 插件不能独立运行,必须运行一个宿主程序中,宿主程序去调用插件。
- 插件一般情况下可以独立安装,android中就可以设计一个apk。
- 宿主程序中可以管理插件,比如添加,删除,禁用等。
- 宿主程序应该保证插件向下兼容,新的宿主程序应该兼容老的插件。
- 插件apk中类的加载,方法的调用。
- 资源访问问题,plugin的上下文已经不存在,无法直接调用,可以考虑文件读写。
- activity生命周期,plguin里面的activity对宿主程序来说只是普通的类,生命周期不受系统控制,可以考虑手动在宿主activity里调用plugin的activity。
关于动态加载apk,理论上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader。
DexClassLoader :可以加载文件系统上的jar、dex、apk
PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用,尽管还有这个类
关于jar、dex和apk,dex和apk是可以直接加载的,因为它们都是或者内部有dex文件,而原始的jar是不行的,必须转换成dalvik所能识别的字节码文件,转换工具可以使用android sdk中platform-tools目录下的dx
转换命令 :dx --dex --output=dest.jar src.jar
- 通过PacageMangager获得指定的apk的安装的目录,dex的解压缩目录,c/c++库的目录
- 创建一个 DexClassLoader实例
- 加载指定的类返回一个Class
- 然后使用反射调用这个Class
- @SuppressLint("NewApi") private void useDexClassLoader(){
- //创建一个意图,用来找到指定的apk
- Intent intent = new Intent("com.suchangli.android.plugin", null);
- //获得包管理器
- PackageManager pm = getPackageManager();
- List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
- //获得指定的activity的信息
- ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
- //获得包名
- String pacageName = actInfo.packageName;
- //获得apk的目录或者jar的目录
- String apkPath = actInfo.applicationInfo.sourceDir;
- //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己
- //目录下的文件
- String dexOutputDir = getApplicationInfo().dataDir;
- //native代码的目录
- String libPath = actInfo.applicationInfo.nativeLibraryDir;
- //创建类加载器,把dex加载到虚拟机中
- DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath,
- this.getClass().getClassLoader());
- //利用反射调用插件包内的类的方法
- try {
- Class<?> clazz = calssLoader.loadClass(pacageName+".Plugin1");
- Object obj = clazz.newInstance();
- Class[] param = new Class[2];
- param[0] = Integer.TYPE;
- param[1] = Integer.TYPE;
- Method method = clazz.getMethod("function1", param);
- Integer ret = (Integer)method.invoke(obj, 1,12);
- Log.i("Host", "return result is " + ret);
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- } catch (InstantiationException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- } catch (InvocationTargetException e) {
- e.printStackTrace();
- }
- }
- package com.suchangli.plugin1;
- public class Plugin1 {
- public int function1(int a, int b){
- return a+b;
- }
- }
1.宿主程序中调用插件的方法
/**
* 加载插件
*/
private void loadPlugin() {
String dexPath = Environment.getExternalStorageDirectory()
+ "/TestB.apk";
//必须通过这种方法建立一个受保护的目录,这个目录在/data/data/该程序下建立,目的是为了防止代码注入攻击
String optimizedDirectory = getApplicationContext().getDir("dex_dir",
MODE_PRIVATE).getAbsolutePath();
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// 建立dex类加载器
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
optimizedDirectory, null, classLoader);
// 通过apk获取package信息, 1是 GET_ACTIVITIES
PackageInfo packInfo = getPackageManager().getPackageArchiveInfo(
dexPath, 1);
if (packInfo != null) {
if (packInfo.activities != null) {
String actName = packInfo.activities[0].name;
Log.d(TAG, "activityname = " + actName);
try {
Class<?> actClass = dexClassLoader.loadClass(actName);
Constructor<?> actConstructor = actClass
.getConstructor(new Class[] {});
Object act = actConstructor.newInstance(new Object[] {});
Log.d(TAG, "activity = " + act);
// 调用一个弹toast的方法
Method method = actClass.getDeclaredMethod("getToast",
new Class[] { Activity.class });
method.setAccessible(true);
method.invoke(act, new Object[] { this });
// 传递给插件自己的引用
Method localMethodSetActivity = actClass.getDeclaredMethod(
"setActivity", new Class[] { Activity.class });
localMethodSetActivity.setAccessible(true);
localMethodSetActivity.invoke(act, new Object[] { this });
//启动插件oncreate
Bundle paramBundle = new Bundle();
Method methodonCreate = actClass.getDeclaredMethod(
"onCreate", new Class[] { Bundle.class });
methodonCreate.setAccessible(true);
methodonCreate.invoke(act, new Object[] { paramBundle });
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} else {
Log.d(TAG, "activityname =null dexpath:" + dexPath);
}
}
插件activity
public class PlugActivity extends Activity {
private static final String TAG = "PlugActivity";
private Activity otherActivity;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate..." + savedInstanceState);
super.onCreate(savedInstanceState);
//其实是偷梁换柱,用宿主程序加载的。
otherActivity.setContentView(new TextView(otherActivity));
}
public void setActivity(Activity paramActivity) {
Log.d(TAG, "setActivity..." + paramActivity);
this.otherActivity = paramActivity;
}
public void getToast(Activity paramActivity) {
Log.d(TAG, "getToast..." + paramActivity);
Toast.makeText(paramActivity, "testB toast", Toast.LENGTH_SHORT).show();
}
}
资源访问问题
apk被宿主程序调起以后,apk中的activity其实就是一个普通的对象,不具有activity的性质,因为系统启动activity是要做很多初始化工作的,而我们在应用层通过反射去启动activity是很难完成系统所做的初始化工作的,所以activity的大部分特性都无法使用包括activity的生命周期管理,这就需要我们自己去管理。谈到activity生命周期,其实就是那几个常见的方法:onCreate、onStart、onResume、onPause等,由于apk中的activity不是真正意义上的activity(没有在宿主程序中注册且没有完全初始化),所以这几个生命周期的方法系统就不会去自动调用了。针对此类问题,采用Fragment是一个不错的方法,Fragment从3.0引入,通过support-v4包,可以兼容3.0以下的android版本。Fragment既有类似于Activity的生命周期,又有类似于View的界面,将Fragment加入到Activity中,activity会自动管理Fragment的生命周期,通过第一篇文章我们知道,apk中的activity是通过宿主程序中的代理activity启动的,将Fragment加入到代理activity内部,其生命周期将完全由代理activity来管理,但是采用这种方法,就要求apk尽量采用Fragment来实现,还有就是在做页面跳转的时候有点麻烦,当然关于Fragment相关的内容我将在后面再做研究,本文不采用Fragment而是通过反射去手动管理activity的生命周期。
我们要在代理activity中去反射apk中activity的所有生命周期的方法,然后将activity的生命周期和代理activity的生命周期进行同步。首先,反射activity生命周期的所有方法,还反射了onActivityResult这个方法,尽管它不是典型的生命周期方法,但是它很有用。
- protected void instantiateLifecircleMethods(Class<?> localClass) {
- String[] methodNames = new String[] {
- "onRestart",
- "onStart",
- "onResume",
- "onPause",
- "onStop",
- "onDestory"
- };
- for (String methodName : methodNames) {
- Method method = null;
- try {
- method = localClass.getDeclaredMethod(methodName, new Class[] { });
- method.setAccessible(true);
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- }
- mActivityLifecircleMethods.put(methodName, method);
- }
- Method onCreate = null;
- try {
- onCreate = localClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class });
- onCreate.setAccessible(true);
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- }
- mActivityLifecircleMethods.put("onCreate", onCreate);
- Method onActivityResult = null;
- try {
- onActivityResult = localClass.getDeclaredMethod("onActivityResult",
- new Class[] { int.class, int.class, Intent.class });
- onActivityResult.setAccessible(true);
- } catch (NoSuchMethodException e) {
- e.printStackTrace();
- }
- mActivityLifecircleMethods.put("onActivityResult", onActivityResult);
- }
其次,同步生命周期,主要看一下onResume和onPause,其他方法是类似的。看如下代码,很好理解,就是当系统调用代理activity生命周期方法的时候,就通过反射去显式调用apk中activity的对应方法。
- @Override
- protected void onResume() {
- super.onResume();
- Method onResume = mActivityLifecircleMethods.get("onResume");
- if (onResume != null) {
- try {
- onResume.invoke(mRemoteActivity, new Object[] { });
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- @Override
- protected void onPause() {
- Method onPause = mActivityLifecircleMethods.get("onPause");
- if (onPause != null) {
- try {
- onPause.invoke(mRemoteActivity, new Object[] { });
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- super.onPause();
- }
最后是发现了一个很好用的框架,详细介绍github里有详细说明,这里附上相关链接。