140M到67M,学而思网校如何在一周内构建一套可持续的瘦身系统

APP为何要减包?

APP体积越大推广转化成本越高,由于平台功能众多,学而思网校的APP体积是在144m左右,疫情期间因为公益直播涌入大量用户,转换率上的硬伤更加暴露出来。同时移动部设定了自我突破的若干指标,转换率是关键指标,背负紧急军令咱们开始了减包任务,必定要作到70m。python

为何不用插件化?

19年团队曾经尝试过插件化技术,通过两个项目试水碰到一系列问题,最终放弃使用插件化,缘由以下:web

  1. 插件技术原理是经过Hook或者Reflect技术修改系统libs和framework代码,Android系统版本 设备 ROM众多,Hook Reflect很难100%兼容。算法

  2. 学而思网校平台有20+的二级工程,一个工程变动从新打包时,插件资源id的从新分配,总体工程变动致使20多插件变更须要从新维护,维护人力成本有点大。json

  3. 插件技术使用时存在数据传递问题 自定义UI显示问题,权限重复申请等问题。浏览器

  4. 插件化的核心是ClassLoader,按照谷歌的文档,最快Android 12将会被限制, 将来有不肯定性。缓存

减包计划实施难度?

  1. 涉及到20+的二级工程 资源类型众多 调用代码分布普遍,要求在底层框架统一实现核心技术。
  2. 须要兼容Android4.4到最新的版本系统,同时核心技术兼容后续系统迭代。设备上须要兼容各个手机品牌的高中低,兼容任务繁重。
  3. 产品迭代迅速,为了不后续开发致使APP慢慢助长,须要设计统一的技术框架保持持久轻量。
  4. 整体开发时间一周,测试一周,各个业务线还在并行开发,为了保障时间节点,技术框架须要作到最小的业务代码代动。

减包前APP体积汇总。

ttc.01.png

经过数据统计发现,20多个工程的res图片资源 assets的lottie动效资源 libs下的so文件合计约有70m。其余零散的100kb文件有6m左右。20多个二级功能,其资源一次性打进APP里是不合理的,毕竟用户经常使用的就那么几个。为何不把资源分离出来托管到云端,使用时再拉取呢?想法很简单,可是面临一系列的问题,咱们有6000多张图片,托管CDN的话,业务代码都要修改访问连接不现实。一个想法在心里产生,能够作一个离线附件的技术框架嘛。微信

附件框架的方案

附件框架:开发时资源打进APP不影响业务方开发调试预览;发版时指定的资源统一分离出来托管到云端,进入对应功能前确保资源包下载完了,运行阶段不受影响。文字虽短,框架层须要支持一下特性:网络

  1. 资源分离须要作到脚本自动化,而且只分离指定目录的资源文件,分离出来的zip应该是多个,而且和20多个工程造成一一对应关系。架构

  2. 资源下载须要作到按需下载,进入哪一个功能下载哪一个资源,避免一次性所有下载致使的loading时间太长。为了减小loading出现,须要根据业务权重作后台预加载机制。框架

  3. 框架层面在保证按需下载的前提下,实现业务层面的统一拦截下载,以免大量的业务代码修改和调试,作到业务方无感知框架。

  4. 之前资源在APP内,附件框架的资源在下载后,框架代码须要作到全方面的资源访问替换技术,以免大量的业务代码变更,作到业务层面无感知。

  5. 考虑到存量用户基数大,各个业务版本迭代资源变更小,为了进一步避免或减小loading出现的几率时间,附件框架能够作增量更新技术。保证存量用户更新资源时,资源包体积减小95%。

  6. 20多个离线zip增量迭代10个版本,会产生上百个资源文件,对应的人力维护成本也大。须要配套的自动化附件包发布脚本,一是减轻负重,二是避免人为性失误。

  7. 框架须要考虑失败重试机制 须要作到多云备份预防网络事故 须要作到内置外置卡双存储避免极端状况。须要完整的日志链条以持续优化。

资源分离技术说明

ttc.02.png

  1. 首先规定了附件目录attach, gradle脚本会给每一个二级工程生成该目录。业务方只须要把lottie so以及其余大文件移动到附件目录,不须要修改代码。

  2. Jekins打release包时,分离脚本启用了,gradle脚本会自动遍历二级工程:每一个工程res下的图片文件会打到zip,源文件会用xml文件占位替换,每一个工程的attach文件会打包到zip中。

  3. 最终Jekins产生了20+的zip文件,打包完成后命令行运行脚本,自动化发布资源文件到云端。

资源发布自动化技术

ttc.03.png

  1. 批量编译点九图 确保APP使用时无失真拉伸
  2. 批量使用熊猫 WEBP技术对图片文件优化 以减小资源体积
  3. 自动对比历史版本归档记录 产生对应的增量更新文件
  4. 同时发布多个资源包到案例云和腾讯云 双云避免网络事故

使用python脚本自动化发布作到人力不及的流程,避免了相似于插件化维护的管理成本。

抽象统一的下载框架

ttc.04.png

  1. 底层框架统一拦截跳转,肯定须要进入的二级模块,检查下载对应资源文件,下载后继续跳转。统一实现了20+业务的核心代码,避免业务改动。

  2. 下载环节作到网络错误感知,阿里云腾讯云自动切换,4次失败重试避免网络事故。文件存储时优先内置卡,次要外置卡存储,避免极端的文件读写问题。

  3. 框架层面统一文件管理,版本迭代管理,避免修改业务代码。同时增量更新确保用户最小的下载量。

资源访问的无缝替换

附件资源分离作到自动化 发布作到自动化 下载作到了抽象统一。再作到无缝替换技术,基本上业务代码变动就很微小。所谓无缝替换,就是从关键接口层面统一APP内置资源 下载资源的访问。核心技术一处实现,业务代码无需变动。下面列举res无缝替换 lottie无缝替换 Glide无缝替换。

ttc.05 (1).png

ttc.07.png

如你所见,无缝替换技术是重写关键接口而非Hook的方式,这让网校APP作到100%兼容;从内核层面进行流替换技术,一处变动全场景生效,避免了大量的业务改动。

祛除Unity 3D内核的历程。

ppte.jpg

在APP多个业务中,互动环节要显示3D粒子效果的机器人,阿丘之类的动画。由于制做3D粒子效果的成本比较大,团队起初定的技术方案是采用Unity 3D渲染模型。发现Unity 3D自己是很出色的特别是对于游戏,可是对于咱们网校APP这个大平台而言,却不是那么合身,缘由以下:U3D的library bin文件占据着15M的APP体积;U3D是不开源的碰到一个手机崩溃无从解决;载入释放U3D内核内存须要5秒产品体验差;使用U3D时内存多开销170m。这种场景让想起几年前在使用Cocos渲染时,为了减小40m的内核库,竟然花费了一周时间精简编译Cocos的艰辛历程。这种场景表明某种尴尬:为了特效引入了一个过重的技术方式,这种技术没法作到轻量化,不大适合平台化的APP。

偶然在使用一个录屏软件时,产生点灵感,3D特效复杂若是设计动画帧成本太大因此设计部不接受,若是咱们作个截屏小工具,运行这些特效连续截屏,截取指定区域,生成动画帧,网校APP直接使用程序截屏的动画帧,就能够祛除U3D Cocos这种重量级内核了吧,毕竟用户看的是屏幕,产品要的是实现了而不是怎么实现。抱着试一试的心态,开始编写这个工具,中途也遇到了些问题。

  1. 时间平滑问题:动画效果很重要一点就是帧之间的时间平滑度,起初的程序控制设定在30ms一帧采集,可是发现实际的采集结果有的是30ms,有得是200ms,时间平滑度出入太大效果不理想。经过时间数据采集,发现采集后编码PNG时间,文件IO时间变更,中间又有系统内存回收致使的。再次修改采集方法,采用双线程模型加高缓存策略,保证了时间平滑度在30ms左右。
  2. 祛除背景问题:截屏窗体采用纯白背景0XFFFFFFFF,设想对截屏图片使用程序去除白色部分,然而发现有些色素是有Alpha通道的。理论上讲白色能够和任意Alpha通道色值混合成目标色值。这就意味着还原Alpha通道色值有些不现实,再次陷入困境。。。查阅了颜色混合公式 Dst = (Src * Alpha + (256 – Src.Alpha * Alpha / 255) * Dst ) / 255, 联想到对于同一帧若是分别采用白色背景和红色背景,利用混合模式对比不就能还原出色素的Alpha和RGB值嘛。因而再次修改采集程序,一个动做分别用红色背景和白色背景采集,生成两套动做。编写类似度算法分别找出每一帧的红色图和白色图,反向色素混合,果真能还原Alpha通道和RGB值~
  3. 祛除噪点问题:在祛除背景还原Alpha通道后,自觉得没问题了,后来发现少许图片有零星噪点,深刻分析代码发现,每一帧的白色帧和红色帧不是100%的吻合,图片边缘合起来对比仍是有那么一两个像素的偏差。开始各类尝试解决这种偏差,祛除噪点,最终找到合适的算法,相似于卷积思想:以白色为基础帧,红色为对比帧,还原白色(X Y)的色素时,经过红色(X Y)周围9个点卷积还原,质量无损失,噪点完美祛除~

解决三面三个问题,Unity 3D截取转动画实现了,每一个动做帧生成时间在4分钟左右。后续编写独立的动画组件把内存控制在15m之内,成功在两个项目中实际应用。本次瘦身方案采用这个策略,祛除掉了Unity 3D内核减掉15m体积,功能依然知足,成功达成目标!本次减包的主要方案就是资源分离下发,祛除Unity 3D,顺便删除少许冗余资源,媒体库合并等方式。

提醒:能够理解作了个工具,能够截取指定区域的画面,经过算法生成了设计级别的动效,这种方式能够应用在多个场景,好比cocos等其余特效技术替换。

咱们遇到过哪些困难?

踩坑一:怎么分离drawable/image附件

安卓最多见的图片是drawable/image,系统调用的方式就那么几种,实现起来会相对轻松些。先从drawable分离着手,开发Android的小伙伴都知道,gradle在编译时会把drawable/images存放在build目录下。起初想添加一个脚本,编译时把这些drawable/images图片替换成占位小文件。通过两天的重复试验,虽然脚本替换成了小占位文件,可是APP编译失不经过了,没办法只能去查阅Gradle编译流程,发现一旦Gradle完成编译前准备,随意更改build是不行的,其中编译环节过多再也不赘述。编译中替换不行,那就换成编译前替换试试看。修复脚本,以工程为单位,识别sourceSet.res,把sourceSet.res copy出一份新的目录,命名为dir。替换dir中全部的drawable/images为占位文件,编译前动态重置sourceSet.res = dir成功了。通过两天多的探索,初步找到图片分离占位的脚本方式,开头还算能够~

踩坑二:怎么无缝替换drawable/image

这个技术是最关键的环节,只有作到无缝才能确保不须要变动各业务代码,从底层确保质量。按照起初设想,进入某个功能前下发本模块的zip文件并解压,显示drawable时无缝替换掉,实际显示占位文件描述的真实图片。为实现无缝替换技术,浏览Android Framework的系统源码,发现可使用Drawable Tag扩展,扩展ReplaceDrawable新类,在xml文件定义 <com.parentsmettins.drawable.ReplaceDrawable file=“project/imagePath”/>,系统内核会反射package包下的ReplaceDrawable实例,能够在实例化载入真实图片显示,运行起来还不错,不用修改业务代码,就能无缝替换显示。忍不住爽了下,赶忙在云平台选择不一样的设备和系统测试兼容性,几台手机崩溃了。失落之余发现,这些手机广泛在6.0如下系统,开始漫长的下载Android各类版本的FrameWork源码作对比, 最后确认:Drawable Tag扩展特性在6.0之前的系统版本是不支持的!想到断定属于6.0如下的系统,Hook Resouces类Cache的get方法扩展支持Drawable Tag,又开始漫长的Resouces Hook测试验证工做,终于算支持6.0如下的系统了,随后在两个独立模块中测试无缝替换显示技术,妥妥的。然而应用到第三个工程测试,APP奔溃了。。。追踪下去发现有个混合drawable载入ReplceDrawable Tag时报错,那个业务的混合drawable使用到了没法Hook的API,这样的API还有几处。困难的工做老是这么意外,暂停编码,再次浏览系统代码。结论以下:不能使用Hook方式兼容,由于总会有不能Hook的地方,实现必须遵照Android标准这样才能稳妥。回顾了Framework对于BitmapDrawable NinePathDrawable的全部API,找到 标准兼容方式。就是修改占位文件内容以下 ,同时重写Resources类的流读取方法,实现方式是获取资源id的类型,若是是xml文件,判断是否有file属性,有就认为是占位文件,返回file指定的已下载文件流。这种全新的方式既遵照Android标准,也不须要Hook,完美兼容各类drawable调用场景。由于咱们的资源描述是标准的Android API,各类版本都支持,替换是从最底层的流层面完成的,各类API追踪都适用。完成这个最核心的无缝替换显示技术,隐约感受到方案是可行的!

踩坑三:怎么无缝显示lotties/image

APP第二大资源是丰富的lottie动效,动效执行环节可能要修饰渲染素材,这样的动效场景遍及各个模块而且数量巨大,不一样伙伴的调用还有很多差别。打包时分离到zip附件中轻松实现,可是无缝替换有些困难。起初设计方式是提供一套兼容API给各个业务方,各个业务方修改自身代码适配。刚开始实施,各个业务方反馈修改代码太多,完成兼容API替换会耗费大量时间,出现BUG的可能性也随之提升,调用兼容API方式实施困难,调整技术方案作到相似drawable/image的无缝实现很是必要。又开始耗费时间阅读lottie源码,发现内核代码会根据images路径和data.json信息从assets中寻找素材文件,猜测能够在lottie内核层面重写资源寻址实现,优先从下载目录中寻址,最终技术验证经过。由于不须要修改对应功能代码,本来计划多人一周的lottie方案,在一天内完成了。这个细节也提醒了咱们,熟悉源码思想的重要性,技术层面深刻一点多想一点,总体工做量小不少。

踩坑四:为何附件library执行崩溃

随着drawable lotties分离无缝接入成功,基本完成了编译链 发布链脚本,也能够把so等library库采用统一的流程来作呀。随后添加library的分离流程,载入so时采用Compat的方式从本地存储卡载入,本觉得是个简单的事情,发现几乎全部的手机执行so程序崩溃。。。

又开始追踪各个系统System.load(path)的源码实现,发如今高版本的系统中,Android的权限更加严格,特别是执行权限。起初library下载到/Android/data目录下,这个目录是没有执行权限的,修改成/data/data目录下,该目录有执行权限,解决了这个问题。

踩坑五:怎么构造抽象统一的下载

目前学而思不少业务中有很多下载代码,下载校验,文件管理等,若是离线资源,20多个业务都要添加下载代码,这对于精简代码很是不利,还须要测试成本确保质量。起初发现几乎全部的模块跳转都在架构组设计的Dispatcher类中实现,便设计在个业务的Dispatcher入口处拦截并下载对应功能资源。忙碌了20多个小时修改了这么多业务的Dispatcher类并检查,跑码测试,忽然发现有个模块的没有资源拦截和下载,致使整个功能素材显示出问题。CR整个代码,发现跳转除了Dispatcher 还有少许的Arouter Scheme 以及原生的Start方式,最初的想法不全面还修改了业务代码,只能回退梳理代码流。发现无论Arouter Dispatcher Scheme最终都调用了Activity的startActivity方法,查阅Android系统的Activity源代码肯定能够用参数Intent的ComponetName来判断要跳转的模块,临时拦截跳转下载本模块资源。由于各个模块的package都是prefix + businessName方式,这为咱们抽象实现20多个业务资源下载提供了可能。编码完毕后,测试起来还不错。然而在全功能测试流程中,又碰到了loading不显示,或者进入直播时直接失败,追踪下去原来是绑定下载服务失败,主要是跨进程问题还有系统差别问题,再次对比不一样版本的Service差别,修正下载服务代码支持跨进程问题。自觉得方案没啥问题,又遇到从学习中心进入模块时,没有走到拦截流程,缘由是拦截代码写在Base类中,绝大部分的业务都继承了Base类,少许的业务没有继承Base类,为了不人为疏漏就编写代码检查脚本,编译时检查全工程的业务Activity若是不是继承基类,就报错中止编译提醒业务方修改继承。有了这个脚本检查,确保了无遗漏才敢进入下一个技术环节。

踩坑六:非离线的首页素材显示问题

在咱们的方案设计中特殊模块工程不分离资源,好比首页,发现,我的中心,其余独立模块是分离附件离线的。应用方案后发现首页等模块少许的素材显示有问题,只能再次开启埋坑之旅。发现出现显示的问题的素材,其名称和其余分离工程的素材重名,gradle打包时选择了占位文件,而首页的原始图片不会编译到APP中。若是与首页资源重名的工程资源还没下发,框架代码找不到下载文件,会显示纯黑 或者纯蓝。由于不知道这种重名资源有多少个,又开始编写脚本统一检查,发现156处重名,共计312个素材!耗费大半天一个个修更名称避免重名,好在这些drawable类修改后,code也能快速识别出来修改资源符。

踩坑七:浏览器WebView怎么崩溃了

在测试中意外发现,应用技术方案后,在WebView中长按,程序崩溃。让人陷入懵逼状态,APP只是无缝替换显示离线资源,WebView只是加载URL连接也不会使用本地资源,怎么会崩溃?事情作到这个地步只能去排查,又开始艰辛的阅读webkit源代码。原来长按WebView时,webkit要弹出选择菜单,菜单的素材是在系统中,在载入WebView组件时,系统Resouces实例会把webkit的素材路径加入进来。起初咱们为了作到无缝替换重写并替换Resources实例,重写后没有载入webkit素材路径致使资源找不到崩溃, 而APP又无法获取不一样版本不一样手机的webkit素材路径一时陷入混沌。通过屡次尝试验证,咱们发现不能简单重写Resources,应该采用装饰者模式重写,这样访问APP资源时返回已下载文件流,访问其余资源如webkit素材,采用System原有的Resources实例实现,这样解决了问题。

踩坑八:Glide为何显示不了本地素材

熟悉Glide的伙伴们都知道,Glide是图片加载显示框架,能够包括url图片,文件图片,APP本地素材等。按照开始的设想,Glide会调用Resources实例载入本地素材显示,咱们的Resources实例重写过能够确保替换显示占位drawable/image,测试中发现一旦使用Glide载入本地素材,就显示一片空白,为避免修改众多的业务代码致使测试周期拉长,又开始埋头阅读Glidde源代码。熟悉内核代码后发现,Glide载入本地图片不是使用Resources实例,而是Uri定位符,Glide之因此这么写是为了统一代码框架便于扩展。认真阅读Glide扩展规则,重写了Local Uri方法,优先从已下载文件中寻址素材,返回 File Uri解决了问题。

踩坑九:自动化打包脚本的编写历程

若是以为资源发布管理还算问题嘛,不就是上传下配置下嘛,请看看起初的经历。绝大部分工做完成后,着手准备20多个zip文件,计算低版本增量更新包,,获取各个zip文件的md5,最后把这么多信息写进配置文件里,上传到云端。就这么简单的人力工做,耗费了大量的时间精力,作完了内心还忐忑不安,若是手动发布配置出错,线上必定出事故,还须要考虑不清楚技术细节的小伙伴也能快速发布依赖附件包。这种场景相似于当初尝试插件化碰到的问题,非技术问题:版本迭代管理成本。

考虑打Release包时经过Jekins托管,打包完毕后Jekins上已经输出20多个业务的zip文件,为何不写个Python脚本,命令行运行,自动发布附件包到云端?有了想法开始各类倒腾,首先配置Jekins Web环境确保HTTP能够访问,Python脚本大约流程以下,按照配置清单从Jekins上下载20多个工程的zip附件,对比历史版本zip附件产生低版本增量包,计算各包md5校验值,批量自动化上传到OSS,汇总各个文件连接 校验信息 增量信息产生config文件在发布到云端。通过3天反复的编码,测试确保脚本OK了,开始使用完整的流程。一切看似正常,忽然发现若干素材显示变形失真了。再次埋头去定位问题,发现失真的图片是 ninePatch图片,熟悉安卓的小伙伴知道ninePatch是特殊的png图片,在studio中按照规则编辑边缘就能使用最小尺寸的图片显示大尺寸确不失真。想这种特殊图片必定在正常编译中有特殊处理,再次开始研究gradle编译流程,发现对于ninePatch素材,gradle会调用aapt程序计算chunk信息保存在图片的metadata中,那python是否能够调用aapt工具对附件的ninePatch素材进行编译呢,又耗费精力在Python脚本中加入aapt编译再次尝试,问题解决了。自我感受是没问题了,然而几天后运行Python脚本时发现,整个运行了2个多小时才发布完毕。。。又开始逐步调试,发现随着迭代版本增多,计算6500多张图片增量包IO操做太多,最终优化算法减小IO次数解决问题。

方案能成功的经验总结

1.基于Android 标准接口重写,避免Hook技术得到很好的兼容性,特别是后续系统兼容上。

2.发版阶段不须要各个业务方独立打附件包,而插件化的方式须要独自打附件包

3.在资源下载更新上咱们作到了存量用户增量更新,而插件化的方式没法作到

4.除了技术自己咱们作到了打包 发布 优化 增量等环节的自动化实现,节约迭代成本

5.咱们在图片资源替换显示上作到了无缝替换,最大程度的下降了业务代码修改量

6.方案实施完毕后,后续的新增项目和需求再也不致使APP持续增加,长期稳定。

7.咱们在构造下发框架作到抽象统一 针对Bug修改时也在底层完成兼容,下降成本

8.释放了开发资源,大规模的自测确保质量。

虽然咱们砍掉了一大半的体积,可是持续减包,持续减小资源体积,优化产品体验还须要坚持下去。后期进入深水区,能够推荐以下研究方向:

  1. 短时间拆分直播工程,把本来50m的直播资源分散开来,进入不一样的直播课时loading的时间会更少。
  2. 中期项目组须要筹划混淆实施方案,尽可能统一素材,动效统一,在UI设计上最大化统一。同时考虑脚本化分析代码,祛除无用代码,统一类似代码。
  3. 长期考虑dex优化,目前考虑到APP的稳定性,没有对dex启动混淆。
  4. 补充优化,能够考虑引用运动适量还原技术替换现有的帧动画 gif动画,大约能减小60%的动效体积。
  5. 补充优化,研究轻量超分重建,难度大收益大
  6. end

做者简介

袁威为好将来高级Android工程师III

招聘信息

好将来技术团队正在热招测试、后台、运维、客户端等各个方向高级开发工程师岗位,你们可扫描下方二维码或微信搜索“好将来技术”,点击本公众号“技术招聘”栏目了解详情,欢迎感兴趣的伙伴加入咱们!

也许你还想看

DStack--基于flutter的混合开发框架

WebRTC源码分析——视频流水线创建(上)

"考试"背后的科学:教育测量中的理论与模型(IRT篇)

公众号底图.png

相关文章
相关标签/搜索