Android之Android apk动态加载机制的研究

转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/22597587(来自singwhatiwanna的csdn博客)

背景

问题是这样的:我们知道,apk必须安装才能运行,如果不安装要是也能运行该多好啊,事实上,这不是完全不可能的,尽管它比较难实现。在理论层面上,我们可以通过一个宿主程序来运行一些未安装的apk,当然,实践层面上也能实现,不过这对未安装的apk有要求。我们的想法是这样的,首先要明白apk未安装是不能被直接调起来的,但是我们可以采用一个程序(称之为宿主程序)去动态加载apk文件并将其放在自己的进程中执行,本文要介绍的就是这么一种方法,同时这种方法还有很多问题,尤其是资源的访问。因为将apk加载到宿主程序中去执行,就无法通过宿主程序的Context去取到apk中的资源,比如图片、文本等,这是很好理解的,因为apk已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文,用别人的Context是无法得到自己的资源的,不过这个问题貌似可以这么解决:将apk中的资源解压到某个目录,然后通过文件去操作资源,这只是理论上可行,实际上还是会有很多的难点的。除了资源存取的问题,还有一个问题是activity的生命周期,因为apk被宿主程序加载执行后,它的activity其实就是一个普通的类,正常情况下,activity的生命周期是由系统来管理的,现在被宿主程序接管了以后,如何替代系统对apk中的activity的生命周期进行管理是有难度的,不过这个问题比资源的访问好解决一些,比如我们可以在宿主程序中模拟activity的生命周期并合适地调用apk中activity的生命周期方法。本文暂时不对这两个问题进行解决,因为很难,本文仅仅对apk的动态执行机制进行介绍,尽管如此,听起来还是有点小激动,不是吗?

工作原理

如下图所示,首先宿主程序会到文件系统比如sd卡去加载apk,然后通过一个叫做proxy的activity去执行apk中的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

android动态加载apk_第1张图片

示例

宿主程序的实现

1. 主界面很简单,放了一个button,点击就会调起apk,我把apk直接放在了sd卡中,至于先把apk从网上下载到本地再加载其实是一个道理。

  1. @Override
  2. publicvoidonClick(Viewv){
  3. if(v==mOpenClient){
  4. Intentintent=newIntent(this,ProxyActivity.class);
  5. intent.putExtra(ProxyActivity.EXTRA_DEX_PATH,"/mnt/sdcard/DynamicLoadHost/plugin.apk");
  6. startActivity(intent);
  7. }
  8. }

点击button以后,proxy会被调起,然后加载apk并调起的任务就交给它了

2. 代理activity的实现(proxy)

  1. packagecom.ryg.dynamicloadhost;
  2. importjava.lang.reflect.Constructor;
  3. importjava.lang.reflect.Method;
  4. importdalvik.system.DexClassLoader;
  5. importandroid.annotation.SuppressLint;
  6. importandroid.app.Activity;
  7. importandroid.content.pm.PackageInfo;
  8. importandroid.os.Bundle;
  9. importandroid.util.Log;
  10. publicclassProxyActivityextendsActivity{
  11. privatestaticfinalStringTAG="ProxyActivity";
  12. publicstaticfinalStringFROM="extra.from";
  13. publicstaticfinalintFROM_EXTERNAL=0;
  14. publicstaticfinalintFROM_INTERNAL=1;
  15. publicstaticfinalStringEXTRA_DEX_PATH="extra.dex.path";
  16. publicstaticfinalStringEXTRA_CLASS="extra.class";
  17. privateStringmClass;
  18. privateStringmDexPath;
  19. @Override
  20. protectedvoidonCreate(BundlesavedInstanceState){
  21. super.onCreate(savedInstanceState);
  22. mDexPath=getIntent().getStringExtra(EXTRA_DEX_PATH);
  23. mClass=getIntent().getStringExtra(EXTRA_CLASS);
  24. Log.d(TAG,"mClass="+mClass+"mDexPath="+mDexPath);
  25. if(mClass==null){
  26. launchTargetActivity();
  27. }else{
  28. launchTargetActivity(mClass);
  29. }
  30. }
  31. @SuppressLint("NewApi")
  32. protectedvoidlaunchTargetActivity(){
  33. PackageInfopackageInfo=getPackageManager().getPackageArchiveInfo(
  34. mDexPath,1);
  35. if((packageInfo.activities!=null)
  36. &&(packageInfo.activities.length>0)){
  37. StringactivityName=packageInfo.activities[0].name;
  38. mClass=activityName;
  39. launchTargetActivity(mClass);
  40. }
  41. }
  42. @SuppressLint("NewApi")
  43. protectedvoidlaunchTargetActivity(finalStringclassName){
  44. Log.d(TAG,"startlaunchTargetActivity,className="+className);
  45. FiledexOutputDir=this.getDir("dex",0);
  46. finalStringdexOutputPath=dexOutputDir.getAbsolutePath();
  47. ClassLoaderlocalClassLoader=ClassLoader.getSystemClassLoader();
  48. DexClassLoaderdexClassLoader=newDexClassLoader(mDexPath,
  49. dexOutputPath,null,localClassLoader);
  50. try{
  51. Class<?>localClass=dexClassLoader.loadClass(className);
  52. Constructor<?>localConstructor=localClass
  53. .getConstructor(newClass[]{});
  54. Objectinstance=localConstructor.newInstance(newObject[]{});
  55. Log.d(TAG,"instance="+instance);
  56. MethodsetProxy=localClass.getMethod("setProxy",
  57. newClass[]{Activity.class});
  58. setProxy.setAccessible(true);
  59. setProxy.invoke(instance,newObject[]{this});
  60. MethodonCreate=localClass.getDeclaredMethod("onCreate",
  61. newClass[]{Bundle.class});
  62. onCreate.setAccessible(true);
  63. Bundlebundle=newBundle();
  64. bundle.putInt(FROM,FROM_EXTERNAL);
  65. onCreate.invoke(instance,newObject[]{bundle});
  66. }catch(Exceptione){
  67. e.printStackTrace();
  68. }
  69. }
  70. }

说明:程序不难理解,思路是这样的:采用DexClassLoader去加载apk,然后如果没有指定class,就调起主activity,否则调起指定的class。activity被调起的过程是这样的:首先通过类加载器去加载apk中activity的类并创建一个新对象,然后通过反射去调用这个对象的setProxy方法和onCreate方法,setProxy方法的作用是将activity内部的执行全部交由宿主程序中的proxy(也是一个activity),onCreate方法是activity的入口,setProxy以后就调用onCreate方法,这个时候activity就被调起来了。

待执行apk的实现

1. 为了让proxy全面接管apk中所有activity的执行,需要为activity定义一个基类BaseActivity,在基类中处理代理相关的事情,同时BaseActivity还对是否使用代理进行了判断,如果不使用代理,那么activity的逻辑仍然按照正常的方式执行,也就是说,这个apk既可以按照执行,也可以由宿主程序来执行。

  1. packagecom.ryg.dynamicloadclient;
  2. importandroid.app.Activity;
  3. importandroid.content.Intent;
  4. importandroid.os.Bundle;
  5. importandroid.util.Log;
  6. importandroid.view.View;
  7. importandroid.view.ViewGroup.LayoutParams;
  8. publicclassBaseActivityextendsActivity{
  9. privatestaticfinalStringTAG="Client-BaseActivity";
  10. publicstaticfinalStringFROM="extra.from";
  11. publicstaticfinalintFROM_EXTERNAL=0;
  12. publicstaticfinalintFROM_INTERNAL=1;
  13. publicstaticfinalStringEXTRA_DEX_PATH="extra.dex.path";
  14. publicstaticfinalStringEXTRA_CLASS="extra.class";
  15. publicstaticfinalStringPROXY_VIEW_ACTION="com.ryg.dynamicloadhost.VIEW";
  16. publicstaticfinalStringDEX_PATH="/mnt/sdcard/DynamicLoadHost/plugin.apk";
  17. protectedActivitymProxyActivity;
  18. protectedintmFrom=FROM_INTERNAL;
  19. publicvoidsetProxy(ActivityproxyActivity){
  20. Log.d(TAG,"setProxy:proxyActivity="+proxyActivity);
  21. mProxyActivity=proxyActivity;
  22. }
  23. @Override
  24. protectedvoidonCreate(BundlesavedInstanceState){
  25. if(savedInstanceState!=null){
  26. mFrom=savedInstanceState.getInt(FROM,FROM_INTERNAL);
  27. }
  28. if(mFrom==FROM_INTERNAL){
  29. super.onCreate(savedInstanceState);
  30. mProxyActivity=this;
  31. }
  32. Log.d(TAG,"onCreate:from="+mFrom);
  33. }
  34. protectedvoidstartActivityByProxy(StringclassName){
  35. if(mProxyActivity==this){
  36. Intentintent=newIntent();
  37. intent.setClassName(this,className);
  38. this.startActivity(intent);
  39. }else{
  40. Intentintent=newIntent(PROXY_VIEW_ACTION);
  41. intent.putExtra(EXTRA_DEX_PATH,DEX_PATH);
  42. intent.putExtra(EXTRA_CLASS,className);
  43. mProxyActivity.startActivity(intent);
  44. }
  45. }
  46. @Override
  47. publicvoidsetContentView(Viewview){
  48. if(mProxyActivity==this){
  49. super.setContentView(view);
  50. }else{
  51. mProxyActivity.setContentView(view);
  52. }
  53. }
  54. @Override
  55. publicvoidsetContentView(Viewview,LayoutParamsparams){
  56. if(mProxyActivity==this){
  57. super.setContentView(view,params);
  58. }else{
  59. mProxyActivity.setContentView(view,params);
  60. }
  61. }
  62. @Deprecated
  63. @Override
  64. publicvoidsetContentView(intlayoutResID){
  65. if(mProxyActivity==this){
  66. super.setContentView(layoutResID);
  67. }else{
  68. mProxyActivity.setContentView(layoutResID);
  69. }
  70. }
  71. @Override
  72. publicvoidaddContentView(Viewview,LayoutParamsparams){
  73. if(mProxyActivity==this){
  74. super.addContentView(view,params);
  75. }else{
  76. mProxyActivity.addContentView(view,params);
  77. }
  78. }
  79. }

说明:相信大家一看代码就明白了,其中setProxy方法的作用就是为了让宿主程序能够接管自己的执行,一旦被接管以后,其所有的执行均通过proxy,且Context也变成了宿主程序的Context,也许这么说比较形象:宿主程序其实就是个空壳,它只是把其它apk加载到自己的内部去执行,这也就更能理解为什么资源访问变得很困难,你会发现好像访问不到apk中的资源了,的确是这样的,但是目前我还没有很好的方法去解决。
2. 入口activity的实现

  1. publicclassMainActivityextendsBaseActivity{
  2. privatestaticfinalStringTAG="Client-MainActivity";
  3. @Override
  4. protectedvoidonCreate(BundlesavedInstanceState){
  5. super.onCreate(savedInstanceState);
  6. initView(savedInstanceState);
  7. }
  8. privatevoidinitView(BundlesavedInstanceState){
  9. mProxyActivity.setContentView(generateContentView(mProxyActivity));
  10. }
  11. privateViewgenerateContentView(finalContextcontext){
  12. LinearLayoutlayout=newLinearLayout(context);
  13. layout.setLayoutParams(newLayoutParams(LayoutParams.MATCH_PARENT,
  14. LayoutParams.MATCH_PARENT));
  15. layout.setBackgroundColor(Color.parseColor("#F79AB5"));
  16. Buttonbutton=newButton(context);
  17. button.setText("button");
  18. layout.addView(button,LayoutParams.MATCH_PARENT,
  19. LayoutParams.WRAP_CONTENT);
  20. button.setOnClickListener(newOnClickListener(){
  21. @Override
  22. publicvoidonClick(Viewv){
  23. Toast.makeText(context,"youclickedbutton",
  24. Toast.LENGTH_SHORT).show();
  25. startActivityByProxy("com.ryg.dynamicloadclient.TestActivity");
  26. }
  27. });
  28. returnlayout;
  29. }
  30. }

说明:由于访问不到apk中的资源了,所以界面是代码写的,而不是写在xml中,因为xml读不到了,这也是个大问题。注意到主界面中有一个button,点击后跳到了另一个activity,这个时候是不能直接调用系统的startActivity方法的,而是必须通过宿主程序中的proxy来执行,原因很简单,首先apk本书没有Context,所以它无法调起activity,另外由于这个子activity是apk中的,通过宿主程序直接调用它也是不行的,因为它对宿主程序来说是不可见的,所以只能通过proxy来调用,是不是感觉很麻烦?但是,你还有更好的办法吗?

3. 子activity的实现

  1. packagecom.ryg.dynamicloadclient;
  2. importandroid.graphics.Color;
  3. importandroid.os.Bundle;
  4. importandroid.view.ViewGroup.LayoutParams;
  5. importandroid.widget.Button;
  6. publicclassTestActivityextendsBaseActivity{
  7. @Override
  8. protectedvoidonCreate(BundlesavedInstanceState){
  9. super.onCreate(savedInstanceState);
  10. Buttonbutton=newButton(mProxyActivity);
  11. button.setLayoutParams(newLayoutParams(LayoutParams.MATCH_PARENT,
  12. LayoutParams.MATCH_PARENT));
  13. button.setBackgroundColor(Color.YELLOW);
  14. button.setText("这是测试页面");
  15. setContentView(button);
  16. }
  17. }

说明:代码很简单,不用介绍了,同理,界面还是用代码来写的。

运行效果

1. 首先看apk安装时的运行效果

android动态加载apk_第2张图片

2. 再看看未安装时被宿主程序执行的效果

android动态加载apk_第3张图片android动态加载apk_第4张图片

说明:可以发现,安装和未安装,执行效果是一样的,差别在于:首先未安装的时候由于采用了反射,所以执行效率会略微降低,其次,应用的标题发生了改变,也就是说,尽管apk被执行了,但是它毕竟是在宿主程序里面执行的,所以它还是属于宿主程序的,因此apk未安装被执行时其标题不是自己的,不过这也可以间接证明,apk的确被宿主程序执行了,不信看标题。最后,我想说一下这么做的意义,这样做有利于实现模块化,同时还可以实现插件机制,但是问题还是很多的,最复杂的两个问题:资源的访问和activity生命周期的管理,期待大家有好的解决办法,欢迎交流。

代码下载:

https://github.com/singwhatiwanna/dynamic-load-apk

http://download.csdn.net/detail/singwhatiwanna/7121505

更多相关文章

  1. android随心笔记-part2-第1个android应用程序
  2. Android下用程序的方法为ListView设置分割线Divider样式
  3. Android中获取应用程序(包)的大小-----PackageManager的使用(二)
  4. Google将推出Android手机版Voice应用程序
  5. android中完全退出当前应用程序的四种方法
  6. 安卓加载模式(Android LauncherMode)
  7. Android LayoutInflater加载.xml文件原理分析
  8. Android程序怎样禁止横竖屏切换
  9. (转载)关于android应用程序的入口Activity

随机推荐

  1. android实现本程序数据的备份与恢复
  2. andorid 记录,以后看
  3. Android四大功能组件深入分析
  4. Android 之 Eclipse 导入 Android 源码
  5. TextView过长显示省略号, TextView文字中
  6. android:textAppearance解析
  7. Android异步处理三:Handler+Looper+Messag
  8. android setGravity()的使用
  9. Android CTS的使用
  10. Android中的Selector的用法