【腾讯Bugly干货分享】Android 插件技术实战总结

本文来自于腾讯Bugly公众号(weixinBugly),未经做者赞成,请勿转载,原文地址:https://mp.weixin.qq.com/s/1p5Y0f5XdVXN2EZYT0AM_Ajava

前言

安卓应用开发的大量难题,其实最后都须要插件技术去解决。android

现今插件技术的使用很是广泛,好比微信、QQ、淘宝、天猫、空间、携程、大众点评、手机管家等等这些你们在熟悉不过的应用都在使用。git

插件技术能够给项目开发带来巨大的好处,好比:并行高效开发、模块解耦、解除单个dex函数不能超过65535的限制、动态更新升级、按需加载等等。github

本文的目的是从一个典型的复杂项目中总结出较为全面与完整的安卓插件技术。安全

掌握好插件技术,须要以下的安卓基础和相关知识,例如:微信

  1. Android应用程序安装,加载过程
  2. Android应用运行机制,生命周期调用原理
  3. Android应用资源编译打包原理
  4. Android应用读取资源原理
  5. Android系统AMS、PMS、NMS等系统服务的运做原理
  6. 增量更新
  7. HOOK等技术

插件技术知识领域如图:网络

这些技术中每个点都须要大篇幅内容才能彻底讲清楚。不过,好在Android是开源的,每个插件技术涉及到的技术点均可以翻阅源码进行进一步的研究。下面我从当前所负责的一个插件化项目(PACEWEAR手表助手)经历,来梳理一下插件技术的应用及核心内容。架构

项目的困惑

PACEWEAR手表助手原自腾讯TOS的智能穿戴项目。并发

由于目前大部分智能手表和手环还不能独立联网通信,须经过蓝牙链接手机,借助手机的网络来完成一系列业务功能。PACEWEAR手表助手就是这么一个手机软件,帮助智能穿戴设备使用手机网络,并经过蓝牙链接的方式完成对智能穿戴设备的各类配置和管理。app

PACEWEAR手表助手项目开始初期,业务并无大面积铺开,三四个工程师还算跑的比较顺利,随着项目的进展,主工程框架、登陆、配对、设置、ota、市场、天气、地图、运动、音乐、健康管理、支付、应用管理、表盘管理等功能不断加入,参与的人也慢慢变多,问题也就多了起来,维护愈来愈困难,总结有以下几点:

  1. 工程频繁报方法数超65535
  2. 多个模块在同一个app中开发代码耦合,架构冗余,牵一发而动全身
  3. 人员效率低下,时间每每花费在沟通,构建问题处理上
  4. 分工不明确,灰色地带重复逻辑比较多
  5. 业务与业务之间互相调用,不够独立
  6. 问题跟进原来越繁琐,牵扯人数众多
  7. 功能愈来愈多,目前这种开发方式不可持续
  8. 链接的手表和手环设备种类愈来愈多

针对以上问题虽然咱们考虑过动态加载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手表助手项目完成插件化而且让全部插件业务都符合产品需求稳定的运行,对插件框架要求首先就须要作到基于框架开发的插件应用功能对齐原生,这样框架就须要:

  1. 支持application、四大组件(activity4个LaunchMode)、so、fragment、notification、toast等基础能力
  2. 支持联调插件应用
  3. 支持加载本地网页等
  4. 支持插件自定义控件和样式
  5. 组件进程配置等原生应用程序的能力;

同时须要这套框架支持将宿主的基础能力:设备帐号信息、和手表通信、统计上报、文件传输、网络、ota、控件库及宿主的资源共享给插件应用;

另外须要将插件运行时间及在宿主中的显示与宿主彻底解耦。否则插件的调整必然要影响到宿主的代码调整,这可不是一个明智的落地方案。

综合上面的要求及项目进行过程当中的调整,通过进一个月的努力,这套框架终于预研成功,正式应用到PACEWEAR手表助手项目上。

这套框架就叫TwsPluginFramework框架(后面简称TPF框架,已经开源:https://github.com/rickdynasty/TwsPluginFramework )。

这套框架相比业界其余插件框架能力对好比下:

另外Hook系统服务的安全隐患是不可预知的,所以TwsPluginFramework框架尽量少的对系统服务等进行hook处理。

TPF框架原理

插件技术的实现原理是源于Android系统(Android系统自己就是一套插件框架,运行在这个系统之上的应用就是一个个的”插件应用”)对应用的管理机制:安装(Install)、运行(Running)、卸载(Uninstall)。

运行在TPF框架之上的插件应用和android应用程序又有所不一样,不一样点主要有下面几点:

  1. 应用程序的安装有android系统负责完成,而插件应用的安装流程由插件框架负责完成;
  2. 插件应用没有走系统的安装流程,组件等信息没有被注册到系统里面,要使插件应用能正常的运行,插件框架须要将这些插件应用内部的组件所有“合法化”;
  3. 插件应用的卸载也不走系统的卸载流程,而是由宿主负责完成的。

上面三个流程中安装、卸载基本和系统的处理方式是同样的。而运行就同样,插件应用程序的运行须要通过“插件框架”这个中间层进行合法化后才能运行在系统里面,这个合法化过程就须要作不少事,下面会重点讲解,先来看一下插件控件的这几个流程和系统的差异:


系统应用管理机制示例图


TPF框架插件应用管理机制示意图

插件框架是插件化项目的核心,它运行在宿主应用里面。宿主程序在启动过程当中的第一件事就是将插件框架加载好,以便接下来能够运行插件应用里面的业务。

插件框架是插件应用的承载体,负责了插件应用的安装、运行、卸载管理。因插件应用并非直接安装在系统里面,所以插件框架就必须承载android系统的这一系列能力:

  1. 必须本身去识别插件应用并完成拷贝解析工做
  2. 必须给插件应用组件赋予android系统正常的生命才能让插件应用正常运行。
  3. 必须本身去清理将要卸载的应用数据和正在运行的功能及组件。

剖析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:一、文本按钮(res配置中英文String) 二、ImageButton(res配置图标)

更多详细的内容请移步到https://github.com/rickdynasty/TwsPluginFramework。

TPF框架给项目团队带来的好处

当前PACEWEAR手表助手项目除宿主应用外还有15个(业务)插件应用,PACEWEAR手表助手仅仅是一个包含基础功能和插件框架的调度平台。后续全部新增长的业务都会议插件应用的方式集成进来,宿主基本不用care到底有哪些业务会集成进来。并且当前PACEWEAR手表助手项目计划将其余两个产品项目合并进来成一个平台产品。这一切的改善很大部分是TPF带来的,下面总结了一下TPF框架的好处:

  1. 业务模块彻底解耦,再也不有调整一个模块而影响到另外一个甚至多个模块的状况。
  2. 各个业务的插件应用开发、编译各自进行,开发效率大幅度提高,从而缩短开发周期。
  3. 业务插件可单独动态更新升级,不须要重启PACEWEAR手表助手即可生效。
  4. 对于宿主 — PACEWEAR手表助手来讲,能够按需求加载须要的插件应用,这样原本多个类似的产品线就能够合成一个,大幅度下降人力成本。
  5. 再也不被65535困扰。
  6. 团队协做更和谐。
  7. ...

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,无非两种状况:一、类被混淆了 二、类不在当前ClassLoader的可视范围内。

解决方案:

  1. 混淆的很容有处理,找出来不作混淆就行。
  2. 不在ClassLoader可视范围内这个就须要注意一下,插件的ClassLoader父类是宿主的ClassLoader,这个天然就不存在插件内部范文不了的状况。在TPF里面屡次出现这个问题的主要缘由在共享库的更新上:TPF提供了一套共享库,这套库里面包括了一套控件、宿主基础能力、和手表通信、网络、文件传输等等一系列共性的内容,在开发阶段不免会对内容进行变动处理,而有些插件工程若是长时间没有更新,那就有可能出现ClassNotFound的问题。这样就须要在调整的时候作兼容,同事插件开发同事及时更新sdk。

Resources$NotFoundException

在TPF里面,插件是能够直接访问宿主提供的共享资源,然而这仅仅只能知足插件内部的逻辑流程。

  1. 但在极少特定机型(好比:vivo)里面会比较奇葩的存在这类问题。
    解决方案:插件的上下文以及Resources对象(PluginResourceWrapper)都是由TPF构造的。在插件的PluginResourceWrapper内部进行重定向到宿主就能够了。

  2. 但对于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


更多精彩内容欢迎关注腾讯 Bugly的微信公众帐号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!

相关文章
相关标签/搜索