Android插件化分析

Android插件化诞生时间不长,但曾红火一时,虽然在我的项目中从未用过插件化技术,但本着对技术的热爱,也投入时间了解、分析热门技术,包括插件化和热修复,对比这两件技术,他们的工作机制都差不多,都是利用ClassLoader来加载APK和dex,不同的是插件化大多加载的是APK,其中携带资源,而热修复主要用于修复代码,不包含资源,修复的单位也大多以dex为主,dex尽量小。

引言

从2012年开始,就有公司在研究这项技术Android插件化,淘宝做得比较早,但淘宝的这项技术一直是保密的。直到2015年才陆续出现很多框架,Android插件化分成很多技术流派,实现的方式都不太一样。Android插件化涉及比较多,包括Android底层系统与插件化相关的类、四大组件的原理与相应插件化实现方式、增量更新、AAPT等技术 。

Android插件化的发展历史

2012年,有人做插件化技术,是大众点评的屠毅敏,他推出了AndroidDynamicLoader框架,用Fragment来实现。大众点评是国内做App比较早的公司,他们积累了很多的经验,尤其是插件化技术 。通过动态加载不同的Fragement,把想换的页面都换掉。我们也是在这个项目中第一次看到了如何通过addAssetPath来读取插件中的资源。

2013年,出现了23Code,23Code提供了一个壳,在这个壳里可以动态下载插件,然后动态运行。可以在壳外编写各种各样的控件,放在这个框架下去运行。这就是Android插件化技术。这个项目的作者和开源地址不是很清楚。

2014年初,阿里员工做了一次技术分享,专门讲淘宝的Altas技术,以及这项技术的大方向,但是很多技术细节没有分享。

2014年底,任玉刚发布了一个Android插件化项目,起名为dynamic-load-apk,算是行业的里程碑项目。它没有Hook太多的系统底层方法,而是从上层,即App应用层解决问题,创建一个继承自Activity的ProxyActivity类,然后让插件中的所有Activity都继承自ProxyActivity,并重写Activity所有的方法。

2015年4月,一个新框架推出来,叫OpenAltas,后来改名为ACDD。这个框架参考了淘宝App的很多经验,主要就是Hook的思想,同时,还首次提出来通过扩展AAPT来解决插件与宿主的资源id冲突的问题。

2015年8月,张勇发布DroidPlugin,这是Android插件化中第二个里程碑式的项目,能把任意的App都加载到宿主里。可以基于这个框架写一个宿主App,然后就可以把别人写的App都当作插件来加载。

2015年9月,阿里推出了Andfix,实现迅速替换的途径。

2015年10月,大众点评的贾吉鑫做了一个项目,起名为Nuwa(女娲),主要思路跟Andfix差不多,都是解决Android的修复问题,能修复线上的任何一个方法。可惜后来没有继续维护。

2015年底,仍然是Android插件化框架,林广亮提出了一个新机制——Small框架,这个机制不太一样的地方就是,通过脚本的方式来解决资源冲突的问题。

2017年 6 月 ,VirtualAPK 是滴滴开源的一套插件化框架,支持几乎所有的 Android 特性,四大组件方面。

2017 年 7 月,RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。

Android插件化的基础知识

首先,做Android系统原代码的人要非常熟悉Binder,如果没有它真的寸步难行。Binder涉及两层技术。你可以认为它是一个中介者模式,在客户端和服务器端之间,Binder就起到中介的作用,要实现四大组件的插件化,就需要在Binder上做修改。Binder服务端的内容没办法修改,只能改客户端的代码。

其次,App打包的流程。代码写完了,执行一次打包操作,中途经历了资源打包、dex生成、签名等过程。其中最重要的就是资源的打包,即AAPT这一步,如果宿主和插件的资源id冲突,一种解决办法就是在这里做修改。

第三,App在手机上的安装流程。熟悉安装流程不仅对插件化有帮助,在遇到安装bug的时候也非常重要。手机安装App的时候,经常会有下载异常,提示资源包不能解析,这时需要知道安装App的这段代码在什么地方,这只是第一步。第二步需要知道,App下载到本地后,具体要做哪些事情。手机有些目录不能访问,App下载到本地之后,放到哪个目录下,然后会生成哪些文件。插件化有个增量更新的概念,如何下载一个增量包,从本地具体哪个位置取出一个包,这个包的具体命名规则是什么等等。

第四,App的启动流程。Activity启动有几种方式?一种是写一个startActivity,第二种是点击手机App,通过手机系统里的Launcher机制,启动App里默认的Activity。通常,App开发人员喜闻乐见的方式是第二种。那么第一种方式的启动原理是什么呢?另外,启动的时候,main函数在哪里?这个main函数的位置很重要,我们可以对它所在的类做修改,从而实现插件化。

第五,做Android插件化需要控制两个地方。首先是插件Dex的加载,如何把插件Dex中的类加载到内存?另外是资源加载的问题。插件可能是apk也可能是so格式,不管哪一种,都不会生成R.id,从而没办法使用。这个问题有几种解决方案。一种是重写Context的getAsset、getResource之类的方法,偷换概念,让插件读取插件里的资源,但缺点就是宿主和插件的资源id会冲突,需要重写AAPT。另一种是重写AMS中保存的插件列表,从而让宿主和插件分别去加载各自的资源而不会冲突。第三种方法,就是打包后,执行一个脚本,修改生成包中资源id。

第六,在实施插件化后,如何解决不同插件的开发人员的工作区问题。比如,插件1和插件2,需要分别下载哪些代码,如何独立运行?就像机票和火车票,如何只运行自己的插件,而不运行别人的插件?这是协同工作的问题。火车票和机票,这两个Android团队的各自工作区是不一样的,这时候就要用到Gradle脚本了,每个项目分别有各自的仓库,有各自不同的打包脚本,只需要把自己的插件跟宿主项目一起打包运行起来,而不用引入其他插件,还有更厉害的是,也可以把自己的插件当作一个App来打包并运行。

Android插件化的流派及开源框架

技术流派

目前分三种:

  • 第一种是动态替换,也就是Hook。可以在不同层次进行Hook,从而动态替换也细分为若干小流派。可以直接在Activity里做Hook,重写getAsset的几个方法,从而使用自己的ResourceManager和AssetPath;也可以在更抽象的层面,也就是在startActivity方法的位置做Hook,涉及的类包括ActivityThread、Instrumentation等;最高层次则是在AMS上做修改,也就是张勇的解决方案,这里需要修改的类非常多,AMS、PMS等都需要改动。总之,在越抽象的层次上做Hook,需要做的改动就越大,但好处就是更加灵活了。没有哪一个方法更好,一切看你自己的选择。

  • 第二种是静态代理,这是任玉刚的框架采取的思路。写一个PluginActivity继承自Activity基类,把Activity基类里面涉及生命周期的方法全都重写一遍,插件中的Activity是没有生命周期的,所以要让插件中的Activity都继承自PluginActivity,这样就有生命周期了。

  • 第三种是Dex合并,Dex合并就是Android热修复的思想。刚才说到了两个项目——AndFix和Nuwa,它们的思想是相同的。原生Apk自带的Dex是通过PathClassLoader来加载的,而插件Dex则是通过DexClassLoader来加载的。但有一个顺序问题,是由Davlik的机制决定的,如果宿主Dex和插件Dex都有一个相同命名空间的类的方法,那么先加载哪个Dex,哪个Dex中的这个类的方法将会占山为王,后面其他同名方法都替换了。

开源框架

在 Android 中实现插件化框架,需要解决的问题主要如下:

  • 资源和代码的加载
  • Android 生命周期的管理和组件的注册
  • 宿主 APK 和插件 APK 资源引用的冲突解决

下面看看几个目前主流的开源框架,看看每个框架具体实现思路和优缺点:

DL动态加载框架(2014年底)
  • Github

  • DL框架原理:动态加载主要有两个需要解决的复杂问题:资源的访问和activity生命周期的管理,而DL框架很好地解决了这些问题。需要说明的一点是,不可能调起任何一个未安装的apk,这在技术上是很难实现的,我们调起的apk必须受某种规范的约束,只有在这种约束下开发的apk,才能将其调起。

DroidPlugin(2015年8月)
  • Github
  • 插件机制:它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。

  • 特点:插件APK完全不需做任何修改,可以独立安装运行、也可以做插件运行。要以插件模式运行某个APK,你无需重新编译、无需知道其源码; 插件的四大组件完全不需要在Host程序中注册,支持Service、Activity、BroadcastReceiver、ContentProvider四大组件;插件之间、Host程序与插件之间会互相认为对方已经”安装”在系统上了;API低侵入性:极少的API。HOST程序只是需要一行代码即可集成Droid Plugin;超强隔离:插件之间、插件与Host之间完全的代码级别的隔离:不能互相调用对方的代码。通讯只能使用Android系统级别的通讯方法;支持所有系统API;资源完全隔离:插件之间、与Host之间实现了资源完全隔离,不会出现资源窜用的情况;实现了进程管理,插件的空进程会被及时回收,占用内存低;插件的静态广播会被当作动态处理,如果插件没有运行(即没有插件进程运行),其静态广播也永远不会被触发。

  • 限制和缺陷:无法在插件中发送具有自定义资源的Notification,例如:带自定义RemoteLayout的Notification、图标通过R.drawable.XXX指定的通知(插件系统会自动将其转化为Bitmap);无法在插件中注册一些具有特殊Intent Filter的Service、Activity、BroadcastReceiver、ContentProvider等组件以供Android系统、已经安装的其他APP调用;缺乏对Native层的Hook,对某些带native代码的apk支持不好,可能无法运行。比如一部分游戏无法当作插件运行。

Small(2015年底)
  • Github

  • 特点:所有插件支持内置宿主包中;插件的编码和资源文件的使用与普通开发应用没有差别;通过设定URI,宿主以及Native应用插件,Web 插件,在线网页等能够方便进行通信;支持Android、iOS、和Html5,三者可以通过同一套Javascript接口实现通信。

  • 缺点:暂不支持 Service 的动态注册,不过这个可以通过将 Service 预先注册在宿主的 AndroidManifest.xml 文件中进行规避,因为 Service 的更新频率通常非常低。

VirtualAPK(2017年6月)
  • Github

  • 原理:VirtualAPK 对插件没有额外的约束,原生的 apk 即可作为插件。插件工程编译生成 apk后,即可通过宿主 App 加载,每个插件 apk 被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如下图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的 App 一样运行;合并宿主和插件的ClassLoader 需要注意的是,插件中的类不可以和宿主重复;合并插件和宿主的资源 重设插件资源的 packageId,将插件资源和宿主资源合并;去除插件包对宿主的引用 构建时通过 Gradle 插件去除插件对宿主的代码以及资源的引用。

RePlugin(2017年7月)
  • Github

  • 特点:RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。

  • 优势:极其灵活:主程序无需升级(无需在Manifest中预埋组件),即可支持新增的四大组件,甚至全新的插件;非常稳定:Hook点仅有一处(ClassLoader),无任何Binder Hook!如此可做到其崩溃率仅为“万分之一”,并完美兼容市面上近乎所有的Android ROM;特性丰富:支持近乎所有在“单品”开发时的特性。包括静态Receiver、Task-Affinity坑位、自定义Theme、进程坑位、AppCompat、DataBinding等;易于集成:无论插件还是主程序,只需“数行”就能完成接入;管理成熟:拥有成熟稳定的“插件管理方案”,支持插件安装、升级、卸载、版本管理,甚至包括进程通讯、协议版本、安全校验等。

Android插件化的发展困境

目前,Android插件化技术也遇到发展困境:

  • 插件化已经沦落为修bug的工具。这跟插件化的初衷不一样,插件化是实现新功能,而不是修复bug。
  • 插件化技术只在中国有市场。国外的公司根本不看好这项技术,这可能是因为他们用GooglePlay,而谷歌官方不建议用插件化这种方式。国外开发者不敢越雷池半步;苹果也禁用该技术。

Android插件化的未来

关于Android插件化未来的方向:

  • 插件化最厉害的方向应该是每个Activity都是一个插件,这样就可以将任何一个出问题的Activity迅速替换。

  • 双开技术是现在非常火的一项技术。

  • 所有数据都增量下载的机制,虽然做起来很麻烦,但是一旦实现,会让App非常快。

通读一遍上面列出来的基础知识,然后再去做App应用,插件化只是一门技术,你最应该关注的是其背后的本质,也就是内功修炼。


叁考资料:

  • 《安卓插件化从入门到放弃》
  • 《安卓插件化的过去现在和未来》