1. 研究背景
随着智能手机和移动互联网的普及和进一步快速发展,我国的互联网行业正在蒸蒸日上,人们对移动应用和产品在不同的使用场景下的需求越来越旺盛。各类移动互联网应用服务商为了满足用户需求,必须快速的迭代和更新产品 [1] 。或者像现在的很多APP一样,都有一个启动欢迎页,在不同的节日和运营活动的情况下,会有不同的广告页出现。再如,在一个应用发布后,突发一个比较严重的问题,需要紧急修复或者更新。
再者,在早期的安卓市场上发布应用,如果应用中含有广告,可能会审核不通过,那么一些个人开发者会在服务器端配置一个开关,审核时关闭开关,应用就不显示广告了,通过审核后,再通过服务器端把广告开关打开,这样就可以很好的规避应用市场的审核。之后,应用市场通过扫描APK内的manifest甚至dex文件,以此来确定开发者是否在APK包里植入广告代码。在服务器端配置开关参数的方法不行了,很多开发者就另辟蹊径。在应用的原生APK代码内部写入广告代码,在用户下载安装运行后,再从服务器下载广告代码,运行,实现广告的功能。该方法是可行的,这就是动态加载 [2] 。
那么接下来我们将具体来说明动态加载。1、应用能够通过在本地加载一些不存在的文件来实现特定的功能;2、这些可执行文件具有可替换性;3、动态加载不包括静态资源(如:启动图,主题,广告在服务器端的控制参数开关等);4、调用外部的Dex文件是Android动态加载的核心思想,在极端情况下,Dex文件可以作为Android APK的一个程序入口,都可以通过在服务器下载来实现Dex文件完成相应的功能 [3] 。
2. 传统PC软件的动态加载技术
传统PC端,动态加载技术被广泛使用,比如有些输入法,在初次安装的时候没有截图功能,在用户第一次使用的时候,会自行从服务器端下载安装,这样就能使用截图功能了。此外,DLL文件(Dynamic Link Library)在许多软件的安装目录中大量存在,一些特定的功能就是PC软件通过调用这些DLL里的代码执行的,这些技术就是一种动态加载 [4] 。在JAVA中,JAR作为其可执行文件,运行于虚拟机JVM上,虚拟机则通过ClassLoader加载JAR文件,并执行其中的代码。所以在JAVA中的动态加载,也是通过动态调用JAR文件来实现的 [5] 。
3. Android应用的动态加载技术定义
与JAVA程序类似,Android应用把虚拟机换成了Dalvik/ART,把JAR换成了Dex。那么我们可以考虑,在Android中怎样来实现动态加载。Android APP运行时,是不是也可以通过下载新的应用,或者通过调用Dex文件来实现 [6] 。
可是在Android上实现并不那么容易,如果用户下载新的APK而不安装,就不可能能够运行。如果用户手动运行这个APK进行安装,就是纯粹的安装了一个新的应用。然后启动使用这个应用。这样的方式,就不是上文所说的动态加载了。
在APK文件中,往往存在一个或者多个Dex文件,所有代码都会被编译到Dex中,应用的所有功能,都是通过执行这些Dex来实现的。APK被构建出来后,无法再更换其Dex文件的,但是,可以通过储存在外部服务器通过网络下载后,通过加载外部的Dex文件来实现动态加载 [7] 。
4. Android动态加载的类型
在Android项目中,动态加载技术主要有两种技术实现 [8] 。
其一,为动态加载.so库。动态加载就是用在了Android的NDK中,通过JNI动态加载.so库中其封装好的方法。JNI运行于Native层,一般由C++编译,所以其执行效率比JAVA代码高很多,因为JAVA代码执行在虚拟机中。所以,Android中为了完成一些对性能要求很高的功能,经常会使用动态加载.so库来完成(如:图片高斯模糊处理,Bitmap的解码等)。另外,.so库在很多安全领域得到广泛使用,因为其是由C++编译的,相比Smail更难破解,只能被反编译成汇编代码。需要特别注意的是,虽然在一般情况下我们把.so库一并打包在APK中,但是.so库也是从外部存储文件加载的。
其二,基于ClassLoader的动态加载.dex/jar/apk。就是我们在上文提到过的“在Android中动态加载由JAVA代码编译的Dex包并执行其中的代码逻辑”,这个技术在常规的Android开发比较少用到,而本文主要写的就是这种动态加载方式。
两种技术的区别:第一种本质上是java调用c++的类库属于api调用,一般把复杂运算和高性能要求的代码用c++实现打包成so库供java调用。第二种是java调用java,在应用运行时需要把java代码编译成class文件加载到虚拟机,通过虚拟机加载class文件的顺序在每次运行时首先加载我们修改过的java文件(同名的java文件只加载一次)实现更新覆盖而不用重新安装应用。
5. Android动态加载的方法和过程
无论是哪种动态加载方式,其基本原理是相通的,都是在程序运行时加载一些外部的可执行文件,然后调用这些文件的某个方法实现业务逻辑 [9] 。但是,需要注意的是,处于对安全的考虑,由于文件是可执行的,Android并不允许直接在手机外部储存加载这类可执行文件。对于这些外部的可执行文件,我们为了确保库不会第三方应用而已修改和拦截,可以先把他们拷贝到data/packagename/内部存储文件路径,然后再将其加载到当前的运行环境中,并调用需要的方法来实现相应的业务逻辑,从而实现动态调用。
与JVM不同,Android的虚拟机不能用ClassCload直接加载dex,而是要用DexClassLoader或者PathClassLoader,他们都是ClassLoader的子类,这两者的区别在于,DexClassLoader可以加载/jar/apk/dex,可以从SK卡中加载未安装的apk,而PathClassLoader要传入系统中apk的存放的Path,所以只能加载已经安装了的APK文件 [10] 。
类加载器说明:如图1,DexClassLoader和PathClassLoader都属于符合双亲委派模型的类加载器,他们并没有重载loadClass方法,它们在加载一个类之前,会去检查自己以及自己以上的类加载器(BaseDexClassLoader和ClassLoader)是否已经加载了这个类。如果已经加载过了,就会直接将之返回,并不会重复加载。DexClassLoader和PathClassLoader其实都是通过DexFile这个类来实现类加载的。Dalvik
虚拟机识别的是dex文件(不是class文件),所以类加载的文件也只能是dex文件,或者包含有dex文件的apk、jar文件。区别在于DexClassLoader需要提供一个可写的outpath路径,用来释放apk或jar包中的dex文件。PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件。而DexClassLoader可以支持apk、jar和dex文件,并且会在指定的outpath路径释放出dex文件 [11] 。
接着来看看DexClassLoader和PathClassLoader的构造方法:

Dex加载过程 [12] :
1.首先通过new DexClassLoader传入四个参数dexPath (需要被加载的文件地址)、optimizedDirectory:dex (文件被加载优化后的dex存放路径)、libraryPath (包含libraries的目录列表)、parent (父类构造器)。
2.在DexClassLoader构造器中会直接调用父类的构造器,将dex文件路径传值给originalPath同时构造一个DexPathList对象。
3.在DexPathList构造器中:把父类构造器赋值给definingContext、把dexPath分割后对应的file数组赋值给dexElements、把libraryPath分割成数组加上系统so库的目录赋值给nativeLibraryDirectories。
4.dexElements是一个简单的实体类,包含File、DexFile、ZipFile (jar、zip、apk形式的file)。在DexFile中会调用native方法openDexFile打开具体的file并输出到优化路径。
Android在运行时使用ClassLoader动态加载外部的Dex文件非常简单,不用覆盖安装新的APK,就可以更改APP的代码逻辑。但是Android却很难使用插件APK里的res资源,这意味着无法使用新的XML布局等资源,同时由于无法更改本地的Manifest清单文件,所以无法启动新的Activity等组件。不过可以先把要用到的全部res资源都放到主APK里面,同时把所有需要的Activity先全部写进Manifest里,只通过动态加载更新代码,不更新res资源,如果需要改动UI界面,可以通过使用纯Java代码创建布局的方式绕开XML布局,也可以使用Fragment代替Activity。某种程度上,简单的动态加载功能已经能满足部分业务需求了,特别是一些早期的Android项目,那时候Android的技术还不是很成熟,而且早期的Android设备更是有大量的兼容性问题,只有这种简单的加载方式才能稳定运行。这种模式的框架比较适用一些UI变化比较少的项目,比如游戏SDK,基本就只有登陆、注册界面,而且基本不会变动,更新的往往只有代码逻辑。
我们可以通过动态加载,让现在的Android应用启动一些“新”的Activity,甚至不用安装就启动一个“新”的APK (原来的APK叫“主APK”,新的APK称为“插件APK”)。主APK需要先注册一个空壳的Activity用于代理执行插件APK的Activity的生命周期。主要有一下特点:
1. 主APK可以启动未安装的插件APK;
2. 插件APK也可以作为一个普通APK安装并且启动;
3. 插件APK可以调用主APK里的一些功能;
4. 主APK和插件APK都要接入一套指定的接口才能实现以上功能;
6. 动态加载技术的作用与代价
凡事都有两面性,特别是这种非官方支持的非常规开发方式,在采用前一定要权衡清楚其作用与代价。如果决定了要采用动态加载技术,个人推荐可以现在实际项目的一些比较独立的模块使用这种框架,把遇到的一些问题解决之后,再慢慢引进到项目的核心模块;如果遇到了一些无法跨越的问题,要有能够迅速投入生产的替代方案。
作用:
1、 规避APK覆盖安装升级的局限性,提高了用户体验,而且也能规避一些安卓市场的限制;
2、 动态修复一些比较紧急的bug;
3、 提高启动速度,因为插件模块可以在需要时才初始化,叫做懒加载;
4、 主项目和插件项目可以并行开发,特别是应用体积较大时,可以把一些模块改为动态加载,以插件的形式,这样也可以减少主项目的体积,提高项目的编译速度;
5、 减少主项目的DEX方法数量,彻底解决65535问题;
6、 从项目管理的角度上来说,分割模块的方式,也做到了代码分离,大大降低了模块之间的耦合度,利于代码管理和bug走查;
7、 在Android应用的推广其他应用时,可以利用动态加载技术,让用户在不用下载新的APK的情况下体验新应用的功能。
代价:
1、 开发方式繁琐,和常规开发不同;
2、 非常规的开发方式,部分Android ROM可能已经改动了Framework层的代码,而有些框架却使用反射强行改动了这些代码,所以存在兼容性的风险,特别是在一些老旧机型上;
3、 随着动态加载框架复杂程度的加深,项目的构建过程也变得复杂,有可能要主项目和插件项目分别构建,再整;
4、 由于插件项目可能是独立开发,可能遇到主项目和插件项目的运行环境的不同,代码逻辑容易出现bug,而且在主项目中调试插件十分繁琐;
5、 兼容性问题,也是采用动态加载的产检在使用系统资源时经常发生的。
7. 结束语
本文详细阐述了移动互联网时代下Android应用的动态加载技术。从android的ClassLoader类入手,提出了可实行的解决方案,一定程度上的解决了应用更新快用户反复安装的烦恼。也提出了动态加载技术的优势和劣势。系统中用到的技术在应用快速迭代上性能和用户体验的提高上具有重要的价值和广阔的应用前景。