安卓应用开发的大量难题,其实最后都需要插件技术去解决。
现今插件技术的使用非常普遍,比如微信、QQ、淘宝、天猫、空间、携程、大众点评、手机管家等等这些大家在熟悉不过的应用都在使用。
插件技术可以给项目开发带来巨大的好处,比如:并行高效开发、模块解耦、解除单个 dex 函数不能超过 65535 的限制、动态更新升级、按需加载等等。
本文的目的是从一个典型的复杂项目中总结出较为全面与完整的安卓插件技术。
掌握好插件技术,需要如下的安卓基础和相关知识,例如: 1. Android 应用程序安装,加载过程 2. Android 应用运行机制,生命周期调用原理 3. Android 应用资源编译打包原理 4. Android 应用读取资源原理 5. Android 系统 AMS、PMS、NMS 等系统服务的运作原理 6. 增量更新 7. HOOK 等技术
插件技术知识领域如图:
这些技术中每一个点都需要大篇幅内容才能完全讲清楚。不过,好在 Android 是开源的,每一个插件技术涉及到的技术点都可以翻阅源码进行进一步的研究。下面我从当前所负责的一个插件化项目 (PACEWEAR 手表助手) 经历,来梳理一下插件技术的应用及核心内容。
PACEWEAR 手表助手原自腾讯 TOS 的智能穿戴项目。
因为目前大部分智能手表和手环还不能独立联网通讯,须通过蓝牙连接手机,借助手机的网络来完成一系列业务功能。PACEWEAR 手表助手就是这么一个手机软件,帮助智能穿戴设备使用手机网络,并通过蓝牙连接的方式完成对智能穿戴设备的各种配置和管理。
PACEWEAR 手表助手项目开始初期,业务并没有大面积铺开,三四个工程师还算跑的比较顺利,随着项目的进展,主工程框架、登录、配对、设置、ota、市场、天气、地图、运动、音乐、健康管理、支付、应用管理、表盘管理等功能不断加入,参与的人也慢慢变多,问题也就多了起来,维护越来越困难,总结有如下几点:
针对以上问题虽然我们考虑过动态加载 jar、html5 等措施来缓解,但最终还是没能彻底从根本上解决这些问题,一直在苦恼着整个项目团队...
这种情况下我们很快意识到需要引入插件化的开发模式,才能一劳永逸地这解决这一系列问题。
团队在 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 手表助手项目完成插件化并且让所有插件业务都符合产品需求稳定的运行,对插件框架要求首先就需要做到基于框架开发的插件应用功能对齐原生,这样框架就需要:
同时需要这套框架支持将宿主的基础能力:设备账号信息、和手表通讯、统计上报、文件传输、网络、ota、控件库及宿主的资源共享给插件应用;
另外需要将插件运行时间及在宿主中的显示与宿主完全解耦。不然插件的调整必然要影响到宿主的代码调整,这可不是一个明智的落地方案。
综合上面的要求及项目进行过程中的调整,经过进一个月的努力,这套框架终于预研成功,正式应用到 PACEWEAR 手表助手项目上。
这套框架就叫 TwsPluginFramework 框架 (后面简称 TPF 框架,已经开源:https://github.com/rickdynasty/TwsPluginFramework)。
这套框架相比业界其他插件框架能力对比如下:
另外 Hook 系统服务的安全隐患是不可预知的,因此 TwsPluginFramework 框架尽可能少的对系统服务等进行 hook 处理。
插件技术的实现原理是源于 Android 系统(Android 系统本身就是一套插件框架,运行在这个系统之上的应用就是一个个的 "插件应用")对应用的管理机制:安装 (Install)、运行 (Running)、卸载 (Uninstall)。
运行在 TPF 框架之上的插件应用和 android 应用程序又有所不同,不同点主要有下面几点:
上面三个流程中安装、卸载基本和系统的处理方式是一样的。而运行就一样,插件应用程序的运行需要经过 "插件框架" 这个中间层进行合法化后才能运行在系统里面,这个合法化过程就需要做很多事,下面会重点讲解,先来看一下插件控件的这几个流程和系统的差别:
系统应用管理机制示例图
TPF 框架插件应用管理机制示意图
插件框架是插件化项目的核心,它运行在宿主应用里面。宿主程序在启动过程中的第一件事就是将插件框架加载好,以便接下来可以运行插件应用里面的业务。
插件框架是插件应用的承载体,负责了插件应用的安装、运行、卸载管理。因插件应用并不是直接安装在系统里面,因此插件框架就必须承载 android 系统的这一系列能力:
下面我就从加载 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 生命周期大家在熟悉不过了,可是在 onCreate 之前系统做很多你所不知道的事。
从点击桌面图标 (或者出发启动一个 activity) 到这个应用 activity 组件进入 onCreate()
这个环节是解决插件组件 activity 完整生命周期的关键。这个环节在 TwsPluginFramework 框架内部的处理流程:
从开始执行 execStartActivity 到最终将 Activity 对象 new 出来这个过程,系统层会去校验需要启动的 activity 的合法性 [是否有在应用的 AndroidManifest.xml 里面注册] 以及按启动要求创建 activity 对象。了解了这点就可以很好的绕过系统的约束,达到需要的目的。
stopService、bindService 以及 sendBroadcast 的流程和 startService 是一样的,这里就不赘述了。
当前插件应用要下架或者需要更新到新版本的时候,就需要将当前的插件应用给卸载掉。这个过程和 Android 系统卸载应用程序是一样的。
和插件应用安装过程相反,这个过程就是清理记录在宿主插件框架里面的信息、删除代码和资源同时停止所有该插件正在运行的组件及服务。
流程如下:
TPF 框架将插件在宿主中的调用时机及显示入口完全与宿主解耦,也就是说插件应用的调整不需调整宿主程序的任何代码。这些都归功于 TPF 提供了一套显示协议框架,插件应用只需要知道显示协议的使用就可以,显示协议 (可以根据项目需求自定义,下面是输出给 PACEWEAR 手表助手插件应用项目的规范) 的概要如下:
- 显示位置pos:1Hotseat;2MyWatchFragment;3ActionBarMenu;4其他
- 分隔符:# 分割DisplayConfig; @ 分割DisplayConfig的属性; = 属性赋值; / 分割属性值图标资源icon:统一使用 模块名_[hotseat or watch_fragment or menu]_描述信息.png 配置在AndroidManifest.xml不需要带后缀。 【normal/focus/press/...】
- 标题title:中文/英文 也可以只配置一个
- 显示内容content:如果是fragment 直接配置name,其他的配置类名信息
- 内容类型ctype:1fragment;2activity;3service;4application;5view
- 插件启动时机:1手动触发2随DM启动3配对成功后
- 插件依赖:1已安装的app2已安装的插件
- ActionBar 配置只在显示位置是Hotseat的前提下可用
- ActionBar标题ab-title:actionbar标题 中文/英文 也可以只配置一个 暂不支持subTitle
- ActionBar右侧按钮显示内容ab-rbtncontent:actionbar右侧按钮点击触发显示内容
- ActionBar右侧按钮显示内容类型ab-rbtnctype: 触发显示内容 的类型1fragment;2activity;3service;4application;5view(当前只支持activity,如果是activity可以不配置)
- ActionBar右侧按钮内容ab-rbtnres: 显示在按钮上的内容根据类型不同而不一样(类型1文本;类型2图标
- ActionBar右侧按钮内容ab-rbtnrestype:1、文本按钮(res配置中英文String)2、ImageButton(res配置图标)
更多详细的内容请移步到 https://github.com/rickdynasty/TwsPluginFramework。
当前 PACEWEAR 手表助手项目除宿主应用外还有 15 个 (业务) 插件应用,PACEWEAR 手表助手仅仅是一个包含基础功能和插件框架的调度平台。后续所有新增加的业务都会议插件应用的方式集成进来,宿主基本不用 care 到底有哪些业务会集成进来。而且当前 PACEWEAR 手表助手项目计划将其他两个产品项目合并进来成一个平台产品。这一切的改善很大部分是 TPF 带来的,下面总结了一下 TPF 框架的好处:
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 是一个很好的方式。
严格来说这个不是 TPF 框架的问题,TPF 框架在处理加载代码上完全是按着系统的规格要求。把这类问题拿出来放这里,只是因为在项目开发过程中插件工程反馈之类问题不较多。
出现 ClassNotFound,无非两种情况:1、类被混淆了 2、类不在当前 ClassLoader 的可视范围内。
解决方案:
在 TPF 里面,插件是可以直接访问宿主提供的共享资源,然而这仅仅只能满足插件内部的逻辑流程。
备注:情况②没法用情况①的方式进行处理的原因这里简单描述一下:应用程序在启动的过程中,在 application 被关联之前 Resources 就创建好了,而且这个 Resources 对象在 ContextImpl 里面还是 final 类型,这样再 java 层就没法实施偷梁换柱的方式进行替换处理。
项目进展过程中更多的 bug 记录请移步:https://github.com/rickdynasty/TwsPluginFramework_Doc。
TwsPluginFramework(TPF)框架现已经开源: https://github.com/rickdynasty/TwsPluginFramework
来源: http://blog.csdn.net/tencent_bugly/article/details/70577599