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

相關文章
相關標籤/搜索