博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android插件式开发学习笔记
阅读量:2342 次
发布时间:2019-05-10

本文共 8891 字,大约阅读时间需要 29 分钟。

最近在学习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),插件程序(apk)。其中插件程序分两种,一种需要安装apk的,一种不需要安装的。前者没什么可说的,直接intent启动插件即可,本文主要说不安装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

DexClassLoader这个类的使用过程基本是这样:
  • 通过PacageMangager获得指定的apk的安装的目录,dex的解压缩目录,c/c++库的目录
  • 创建一个 DexClassLoader实例
  • 加载指定的类返回一个Class
  • 然后使用反射调用这个Class
下面是代码部分,代码参考自《Android内核剖析》(作者柯元旦,这本书不错,推荐阅读):
[html] 
  1. @SuppressLint("NewApi") private void useDexClassLoader(){  
  2.     //创建一个意图,用来找到指定的apk  
  3.     Intent intent = new Intent("com.suchangli.android.plugin", null);  
  4.     //获得包管理器  
  5.     PackageManager pm = getPackageManager();  
  6.     List<ResolveInfo> resolveinfoes =  pm.queryIntentActivities(intent, 0);  
  7.     //获得指定的activity的信息  
  8.     ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;  
  9.       
  10.     //获得包名  
  11.     String pacageName = actInfo.packageName;  
  12.     //获得apk的目录或者jar的目录  
  13.     String apkPath = actInfo.applicationInfo.sourceDir;  
  14.     //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己  
  15.     //目录下的文件  
  16.     String dexOutputDir = getApplicationInfo().dataDir;  
  17.       
  18.     //native代码的目录  
  19.     String libPath = actInfo.applicationInfo.nativeLibraryDir;  
  20.     //创建类加载器,把dex加载到虚拟机中  
  21.     DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath,  
  22.             this.getClass().getClassLoader());  
  23.       
  24.     //利用反射调用插件包内的类的方法  
  25.       
  26.     try {  
  27.         Class<?> clazz = calssLoader.loadClass(pacageName+".Plugin1");  
  28.           
  29.         Object obj = clazz.newInstance();  
  30.         Class[] param = new Class[2];  
  31.         param[0] = Integer.TYPE;  
  32.         param[1] = Integer.TYPE;  
  33.           
  34.         Method method = clazz.getMethod("function1", param);  
  35.           
  36.         Integer ret = (Integer)method.invoke(obj, 1,12);  
  37.           
  38.         Log.i("Host", "return result is " + ret);  
  39.           
  40.     } catch (ClassNotFoundException e) {  
  41.         e.printStackTrace();  
  42.     } catch (InstantiationException e) {  
  43.         e.printStackTrace();  
  44.     } catch (IllegalAccessException e) {  
  45.         e.printStackTrace();  
  46.     } catch (NoSuchMethodException e) {  
  47.         e.printStackTrace();  
  48.     } catch (IllegalArgumentException e) {  
  49.         e.printStackTrace();  
  50.     } catch (InvocationTargetException e) {  
  51.         e.printStackTrace();  
  52.     }  
  53.    }  
Plugin1.apk中的一个类:
[html] 
  1. package com.suchangli.plugin1;  
  2.   
  3. public class Plugin1 {  
  4.     public int function1(int a, int b){  
  5.           
  6.         return a+b;  
  7.     }  
  8. }  
Log输出的结果:
这是我试验用的一个demo:

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加载到宿主程序中去执行,就无法通过宿主程序的Context去取到apk中的资源,比如图片、文本等,这是很好理解的,因为apk已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文,用别人的Context是无法得到自己的资源的,不过这个问题貌似可以这么解决:将apk中的资源解压到某个目录,然后通过文件去操作资源,这只是理论上可行,实际上还是会有很多的难点的。
对于已经安装的apk插件,资源访问可以通过createPackageContext获得插件的context,很方便的获取资源。
对于未安装的apk,最原始的办法是解压apk然后手动获取,但太麻烦了。有位大神开发了一款叫dynamic-load-apk的框架,其中使用了重写API的getResource方法把插件apk也加入R.java的方法,很好的解决了该问题,后面有详细说明。
生命周期问题

    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这个方法,尽管它不是典型的生命周期方法,但是它很有用。

[java] 
  1. protected void instantiateLifecircleMethods(Class<?> localClass) {  
  2.     String[] methodNames = new String[] {  
  3.             "onRestart",  
  4.             "onStart",  
  5.             "onResume",  
  6.             "onPause",  
  7.             "onStop",  
  8.             "onDestory"  
  9.     };  
  10.     for (String methodName : methodNames) {  
  11.         Method method = null;  
  12.         try {  
  13.             method = localClass.getDeclaredMethod(methodName, new Class[] { });  
  14.             method.setAccessible(true);  
  15.         } catch (NoSuchMethodException e) {  
  16.             e.printStackTrace();  
  17.         }  
  18.         mActivityLifecircleMethods.put(methodName, method);  
  19.     }  
  20.   
  21.     Method onCreate = null;  
  22.     try {  
  23.         onCreate = localClass.getDeclaredMethod("onCreate"new Class[] { Bundle.class });  
  24.         onCreate.setAccessible(true);  
  25.     } catch (NoSuchMethodException e) {  
  26.         e.printStackTrace();  
  27.     }  
  28.     mActivityLifecircleMethods.put("onCreate", onCreate);  
  29.   
  30.     Method onActivityResult = null;  
  31.     try {  
  32.         onActivityResult = localClass.getDeclaredMethod("onActivityResult",  
  33.                 new Class[] { int.classint.class, Intent.class });  
  34.         onActivityResult.setAccessible(true);  
  35.     } catch (NoSuchMethodException e) {  
  36.         e.printStackTrace();  
  37.     }  
  38.     mActivityLifecircleMethods.put("onActivityResult", onActivityResult);  
  39. }  

其次,同步生命周期,主要看一下onResume和onPause,其他方法是类似的。看如下代码,很好理解,就是当系统调用代理activity生命周期方法的时候,就通过反射去显式调用apk中activity的对应方法。

[java] 
  1. @Override  
  2. protected void onResume() {  
  3.     super.onResume();  
  4.     Method onResume = mActivityLifecircleMethods.get("onResume");  
  5.     if (onResume != null) {  
  6.         try {  
  7.             onResume.invoke(mRemoteActivity, new Object[] { });  
  8.         } catch (Exception e) {  
  9.             e.printStackTrace();  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. @Override  
  15. protected void onPause() {  
  16.     Method onPause = mActivityLifecircleMethods.get("onPause");  
  17.     if (onPause != null) {  
  18.         try {  
  19.             onPause.invoke(mRemoteActivity, new Object[] { });  
  20.         } catch (Exception e) {  
  21.             e.printStackTrace();  
  22.         }  
  23.     }  
  24.     super.onPause();  
  25. }  

最后是发现了一个很好用的框架,详细介绍github里有详细说明,这里附上相关链接。
https://github.com/singwhatiwanna/dynamic-load-apk
http://blog.csdn.net/singwhatiwanna/article/details/39937639

你可能感兴趣的文章
solvepnp三维位姿估算
查看>>
旋转矩阵、欧拉角、四元数理论及其转换关系
查看>>
机器人笛卡尔空间坐标系轨迹规划的研究
查看>>
视觉SLAM——第三章 Eigen几何模块Geometry使用 四元素 欧式变换矩阵
查看>>
半闲居士视觉SLAM十四讲笔记(3)三维空间刚体运动 - part 5 Eigen_Geometry、Pangolin安装
查看>>
程序员的绘图利器 — Gnuplot
查看>>
Ubuntu14.04中安装gnuplot
查看>>
gnuplot 命令解析
查看>>
【C++】 ofstream列对齐和设置小数点精度
查看>>
微信圣诞帽:OpenCV 库Linux下c++实现
查看>>
gnuplot详细操作
查看>>
Ubuntu 下 /etc/resolv.conf文件总是自动清除问题的解决方案
查看>>
c++ 程序中实现抛出异常
查看>>
机器人学导论(一)——空间描述和变换
查看>>
三维矩阵旋转、平移的左乘与右乘分析
查看>>
三维空间中的几何变换-平移旋转缩放
查看>>
百度Apollo开源架构搭建(VMware虚拟机版)
查看>>
百度无人车ApolloAuto使用入门
查看>>
均方根误差(RMSE),平均绝对误差(MAE),标准差(Standard Deviation)的对比
查看>>
方差、标准差和协方差三者之间的定义与计算
查看>>