前段时间,公司项目完成了插件化的开发,自己也因此学习了很多Android插件化的知识,于是想把这些内容记录下来,本次带来Android插件化的第一篇:动态加载综述
Android插件化学习之路(一)之动态加载综述
Android插件化学习之路(二)之ClassLoader完全解析
Android插件化学习之路(三)之调用外部.dex文件中的代码
Android插件化学习之路(四)之使用插件中的R资源
Android插件化学习之路(五)之代理Activity
Android插件化学习之路(六)之动态创建Activity
Android插件化学习之路(七)之DL插件开发该注意的坑
Android插件化学习之路(八)之DynamicLoadApk 源码解析(上)
Android插件化学习之路(九)之DynamicLoadApk 源码解析(下)
1.什么是动态加载?
动态加载技术应由以下几个部分组成:
1) 应用在运行的时候通过加载一些本地不存在的可执行文件实现一些特定的功能;
2) 这些可执行文件是可以替换的;
3) 更换静态资源(比如换启动图、换主题、或者用服务器参数开关控制广告的隐藏现实等)不属于动态加载;
4) Android中动态加载的核心思想是动态调用外部的 dex文件,极端的情况下,Android APK自身带有的Dex文件只是一个程序的入口(或者说空壳),所有的功能都通过从服务器下载最新的Dex文件完成;
2.动态加载的类型
Android项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种:
1) 动态加载so库;
2) 动态加载dex/jar/apk文件(现在动态加载普遍说的是这种);
第一种,Android中NDK中其实就使用了动态加载,动态加载.so库并通过JNI调用其封装好的方法。后者一般是由C/C++编译而成,运行在Native层,效率会比执行在虚拟机层的Java代码高很多,所以Android中经常通过动态加载.so库来完成一些对性能比较有需求的工作(比如T9搜索、或者Bitmap的解码、图片高斯模糊处理等)。此外,由于so库是由C/C++编译而来的,只能被反编译成汇编代码,相比中dex文件反编译得到的Smali代码更难被破解,因此so库也可以被用于安全领域。这里为后面要讲的内容提前说明一下,一般情况下我们是把so库一并打包在APK内部的,但是so库其实也是可以从外部存储文件加载的。
第二种,“基于ClassLoader的动态加载dex/jar/apk文件”,就是我们上面提到的“在Android中动态加载由Java代码编译而来的dex包并执行其中的代码逻辑”,这是常规Android开发比较常用到的一种技术,目前网络上大多文章说到的动态加载指的就是这种。
3.Android中的动态加载技术
Java的可执行文件是Jar,运行在虚拟机上JVM上,虚拟机通过ClassLoader加载Jar文件并执行里面的代码。所以Java程序也可以通过动态调用Jar文件达到动态加载的目的。
Android项目中,所有Java代码都会被编译成dex文件,Android应用运行时,就是通过执行dex文件里的业务代码逻辑来工作的。使用动态加载技术可以在Android应用运行时加载外部的dex文件,而通过网络下载新的dex文件并替换原有的dex文件就可以达到不安装新APK文件就升级应用(改变代码逻辑)的目的。
4.Android动态加载的大致过程
无论上面的哪种动态加载,其实基本原理都是在程序运行时加载一些外部的可执行的文件,然后调用这些文件的某个方法执行业务逻辑。需要说明的是,因为文件是可执行的(so库或者dex包,也就是一种动态链接库),出于安全问题,Android并不允许直接加载手机外部存储这类noexec(不可执行)存储路径上的可执行文件。
对于这些外部的可执行文件,在Android应用中调用它们前,都要先把他们拷贝到data/packagename/内部储存文件路径,确保库不会被第三方应用恶意修改或拦截,然后再将他们加载到当前的运行环境并调用需要的方法执行相应的逻辑,从而实现动态调用。
动态加载的大致过程就是:
1) 把可执行文件(.so/dex/jar/apk)拷贝到应用APP内部存储;
2) 加载可执行文件;
3) 调用具体的方法执行业务逻辑;
5.动态加载 so库
动态加载so库应该就是Android最早期的动态加载了,不过so库不仅可以存放在APK文件内部,还可以存放在外部存储。Android开发中,更换so库的情形并不多,但是可以通过把so库挪动到APK外部,减少APK的体积,毕竟许多so库文件的体积可是非常大的。
6.动态加载 dex/jar/apk文件
我们经常讲到的那种Android动态加载技术就是这种,后面我们谈到“动态加载”如果没有特别指定,均默认是这个。
基础知识:类加载器ClassLoader和dex文件
动态加载dex/jar/apk文件的基础是类加载器ClassLoader,它的包路径是java.lang,由此可见其重要性,虚拟机就是通过类加载器加载其需要用的Class,这是Java程序运行的基础。
现在网上有多种基于ClassLoader的Android动态加载的开源项目,大部分核心思想都差不多,按照复杂程度以及具体实现的框架,大致可以分为以下三种形式。
简单的动态加载模式
Android应用在运行时使用ClassLoader动态加载外部的dex文件非常简单,不用覆盖安装新的APK,就可以更改APP的代码逻辑。但是Android却很难使用插件APK里的res资源,这意味着无法使用新的XML布局等资源,同时由于无法更改本地的Manifest清单文件,所以无法启动新的Activity等组件。
不过可以先把要用到的全部res资源都放到主APK里面,同时把所有需要的Activity先全部写进Manifest里,只通过动态加载更新代码,不更新res资源,如果需要改动UI界面,可以通过使用纯Java代码创建布局的方式绕开XML布局。同时也可以使用Fragment代替Activity,这样可以最大限度得避开“无法注册新组件的限制”。
这种模式的框架比较适用一些UI变化比较少的项目,比如游戏SDK,基本就只有登陆、注册界面,而且基本不会变动,更新的往往只有代码逻辑。
代理Activity模式
我们可以通过动态加载,让现在的Android应用启动一些“新”的Activity,甚至不用安装就启动一个“新”的APK。宿主APK需要先注册一个空壳的Activity用于代理执行插件APK的Activity的生命周期。
1) 宿主APK可以启动未安装的插件APK;
2) 插件APK也可以作为一个普通APK安装并且启动;
3) 插件APK可以调用宿主APK里的一些功能;
4) 宿主APK和插件APK都要接入一套指定的接口框架才能实现以上功能;
同时也主要有一下几点限制:
1) 需要在Manifest注册的功能都无法在插件实现,比如应用权限、LaunchMode、静态广播等;
2) 宿主一个代理用的Activity难以满足插件一些特殊的Activity的需求,插件Activity的开发受限于代理Activity;
3) 宿主项目和插件项目的开发都要接入共同的框架,大多时候,插件需要依附宿主才能运行,无法独立运行;
代理Activity模式的核心在于“使用宿主的一个代理Activity为插件所有的Activity提供组件工作需要的环境”,随着代理模式的逐渐成熟,现在还出现了“使用Hack手段给插件的Activity注入环境”的模式。
动态创建Activity模式
动态创建Activity模式的核心是“运行时字节码操作”,现在宿主注册一个不存在的Activity,启动插件的某个Activity时都把想要启动的Activity替换成前面注册的Activity,从而是后者能正常启动。
1) 主APK可以启动一个未安装的插件APK;
2) 插件APK可以是任意第三方APK,无需接入指定的接口,理所当然也可以独立运行;
作用
1) 规避APK覆盖安装的升级过程,提高用户体验,顺便能 规避 一些安卓市场的限制;
2) 动态修复应用的一些 紧急BUG,做好最后一道保障;
3) 当应用体积太庞大的时候,可以把一些模块通过动态加载以插件的形式分割出去,这样可以减少主项目的体积,提高项目的编译速度,也能让主项目和插件项目并行开发;
4) 插件模块可以用懒加载的方式在需要的时候才初始化,从而 提高应用的启动速度;
5) 从项目管理上来看,分割插件模块的方式做到了 项目级别的代码分离,大大降低模块之间的耦合度,同一个项目能够分割出不同模块在多个开发团队之间 并行开发,如果出现BUG也容易定位问题;
6) 在Android应用上 推广 其他应用的时候,可以使用动态加载技术让用户优先体验新应用的功能,而不用下载并安装全新的APK;
7) 减少主项目DEX的方法数,65535问题 彻底成为历史(虽然现在在Android Studio中很容易开启MultiDex,这个问题也不难解决);
缺点
1) 开发方式可能变得比较诡异、繁琐,与常规开发方式不同;
2) 随着动态加载框架复杂程度的加深,项目的构建过程也变得复杂,有可能要主项目和插件项目分别构建,再整合到一起;
3) 由于插件项目是独立开发的,当主项目加载插件运行时,插件的运行环境已经完全不同,代码逻辑容易出现BUG,而且在主项目中调试插件十分繁琐;
4) 非常规的开发方式,有些框架使用反射强行调用了部分Android系统Framework层的代码,部分Android ROM可能已经改动了这些代码,所以有存在兼容性问题的风险,特别是在一些古老Android设备和部分三星的手机上;
5) 采用动态加载的插件在使用系统资源(特别是Theme)时经常有一些兼容性问题,特别是部分三星的手机;
上面说到的都是基于ClassLoader的动态加载技术(除了加载SO库外),使用ClassLoader的一个特点就是,如果程序不重新启动,加载过一次的类就无法重新加载。因此,如果使用ClassLoader来动态升级APP或者动态修复BUG,都需要重新启动APP才能生效。
除了使用ClassLoader外,还可以使用jni hook的方式修改程序的执行代码。前者是在虚拟机上操作的,而后者做的已经是Native层级的工作了,直接修改应用运行时的内存地址,所以使用jni hook的方式时,不用重新应用就能生效。
目前采用jni hook方案的项目中比较热门的有阿里的dexposed和AndFix。
DL dynamic-load-apk
android-pluginmgr
360 DroidPlugin
携程网 DynamicAPK
女娲 Nuwa
Android-Plugin-Framework