胡琪

为今天工作,为明天投资,为未来孵化一些东西!

安卓动态加载技术

何为动态加载技术

在谈动态加载之前,先来看一个场景,很多App产品的启动页会在一些重要的节日将原本平常的启动页图片更换为在某个节日相应的应景图片,如在国庆节时百度地图启动页面会是关于国庆祝福的图片,在过年的时候会是xx公司给大家拜年的图片,这样不仅能够提高用户体验,让App不在单调,另一方面,也给了App以温度,记得腾讯QQ在我的生日那天启动页面是祝我生日快乐的页面,这样能够让用户感觉到这个App的温暖,(虽然只是一个简短的启动页面的祝福,虽然腾讯没在我的生日给我免费开个会员,只是假惺惺的说了句祝福的话,但实际上这反映的是该App背后的企业对用户体验的重视,说句实话,在中国互联网企业中,个人觉得腾讯的产品是做的最好的,没有之一)很显然,这些启动图片是不可能在发布Apk的时候就直接打包进去的,而是通过网络从服务端下载下来,然后按需显示出来的,再比如很多App都会有换肤功能,比如QQ会有很多个性设置模块,有些皮肤甚至是以apk插件的形式存在于服务端,当用户觉得该皮肤不错,点击更换皮肤的时候客户端才从服务端将该皮肤插件apk下载下来,然后显示在客户端。这就是动态加载技术,即客户端能够在运行时动态的调用外部代码从而实现在不更改客户端代码的前提下更新客户端的App的功能,其本质是客户端的类加载器加载从服务端下载下来的apk、dex、jar(必须包含dex文件)文件中的资源(如类的代码,图片资源等)供客户端App使用。

动态加载技术的好处

  • 能够动态加载资源文件,如将某些不常用的资源以额外的apk或者jar包的形式提供,而不需要一起打包在主apk中,实现按需使用,从而可以减小apk的体积,如换肤功能,某些皮肤可能用户永远都不会使用,因此不需要打包在主apk中
  • 能够在不修改已经发布的apk代码的前提下动态增加或者更新该apk的功能,如插件化技术,将某些功能以插件的形式提供,这样加载不同的插件就可以实现更新不同的功能

下面就以前面说到的腾讯QQ换肤功能为例分析换肤中的动态加载资源的原理,然后带领大家实现该功能!

 

换肤原理

前面说过,一般App的换肤功能是将皮肤以插件的形式存放于服务端,当用户点击换肤之后客户端App会从服务端去下载该插件apk到本地,然后通过动态加载技术使用该apk中的资源,包括类的代码和图片等。在这里就涉及到两个概念,宿主App和插件App,宿主App指的是调用插件apk的App,如QQ换肤功能案例中的QQ,而插件App指的是提供资源供客户端App调用的App,如QQ换肤功能案例中的某一个皮肤插件(通常是以apk或者dex文件格式的形式提供的)。

那么这里就涉及到一个问题就是插件apk是未安装的,(当然我们也可以在用户下载了该皮肤包插件后弹出一个安装界面让用户选择安装,但是这样很明显用户体验太差),我们知道在安卓中使用资源文件需要Context对象,但是apk未安装,没走安卓系统中apk的初始化启动过程,因此不能够得到该未安装的apk的Context,那么我们只能退而求其次,看能否得到未安装apk的Resource对象?因为我们通过Context来访问资源文件本质上是通过getResources()函数来访问的,而该函数实际上返回的是Resources对象,因此我们如果能够直接得到未安装apk的Resources的话,就能够操作其资源文件了。因此我们先看一下Resources类的构造函数

可以看到Resources类的构造函数包含三个参数,其中第一个参数为AssetManager,大家可能回想这很简单啊,我们实例化一个AssetManager对象传进去不就可以了,事实上不是这么简单的,还是和前面的原因一样,安卓系统中的很多对象的使用是需要走apk初始化启动过程的,不是简单的创建一个java对象就可以使用的,因为每个APK文件在进程中都对应有一个全局的Resourses对象以及一个全局的AssetManager对象,也就是说如果只是简单的实例化一个AssetManager对象传进去,那么这个对象仅仅只是一个普通的java类而已,不具备安卓中全局AssetManager对象的功能,因此也就不具备使用插件apk中资源的功能,因为该AssetManager根本就没走apk初始化启动过程,因此和插件apk无任何关系。但是在AssetManager类中我们看到了addAssetPath函数:代码如下:

从注释可以看到该函数的作用是将额外的assets添加到AssetManager中,其参数是一个文件夹或者zip文件路径,而apk文件本质上就是一个zip包,事实上走了安卓系统初始化过程的apk之所以能够通过AssetManager调用资源也是系统调用了该函数建立起了资源与apk之间对应关系。但是从注释可以看到该函数是隐藏的,因此不能够直接调用,但是我们可以通过反射机制来调用该函数。代码如下:

首先创建一个AssetManager对象,然后通过反射调用addAssetPath函数将apk资源文件加载到对应的AssetManager中,这样我们构建的AssetManager才不是一个普通的java对象,而是安卓中的可以访问资源的AssetManager对象,然后就可以按照前面说的构造Resources对象了。代码如下:

这样就得到了未安装的插件apk中的全局Resources对象了,然后就可以使用该对象像操作已经安装了的apk那样使用未安装的插件apk中的资源了。因此接下来就是如何获取插件apk中的资源的id,因为我们知道在宿主apk中访问资源都是通过R文件形式来访问的,说白了就是如何加载插件apk中的R类,这个就是类加载器的原理了,和java中的类加载类似,在安卓中可以通过DexClassLoader类来加载未安装的apk,当然如果是dex文件的话还可以使用PathClassLoader这个类,这两个类的区别在于DexClassLoader可以传入包含dex的apk或者jar包路径作为参数,而PathClassLoader只能传入dex路径或者已安装的apk路径(已经安装的apk存在缓存的dex文件),因此对于未安装的apk我们需要使用DexClassLoader来加载类,该类的定义如下:

其每个参数的含义如下:

  • dexPath:包含dex文件的路径,如apk或者包含dex文件的jar包
  • optimizedDirectory:这个是从apk中释放出的dex文件的保存路径
  • librarySearchPath:顾名思义lib搜素路径,一般为null
  • parent:父加载器

得到DexClassLoader对象之后我们就可以使用loadClass函数来加载我们要加载的类,如换肤案例中,我们需要获得换肤图片的id,那么我们需要加载R$mipmap类(android studio项目工程图片资源一般保存在mipmap中),然后就可以通过反射获得该类中的一些字段值,代码如下:

这样我们就得到了插件apk中换肤图片的id,然后就可以像操作已安装的apk那样通过id来设置皮肤图片了。

换肤实例

前面的理论知识讲解完了,那么接下来就通过实战来实现换肤功能。在android studio中创建一个工程命名为ChangeSkin,这里为了简单起见,主界面只包括一个ImageView和一个Button,一个用来显示皮肤图片,一个用来点击切换图片。如图

《安卓动态加载技术》

然后创建一个插件apk工程,命名为skinplugin,当然插件apk在创建的时候可以选择无Activity,因为只需要提供一些资源而已,无需界面。在一般的换肤功能app中,皮肤插件都是从服务端通过网络下载至本地然后使用,这里为了简单起见,直接把插件apk放到宿主apk的assets目录下,事实上这和从网络下载至本地无实质性差异,整个宿主apk代码如下:

整个代码注释很详细,原理都是前面介绍过的,大家应该能够看懂,然后运行工程,会出现上图所示的界面,主界面显示QQ图像,下面是换肤按钮,点击换肤按钮,可以看到憨态可掬的企鹅被长相甜美的妹纸取代了哦,如图:

《安卓动态加载技术》

而妹纸图是在插件apk中的,也就是说已经达到了不安装插件apk,而在宿主apk中使用插件apk的资源的效果哦,也就达到了动态加载资源的目的。当然这里只是动态加载资源文件,事实上还可以动态加载Activity,不过这个比较复杂,关于动态加载技术的应用插件化技术目前在github上也可以找到一些著名的开源框架,如360的DroidPlugin,感兴趣的同学可以去看下。

源码下载地址:

本地下载

 

 

注:本文首次发表于www.huqi.tk,谢绝转载,如需转载,请注明出处:www.huqi.tk

 

扫描下方二维码实时接收最新技术干货推送

《安卓动态加载技术》

扫描二维码实时接收最新技术干货推送,而且会不定期的发布互联网名企内推机会哦!

打赏

点赞
    1. huqi说道:

      觉得不错的话 :razz: ,可以浏览下本站其他文章,也欢迎将本站收藏到浏览器持续访问,另外也欢迎关注我的微信公众号实时接受最新文章推送。

  1. Selina说道:

    :eek: 昨天还有歌曲播放,今天怎么就没了?? :cry:

    1. huqi说道:

      因为那个音乐播放的插件影响到了页面部分内容排版,所以去掉了。

  2. Nature说道:

    大哥你这搞的有点复杂啊,实现动态加载图片的话,启动页每次启动从网络请求图片,让后保存到本地不就行了?

    1. huqi说道:

      如果只是启动页换图片不需要用到动态加载,文中案例是动态换肤,只不过皮肤简化为了一张图片,但原理是一样的,不是换启动页图片

  3. 北林说道:

    强无敌

  4. pbl说道:

    changeSkin方法里的 try下面的那个clazz 对象好像乱码了 :confused:

    1. huqi说道:

      其他文章某些字符也出现过乱码,可能是用的这个代码高亮的插件对某些符号不兼容导致的,你可以点击文章末尾的本地下载查看源码哦!

  5. pbl说道:

    :sad: 怎么去加载插件中的类,然后调用方法啊?

    1. huqi说道:

      加载插件中的类实际上就是动态加载指定路径的dex,如Assets目录下的插件dex,然后调用该插件dex中的类,本质上是MultiDex,可以参看 http://www.huqi.tk/index.php/2017/10/08/dex_loader_multidex_hotfix/

发表评论

电子邮件地址不会被公开。 必填项已用*标注