安卓应用开发的大量难题,其实最后都需要插件技术去解决。
现今插件技术的使用非常普遍,比如微信、QQ、淘宝、天猫、空间、携程、大众点评、手机管家等等这些大家在熟悉不过的应用都在使用。
插件技术可以给项目开发带来巨大的好处,比如:并行高效开发、模块解耦、解除单个dex函数不能超过65535的限制、动态更新升级、按需加载等等。
本文的目的是从一个典型的复杂项目中总结出较为全面与完整的安卓插件技术。
掌握好插件技术,需要如下的安卓基础和相关知识,例如:
Android应用程序安装,加载过程
Android应用运行机制,生命周期调用原理
Android应用资源编译打包原理
Android应用读取资源原理
Android系统AMS、PMS、NMS等系统服务的运作原理
增量更新
HOOK等技术
插件技术知识领域如图:
这些技术中每一个点都需要大篇幅内容才能完全讲清楚。不过,好在Android是开源的,每一个插件技术涉及到的技术点都可以翻阅源码进行进一步的研究。下面我从当前所负责的一个插件化项目(PACEWEAR手表助手)经历,来梳理一下插件技术的应用及核心内容。
项目的困惑
PACEWEAR手表助手原自腾讯TOS的智能穿戴项目。
因为目前大部分智能手表和手环还不能独立联网通讯,须通过蓝牙连接手机,借助手机的网络来完成一系列业务功能。PACEWEAR手表助手就是这么一个手机软件,帮助智能穿戴设备使用手机网络,并通过蓝牙连接的方式完成对智能穿戴设备的各种配置和管理。
PACEWEAR手表助手项目开始初期,业务并没有大面积铺开,三四个工程师还算跑的比较顺利,随着项目的进展,主工程框架、登录、配对、设置、ota、市场、天气、地图、运动、音乐、健康管理、支付、应用管理、表盘管理等功能不断加入,参与的人也慢慢变多,问题也就多了起来,维护越来越困难,总结有如下几点:
工程频繁报方法数超65535
多个模块在同一个app中开发代码耦合,架构冗余,牵一发而动全身
人员效率低下,时间往往花费在沟通,构建问题处理上
分工不明确,灰色地带重复逻辑比较多
业务与业务之间互相调用,不够独立
问题跟进原来越繁琐,牵扯人数众多
功能越来越多,目前这种开发方式不可持续
连接的手表和手环设备种类越来越多
针对以上问题虽然我们考虑过动态加载jar、Html5等措施来缓解,但最终还是没能彻底从根本上解决这些问题,一直在苦恼着整个项目团队...
寻找适合项目的插件框架
这种情况下我们很快意识到需要引入插件化的开发模式,才能一劳永逸地这解决这一系列问题。
引入Dynamic-load-apk插件框架
团队在2015年中开始引入了Dynamic-load-apk(后面简称DyLA)框架,这套框架是从App应用层解决加载插件的问题:创建一个继承自Activity的ProxyActivity类,然后让插件中的所有Activity都继承自ProxyActivity,并重写Activity所有的方法。然而在功能上,仅支持Activity组件,这个是这套框架最大的短板;另外基于这套框架进行的插件应用开发,依赖条件复杂[需要内置jar包,组件必须实现ProxyActivity的所有接口]、调试困难等各种问题。重重约束是的项目插件化业务进展及其缓慢,比如支付模块两个同事开发了两个月最后发现很多需求没法实现,最终不得不放弃插件化;健康模块开发不到两周的同事开始抓狂,被各种问题不断折腾着(为啥不能联调、为什么这个要特殊处理、为什么这里资源找不到等等)。最后仅有健康、Yiya语音极少数几个模块勉强插件化。随着项目的进展,业务模块的不断增多,当初的问题不但没有得到解决,反而增加了对DyLA模块的维护,这个状态一直持续到了2016下半年9月。
预研适合项目的插件框架
PACEWEAR手表助手项目团队在9月份初对比了一些开源插件框架的能力:
同时评估了他们的优缺点,最后确定基于APF进行开发一套适合PACEWEAR手表助手的插件框架。
然而,仅是支持application和四大组件还远远不能满足PACEWEAR手表助手项目的要求,PACEWEAR手表助手有二十多个业务模块,第一批需要进行插件化的就有十五个,由不同的同事进行开发负责,而且有些业务还需要和第三方进行交互对接...因此,团队要能高效的将PACEWEAR手表助手项目完成插件化并且让所有插件业务都符合产品需求稳定的运行,对插件框架要求首先就需要做到基于框架开发的插件应用功能对齐原生,这样框架就需要:
支持application、四大组件(activity4个LaunchMode)、so、fragment、notification、toast等基础能力
支持联调插件应用
支持加载本地网页等
支持插件自定义控件和样式
组件进程配置等原生应用程序的能力;
同时需要这套框架支持将宿主的基础能力:设备账号信息、和手表通讯、统计上报、文件传输、网络、ota、控件库及宿主的资源共享给插件应用;
另外需要将插件运行时间及在宿主中的显示与宿主完全解耦。不然插件的调整必然要影响到宿主的代码调整,这可不是一个明智的落地方案。
综合上面的要求及项目进行过程中的调整,经过进一个月的努力,这套框架终于预研成功,正式应用到PACEWEAR手表助手项目上。
这套框架就叫TwsPluginFramework框架(后面简称TPF框架,已经开源:https://github.com/rickdynasty/TwsPluginFramework )。
这套框架相比业界其他插件框架能力对比如下:
另外Hook系统服务的安全隐患是不可预知的,因此TwsPluginFramework框架尽可能少的对系统服务等进行hook处理。
TPF框架原理
插件技术的实现原理是源于Android系统(Android系统本身就是一套插件框架,运行在这个系统之上的应用就是一个个的”插件应用”)对应用的管理机制:安装(Install)、运行(Running)、卸载(Uninstall)。
运行在TPF框架之上的插件应用和android应用程序又有所不同,不同点主要有下面几点:
应用程序的安装有android系统负责完成,而插件应用的安装流程由插件框架负责完成;
插件应用没有走系统的安装流程,组件等信息没有被注册到系统里面,要使插件应用能正常的运行,插件框架需要将这些插件应用内部的组件全部“合法化”;
插件应用的卸载也不走系统的卸载流程,而是由宿主负责完成的。
上面三个流程中安装、卸载基本和系统的处理方式是一样的。而运行就一样,插件应用程序的运行需要经过“插件框架”这个中间层进行合法化后才能运行在系统里面,这个合法化过程就需要做很多事,下面会重点讲解,先来看一下插件控件的这几个流程和系统的差别:
系统应用管理机制示例图
TPF框架插件应用管理机制示意图
插件框架是插件化项目的核心,它运行在宿主应用里面。宿主程序在启动过程中的第一件事就是将插件框架加载好,以便接下来可以运行插件应用里面的业务。
插件框架是插件应用的承载体,负责了插件应用的安装、运行、卸载管理。因插件应用并不是直接安装在系统里面,因此插件框架就必须承载android系统的这一系列能力:
必须自己去识别插件应用并完成拷贝解析工作
必须给插件应用组件赋予android系统正常的生命才能让插件应用正常运行。
必须自己去清理将要卸载的应用数据和正在运行的功能及组件。
剖析TPF框架
下面我就从加载TPF插件框架、安装插件应用程序、运行插件应用程序、卸载插件应用程序四个环节详细讲述一下TPF框架内幕。
加载TPF插件框架
宿主程序在启动过程中的首要事情就是将插件框架加载好,以便接下来可以将插件应用正常的运作起来。插件框架在整个项目工程中扮演的是一个极其核心的角色:除了负责所有插件应用的安装卸载,还需要赋予插件应用组件一个合法的身份。
在android系统中,应用程序运行的背后有很多服务在维持这些组件的运作,比如ActivityManagerService、PackageManagerService、WindowManagerService、NotificationManagerService等以及应用程序背后的ActivityThread等等,这些都是TPF框架需要Hook的范围内容。
具体的流程如下:
为了让插件应用内部的组件合法化,插件框架需要对应用程序做一些HOOK处理,以便让插件的组件能正常运行。
安装插件应用程序
插件应用程序要能够运行在宿主里面,首先得经过安装这个过程让宿主知道当前这个插件应用的信息,然后插件框架就会将当前插件解压拷贝到指定目录以便后面的运行需要。
在TwsPluginFramework框架中,插件包就是一个应用程序apk。对插件信息的收集方式和系统一样,通过解析AndroidManifest.xml来收集应用信息,包括版本、sdk、application、四大组件等等。
具体的流程如下:
这个过程基本和应用程序的安装过程无异,只是插件应用程序的显示图标等内容直接由插件框架在解析的过程中获取并拷贝到私有目录下面。
运行插件应用程序
运行插件内部的任何组件之前,首先得加载好插件的代码和资源,然后就在构建插件的上下文以及Application等信息,TwsPluginFramework框架启动插件的流程图如下:
类加载
在TwsPluginFramework框架中,通过DexClassLoader来加载插件应用的代码, DexClassLoade的使用示意图如下:
TwsPluginFramework框架在构建插件应用的ClassLoader的时候会指定其父ClassLoader为宿主的。这样插件内部就可以直接访问宿主的代码内容。
资源加载
在TwsPluginFramework框架中资源的加载和系统一样,也是通过AssetManager的addAssetPath/addAssetPaths方法进行处理的,只是这两个方法是隐藏的,得用通过反射来调用。
在TwsPluginFramework框架里,在构建插件应用上下文Resource的时候,将宿主的资源与插件的资源合并在一起了。这样做的好处就是插件应用可以共享宿主的资源数据。
对于插件框架来说,如何处理插件资源和宿主资源是一个非常纠结的选择:
然而,资源合并方案就得处理资源ID冲突问题,在TwsPluginFramework框架里面是通过修改AAPT来指定插件应用资源的package id,从而达到区分宿主和插件的资源id的目的。
生命周期
插件应用程序是运行在插件框架这个中间层上面的,而非直接运行在android系统里的。也正因为如此,插件框架就需得自己去完成应用程序包的内容加载以及组件的生命赋予工作。
在Android的世界里面,应用的组件是有“生命”的,比如:activity、service、BroadcastReceive、application等,这种“生命”是由Android系统所赋予的。
对于应用程序来说,只要在AndroidManifest.xml里面注册便可以轻易获得这种生命,因为应用的I(安装)R(运行)U(卸载)是由安装系统来承载的。而对于插件应用的I(安装)R(运行)U(卸载)是由运行在宿主里面的插件框架来承载的。仅因这一点的差别,使得插件应用内部的组件如果不做一些特殊处理,系统是不会给予它们“生命”的。
在TwsPluginFramework框架里面,插件的组件是拥有真正生命周期,完全交由系统管理、非反射代理。插件应用并没有经过系统安装,内部的组件并没有注册到系统里面。那TPF是怎么做到让插件里面的组件也能让系统给没被注册的插件应用组件拥有完整生命周期的?
答案就在TPF框架里面的两个计策: 偷梁换柱、瞒天过海。
瞒天过海:在宿主中提前申明好多个组件,在向系统请求启动的过程中用这些预先申明号的组件去做请求,等系统的校验流程结束后换回成目标的插件组件,从而达到瞒过系统。
瞒天过海环节需要在宿主中申明好用来做替身的receiver、service(多个)[独立进程的单独配置多个]、activity(多个) [不同single模式的单独配置多个]。
偷梁换柱:为了让系统能够按着我们的意愿在组件启时将目标插件组件替换成宿主中预先申明号的对应组件,等系统校验环节过了在换回成目标插件组件,我们就需要替换掉应用程序空间一些重要的处理对象,比如:ActivityThread里面负责应用程序与系统交互的Instrumentation对象以及组件处理流程的回调Handler.Callback等。
下面就以基本组件的启动流程来描述一下这两个计策:
Activity
Activity生命周期大家在熟悉不过了,可是在onCreate之前系统做很多你所不知道的事。
从点击桌面图标(或者出发启动一个activity)到这个应用activity组件进入onCreate()。这个环节是解决插件组件activity完整生命周期的关键。这个环节在TwsPluginFramework框架内部的处理流程:
从开始执行execStartActivity到最终将Activity对象new出来这个过程,系统层会去校验需要启动的activity的合法性[是否有在应用的AndroidManifest.xml里面注册]以及按启动要求创建activity对象。了解了这点就可以很好的绕过系统的约束,达到需要的目的。
Service
stopService、bindService以及sendBroadcast的流程和startService是一样的,这里就不赘述了。
卸载插件应用程序
当前插件应用要下架或者需要更新到新版本的时候,就需要将当前的插件应用给卸载掉。这个过程和Android系统卸载应用程序是一样的。
和插件应用安装过程相反,这个过程就是清理记录在宿主插件框架里面的信息、删除代码和资源同时停止所有该插件正在运行的组件及服务。
流程如下:
显示协议框架
TPF框架将插件在宿主中的调用时机及显示入口完全与宿主解耦,也就是说插件应用的调整不需调整宿主程序的任何代码。这些都归功于TPF提供了一套显示协议框架,插件应用只需要知道显示协议的使用就可以,显示协议(可以根据项目需求自定义,下面是输出给PACEWEAR手表助手插件应用项目的规范) 的概要如下:
显示位置pos: 1 Hotseat; 2 MyWatchFragment; 3 ActionBarMenu; 4 其他
分隔符: # 分割DisplayConfig; @ 分割DisplayConfig的属性; = 属性赋值; / 分割属性值
图标资源icon:统一使用 模块名[hotseat or watch_fragment or menu]描述信息.png 配置在AndroidManifest.xml不需要带后缀。 【normal/focus/press/…】
标题title:中文/英文 也可以只配置一个
显示内容content:如果是fragment 直接配置name,其他的配置类名信息
内容类型ctype:1 fragment; 2 activity; 3 service; 4 application; 5 view
插件启动时机: 1 手动触发 2 随DM启动 3 配对成功后
插件依赖: 1 已安装的app 2 已安装的插件
ActionBar 配置只在显示位置是Hotseat的前提下可用
ActionBar标题ab-title:actionbar标题 中文/英文 也可以只配置一个 暂不支持subTitle
ActionBar右侧按钮显示内容ab-rbtncontent:actionbar右侧按钮点击触发显示内容
ActionBar右侧按钮显示内容类型ab-rbtnctype: 触发显示内容 的类型 1 fragment; 2 activity; 3 service; 4 application; 5 view(当前只支持activity,如果是activity可以不配置)
ActionBar右侧按钮内容ab-rbtnres: 显示在按钮上的内容根据类型不同而不一样(类型1 文本;类型2 图标
ActionBar右侧按钮内容ab-rbtnrestype:1、文本按钮(res配置中英文String) 2、ImageButton(res配置图标)
更多详细的内容请移步到https://github.com/rickdynasty/TwsPluginFramework。
TPF框架给项目团队带来的好处
当前PACEWEAR手表助手项目除宿主应用外还有15个(业务)插件应用,PACEWEAR手表助手仅仅是一个包含基础功能和插件框架的调度平台。后续所有新增加的业务都会议插件应用的方式集成进来,宿主基本不用care到底有哪些业务会集成进来。而且当前PACEWEAR手表助手项目计划将其他两个产品项目合并进来成一个平台产品。这一切的改善很大部分是TPF带来的,下面总结了一下TPF框架的好处:
业务模块完全解耦,不再有调整一个模块而影响到另一个甚至多个模块的情况。
各个业务的插件应用开发、编译各自进行,开发效率大幅度提升,从而缩短开发周期。
业务插件可单独动态更新升级,不需要重启PACEWEAR手表助手便可生效。
对于宿主 — PACEWEAR手表助手来说,可以按需求加载需要的插件应用,这样本来多个相似的产品线就可以合成一个,大幅度降低人力成本。
不再被65535困扰。
团队协作更和谐。
...
TPF框架一路走过的经典Bug
Theme/Style异常
Log截图:
这类问题主要出现在第一套区分资源ID方案(通过public.xml的public-padding特性来处理)上,这类问题的根本原因是:android系统处理应用资源,在底层处理ResourceTable的bag资源的出现了异常。
Android资源管理机制是一个非常复杂的课题(包括:资源打包、资源加载、资源寻找,每一块又分java层和C层),有兴趣兴趣的可以去翻一下源码,在线地址:http://androidxref.com 。简单来说这个问题:“就是style不同于其他资源,style本身是不创建资源的,它仅仅是一个资源的应用集合,而系统访问资源是通过偏移量的方式去获取资源。这种方式在同一个packageID的段来说,只要style是连续的就ok。但是如果不符合这个要求,那上面的问题就会出现。”
在TPF的第一套区分资源ID方案中,通过public.xml的public-padding特性来区分资源id,不难做到让style连续,但要做到多个插件工程并发的情况下做到连续却是基本不可能。这也是为什么TPF放弃了这套方案的原因。
明白了其中的原因,要解决这类问题也就简单了。
解决方案:尽可能的符合系统规则,在同一个packageID段内让相同type的资源ID连续就行。当前通过修改aapt来指定资源的packageID是一个很好的方式。
ClassNotFound
严格来说这个不是TPF框架的问题,TPF框架在处理加载代码上完全是按着系统的规格要求。把这类问题拿出来放这里,只是因为在项目开发过程中插件工程反馈之类问题不较多。
出现ClassNotFound,无非两种情况:1、类被混淆了 2、类不在当前ClassLoader的可视范围内。
解决方案:
混淆的很容有处理,找出来不做混淆就行。
不在ClassLoader可视范围内这个就需要注意一下,插件的ClassLoader父类是宿主的ClassLoader,这个自然就不存在插件内部范文不了的情况。在TPF里面多次出现这个问题的主要原因在共享库的更新上:TPF提供了一套共享库,这套库里面包括了一套控件、宿主基础能力、和手表通讯、网络、文件传输等等一系列共性的内容,在开发阶段难免会对内容进行变更处理,而有些插件工程如果长时间没有更新,那就有可能出现ClassNotFound的问题。这样就需要在调整的时候做兼容,同事插件开发同事及时更新sdk。
Resources$NotFoundException
在TPF里面,插件是可以直接访问宿主提供的共享资源,然而这仅仅只能满足插件内部的逻辑流程。
但在极少特定机型(比如:vivo)里面会比较奇葩的存在这类问题。
解决方案:插件的上下文以及Resources对象(PluginResourceWrapper)都是由TPF构造的。在插件的PluginResourceWrapper内部进行重定向到宿主就可以了。
但对于Notification等这些系统的通用服务也是会出这类问题。这些服务内部通过id获取资源,最终是会落到宿主的上下文上面。而对于宿主来说,插件的资源是不可见,自然就没法通过插件的resID来获取插件的资源。
解决方案:像Notification这类的系统服务,如果需要传递资源id到系统里面进行处理获取资源,一律使用宿主的资源id。
备注:情况②没法用情况①的方式进行处理的原因这里简单描述一下:应用程序在启动的过程中,在application被关联之前Resources就创建好了,而且这个Resources对象在ContextImpl里面还是final类型,这样再java层就没法实施偷梁换柱的方式进行替换处理。
项目进展过程中更多的bug记录请移步:https://github.com/rickdynasty/TwsPluginFramework_Doc
TwsPluginFramework(TPF)框架现已经开源:
https://github.com/rickdynasty/TwsPluginFramework