前言
自2016年8月至2017年10月,今日頭條iOS端已經進行了3輪安裝包大小優化。
2016.08-2016.10第一期優化78MB -> 42MB
(1)刪除Swift代碼,從混編改為純OC編程
(2)無用資源文件清理
(3)工具類、工具方法合并
(4)編譯選項優化
2017.05-2017.07 第二期優化 優化3MB
(1)無用資源文件清理
(2)無用類清理,無用第三方庫清理
(3)C++ pb庫改為light版本
(4)Today Extension改寫
(5)解決cocoapod重復合并資源文件問題
2017.09-2017.10 第三期優化,進行中 優化5MB
(1)無用業務排查下線
(2)正式版下線調試相關代碼
(3)部分資源文件云端下載
(4)使用tint color精簡圖標
(5)OC pb庫切換到google官方編譯版本
在安裝包大小優化的初期,我們主要跟隨網絡上常規的優化思路進行優化,取得了明顯的優化效果。而到了優化后期,明顯冗余可優化的部分已經基本優化完畢,但我們希望還能進一步挖掘、發現優化點,同時控制安裝包大小的增長。
經過探究和實踐,我們還挖掘出了一些原創度較高的優化思路,并取得了不錯的效果,同時實行了一些控制安裝包大小增長的措施。此次我們將重點分享這些優化思路,以及在安裝包優化后期執行的工作和使用的工具,還有一些失敗的經驗。
一. 基礎方法
在安裝包優化過程中,有一些基礎方法幫助我們發掘優化點。無論優化進行到了哪個階段,這些基礎方法都貫穿在整個安裝包優化過程中。
為了便于觀察和統計,在沒有特殊說明的情況下,本文中的“安裝包大小”指的是CI平臺打出的ipa的大小。
1.1 審查安裝包中的每個文件
審查安裝包中的每個文件是最為簡單有效的挖掘優化點的方式,在包大小優化過程中也應被反復執行。
在審查安裝包文件的過程中,我們多次發現了冗余的資源文件,進而發現了cocoapods在合并資源文件時的陷阱;我們發現了extension的大小略高于預期,有精簡空間;還促使一些資源bundle改為了云端下載的模式。
1.2 分析link map
link map是編譯鏈接時可以生成的一個txt文件,它生成目的就是幫助程序員分析包大小。link map記錄了每個方法在當前的二進制架構下占據的空間。通過分析link map,我們可以了解每個類甚至每個方法占據了多少安裝包空間。
在編譯時開啟Xcode build setting中的Write Link Map File開關,Xcode就會生成一份link map文件。
目前已經有不少開源的分析link map的工具,可以輸出每個類、每個靜態庫占用的空間,并進行排序。通過查看link map,我們可以對二進制代碼占據的包大小空間有個直觀了解,同時在引入第三方庫時也可以使用link map作出評估。
針對頭條app,我們還對link map做了可視化更好的分析,這些將在下文中描述。
在優化過程中,我們將優化的重點分為了兩大部分:代碼和資源。針對這兩部分,我們分別挖掘和總結了優化思路、優化工具。
二. 如何進行二進制文件優化
通過審查頭條的安裝包和對比競品的安裝包,我們發現頭條的二進制文件占了相當大的體積(100+MB)。想要優化二進制文件的大小,我們必須精簡代碼。
在精簡代碼層面上,我們主要從兩個思路著手:使用技術手段排查刪減冗余代碼、監控代碼的增長情況和分布。另外優化編譯選項也是行之有效的方法。
2.1 技術手段排查冗余代碼
沒有被引用的類和方法是可以通過技術手段被篩選出來的。
MachO文件中有__DATA.__objc_classrefs和__DATA.__objc_selrefs段,分別近似于“被使用的類的集合”和“被使用的方法的集合”。通過取差集的方式可以篩選出未被使用的類和方法。
2.1.1 排查無用類
使用otool命令可查看__DATA.__objc_classrefs段和__DATA.__objc_classlist段,兩者的差集可以認為是定義了但未使用的類。
不過__DATA.__objc_classrefs段和__DATA.__objc_classlist段中都只提供了類在二進制文件中的位置地址,而沒有提供類名等可讀信息。所以在獲取到差集后,還需要結合
命令的輸出,將地址轉換成可讀的類名。
使用腳本篩選出差集對應的類后,還需要進行一遍人工選擇。因為動態使用的類、從nib或storyboard初始化的類以及在同一個文件中定義的多個類會被誤判為未使用的類。這需要結合業務進行一次梳理。
最終,頭條刪除了70個類,ipa體積減小約300KB。
2.1.2 排查無用方法
所有已經被實現的方法可以通過linkmap來獲取,對linkmap做grep操作即可獲得結果:
而所有已經被使用的方法可以通過對二進制文件逆向獲得。使用otool工具逆向二進制文件的__DATA.__objc_selrefs 段,提取可執行文件里引用到的方法名:
使用這種方法取到的差集,還需要排除掉系統API中的protocol,accessor方法等。
使用這個方法,頭條排查出了無用方法2000余個,總共累積約2MB,其中最長的方法約7KB。考慮到刪除方法的工作量和風險都相對較大,目前我們僅對其中很小一部分進行了刪除。
2.1.3 extension代碼精簡
由于頭條iOS端最低需要支持iOS7,所以頭條中的庫都以靜態庫形式集成。這種集成方式會導致,一旦extension依賴了一些基礎庫,這些基礎庫的占用的體積將會全部算入extension的體積中。
在審查安裝包內每個文件時,我們發現頭條的today extension占用了2MB左右,而extension本身的功能非常簡單,顯然存在著精簡的空間。我們對extension進行了重構,使它盡可能的少依賴基礎庫,盡可能所有功能都用系統自帶的框架完成。最終today extension的大小從2MB縮減為了300KB。
2.2 各業務線對包大小占用的展示和監控
在用技術手段排查冗余代碼的過程中,我們發現了一個問題:用技術手段排查能得到的是“未被引用的類和方法”,而事實上,有很多類和方法雖然被引用了,但實際上是永遠不會執行的。這些方法是無法通過技術手段排查出來的,必須依賴于對業務熟悉的開發人員人工排查。
因此我們轉換了代碼優化的思路,從“排查和刪除冗余代碼”轉向了“對各業務線代碼占用狀況進行摸底和監控”。這有助于我們從全局的角度了解頭條的代碼狀況,找到“性價比”較低的模塊。
2.2.1 各業務線代碼所占安裝包大小可視化展示
進行“摸底和監控”的第一步,是需要展示出各業務線代碼所占的安裝包大小。頭條app內集成的業務方向分為主端、視頻、UGC、商業化、問答、火山直播等,我們的目的是獲得這些業務方向的代碼,分別占用了多少安裝包大小。
要統計代碼對包大小的貢獻,基本的方法是查看link map。網絡上流行著一些分析link map的工具,可以查看每個類、每個靜態庫所占的大小。然而,頭條app業務龐大,類眾多(幾千個),且主要功能都在主工程而非靜態庫中,展示每個類所占的二進制文件大小依然可讀性較差,無法從中看出每個業務占用的大小。
如何將一個類歸類到某個業務線?這時我們想到了一項可以利用的線程的數據:工程的目錄結構。
在頭條iOS端中,由于近半年平臺化的建設和代碼的不斷整理,工程的目錄結構已經比較清晰可讀,各個group的命名也能表明這個group屬于哪個業務線。所以結合工程的目錄結構,我們應該可以較方便的將一個類歸類到某個業務線中去。
于是我們開發了一個腳本,按照工程目錄的樹形結構輸出各個類、各個group的大小占用(下稱“link map分析報表”)。通過一些文本編輯軟件(比如sublime)提供的折疊/展開功能,我們可以方便的查看各個group的包大小占用情況,就像在Xcode上瀏覽工程目錄一樣。
腳本使用開源的ruby工具Xcodeproj讀取工程目錄,使用開源的python腳本linkmap.py讀取link map,獲得類和靜態庫的大小,最后進行整合,輸出link map分析報表。
上圖:link map分析報表展示,呈現出和工程目錄一致的結果,并且能看到每個group的包大小占用情況。
雖然頭條工程中的類有幾千個,但group的數目顯然是非常有限的。人工的對一些group進行合計,我們可以輕松的獲得各個業務線占用的二進制文件大小。
這些數據也成為了我們推動各業務線“刪除冗余代碼、下線廢棄業務、為包大小優化作貢獻”的依據。對于安裝包大小占用特別大的業務,我們會優先進行包大小優化推動。
另外,通過觀察link map分析報表,我們還能發現了一些收益較低但代碼頗多的業務。比如此次我們發現,“動態”業務相關代碼占用的二進制文件大小竟然達到了1.4MB。作為一個準下線業務,這一占用量較為驚人,我們需要推動加速這類代碼的下線。
而在業務方對自身業務進行優化時,link map分析報表也具有指導性意義。業務方可以查看自己業務中哪些子方向相關的代碼占用最多從而優先進行優化。
對各業務線所占包大小進行盤點后,我們推動各業務線對無用的業務和代碼進行了下線,最終優化包大小1.8MB。在這一階段的包大小優化中,1.8MB的代碼優化是比較可觀的。
為了便于及時的產出報告,這一腳本可以集成到工程的Run Script中去,這樣在需要時只用build一下,在構建的最后一步便會執行腳本,產出分析報告。
2.2.2 版本差異監控
在安裝包大小的優化中,我們不僅需要優化當前的安裝包大小,更需要控制包大小的增長。在前幾次的安裝包大小優化過程中,我們有過業務增長造成的包大小增長快過包大小優化量的尷尬局面;而當技術組的重心從包大小優化中移開時,業務產生的增量往往也讓人措手不及。
我們在自動打包平臺增加了一些用于監控版本差異的腳本,用于對比各個版本間包大小的增長量。如果發現當前版本的包大小較上一版本有較大增長,則會以釘釘報警的方式通知給開發人員。
這時我們就需要排查這兩個版本間包大小的增長點。為此我們編寫了一個腳本輔助我們快速排查版本間的差異。
這個腳本執行的工作有:
1、從打包平臺上抓去特定兩個版本的安裝包和linkmap文件。
2、解壓兩個安裝包,對比安裝包中的每個文件,輸出每個文件的差量值
3、解壓兩個asset.car文件,對比安裝包中的圖片的差量,輸出增加/減少的圖片的top x
4、處理和對比兩個linkmap文件,輸出增長/減小的類的top x
最后它可以輸出報表,幫助開發人員快速定位版本間的增長點。
2.2.3 功能分支增量監控
版本差異監控的時機在于合碼完成后。我們經常震驚的發現當前版本的包大小比上一版本增加了4MB,然后慌張的排查增長點。由于此時合碼已經完成,發版迫在眉睫,即便排查到了增長點,也很難督促業務方進行優化。
所以我們需要將包大小增長的監控前置。目前,我們通過對比feature分支和主分支,企圖在需求開發階段就能發現包大小的異常增長。在功能包下載的界面,我們列出了每個功能分支的包大小增量,提醒開發測試人員關注自己負責的需求對包大小的影響。
具體的實現方式是,在CI平臺上,主分支每次出包時,都用包大小在當前commit上打一個tag,比如size/58000000。而feature分支每次出包時,都通過git description命令查詢距離當前commit最近的size tag,進行對比,將包大小增量顯示出來。
這樣每個需求對包大小的貢獻可以一目了然,便于各業務方在需求開發階段及時發現超出預期的包大小增量。
但是這個方式也有弊端:用tag的方式來記錄可以說是比較tricky的,不太符合git這個工具的設計目的。之后我們可能會考慮換一種方式記錄包大小的變化趨勢,比如使用orphan分支。
2.3 編譯選項改進
在精簡代碼之外,對編譯選項做一次審查有時能帶來意想不到的效果。頭條在首次進行包大小優化時,發現由于我們未使用Xcode的archive功能導致正式包中的調試符號依然被保留了下來,直接導致包大小增大20MB。
而在優化的后期,我們又發現了一個可改進的編譯選項:LTO,即Link Time Optimization。
蘋果在2016年的WWDC What’s new in LLVM中詳細介紹了這一功能。LTO能帶來的優化有:
(1)將一些函數內聯化
(2)去除了一些無用代碼
(3)對程序有全局的優化作用
在build setting中開啟Link-Time Optimization為Incremental,經測試可縮減安裝包大小500KB左右。蘋果還聲稱LTO對app的運行速度也有正向幫助。
但LTO也會帶來一點副作用。LTO會降低編譯鏈接的速度,因此只建議在打正式包時開啟;開啟了LTO之后,link map的可讀性明顯降低,多出了很多數字開頭的“類”(LTO的全局優化導致的),導致我們還經常需要手動關閉LTO打包來閱讀link map。
三. 如何進行資源文件優化
比起精簡代碼,資源文件優化思路較多,風險也相對較小。在資源文件優化方面我們做了更多嘗試,有成功的經歷,也有失敗的經驗。由于頭條app內有優化空間的資源主要為圖片,故本文中“資源”與“圖片”可認為是等價詞匯。
在本次包大小優化期間,頭條需要支持iOS7-iOS11之間的系統。與蘋果建議的方式一致,頭條使用asset catalog來管理圖片。絕大部分圖片均為png格式。每種圖片都加入了2x圖和3x圖。以下的優化和討論也將基于這個前提。
對于資源文件的優化,我們主要采用了3個思路:
(1)圖片壓縮
(2)將圖片放置到云端
(3)排查和清除冗余圖片
3.1 圖片壓縮
想要優化資源文件,可能大家想到的第一個優化方式就是壓縮。而更進一步,我們也考慮了是否能用webP等空間占用更小的格式來替換png圖片。
3.1.1 png圖片壓縮
我們嘗試了一個小有名氣的png壓縮工具:ImageOptim。這個工具能夠在不改變圖片質量的情況下壓縮圖片的大小。打開設置,我們能看到和選擇它使用的壓縮算法。
ImageOptim會對每張圖片分別應用以上幾種壓縮算法,然后對比每種壓縮算法產出的圖片,選取最小的那張作為輸出結果。
我們使用ImageOptim對工程中幾乎所有的圖片做了一次壓縮。整個過程持續了若干小時。在壓縮過程中,我們發現,大部分圖片都能被壓縮到原來的70%左右,個別圖片能獲得更高的壓縮比。
ImageOptim的表現無疑是可觀的。然而當我們滿懷期望的提交修改、打包后,得到的結果卻有點出乎意料。雖然工程中的圖片都經過了ImageOptim壓縮,但我們的ipa大小并沒有什么變化。
在查閱了一些文檔后,我們了解到,Xcode在構建的過程中,有一個步驟叫做compile asset catalog。在這個步驟中,Xcode會自行對png圖片作壓縮,并且會壓縮成能夠快速讀取渲染的格式。如果我們對工程中的圖片進行了ImageOptim的壓縮,在compile asset catalog的過程中,Xcode會用自己的算法重新壓縮,而這個”重新壓縮“的過程,相當于將ImageOptim的壓縮“回滾“了,很可能反而增大了圖片。
這也就表明了,無論我們怎么壓縮工程中的png圖片,對包大小優化來說都是徒勞的。(但用ImageOptim工具壓縮jpg圖片還是有效的。)
尋求Xcode中與圖片相關的配置項
我們對png格式沒有做深入研究,所以這里不能清楚解釋這兩個壓縮過程究竟做了什么。但是Xcode讓png圖片“增大”的行為還是讓我們感到不甘,于是我們開始尋求是否有一些配置項能夠關閉Xcode的壓縮過程。
可能的配置項:
Compress PNG Files (COMPRESS_PNG_FILES)
Optimization (ASSETCATALOG_COMPILER_OPTIMIZATION)
經過試驗,Compress PNG Files選項對asset catalog中的資源無效,因為這個選項僅適用于零散資源文件。
Optimization置為space也對包大小沒有任何影響,原因有兩點:
(1)頭條工程使用cocoapods管理,并且命中了cocoapods合并asset catalog的策略,asset catalog的編譯過程在cocoapods生成的腳本中,故build settings中的設置無效;
(2)Optimization參數只對最低支持iOS8及以上的app起作用。具體的分析過程下文中將介紹。
尋求修改構建過程
由于無法用正當途徑跳過、改變Xcode對png的壓縮過程,我們還抱著希望想尋求是否有其他trick的方式能夠阻止Xcode壓縮圖片。
比如我們是否可以在編譯期間插入腳本來干預compile asset catalog的過程?我們是否可以更改build rule來定制對asset catalog的處理方式?
在編譯過程中,我們能看到compile asset catalog這個過程使用的工具是actool。這是一個內置在Xcode里的工具。我們可以在以下路徑中找到actool:
遺憾的是,actool并非一個腳本,而是一個編譯完成的二進制文件。這就導致compile asset catalog的過程變成了完全的黑盒。 我們嘗試了直接將actool工具刪除,但這樣會直接導致無法構建成功。顯然Xcode的設計者不會希望用戶干預它的構建過程。
經過分析頭條工程的構建過程,我們發現,由于頭條的工程使用cocoapods進行了庫管理,并且頭條的工程滿足了一些條件,實際上真正有效執行asset catalog編譯的過程是在[CP] Copy Pods Resources這個腳本中。這也是上文中設置Optimization參數無效的原因之一。
這個腳本調用了actool工具完成了最后一步:
在這里我們可以看到actool的一些參數,改動這些參數是否能改變actool的壓縮策略呢? 遺憾的是,我們測試了去掉--compress-pngs參數、增加--optimization time和增加--optimization space參數,發現這些改動對包大小都沒有任何影響。
為什么增加--optimization time和增加--optimization space參數對包大小沒有影響呢?這個結果顯然非常不符合預期,而網絡上關于ASSETCATALOG_COMPILER_OPTIMIZATION參數的文檔也甚少,讓我們疑惑不已。
通過demo實驗,我們發現,當工程不依賴cocoapods時,build setting中修改optimization選項是有效的,那理論上,在腳本調用actool時傳入--optimization space,應該也能起作用才對。
經過將頭條工程多次與demo對比,最終我們發現了問題的根源:頭條的工程最低支持iOS7,而optimization參數似乎在iOS8及以后才能起作用。在這一輪包大小優化期間,我們還無法放棄iOS7,所以optimization的思路只能就此終止。
如果考慮修改build rule呢?我們是不是可以嘗試使用自定義的工具編譯asset catalog?但經過嘗試,這個方法似乎也行不通。因為build rule是用來處理Xcode不認識的源代碼類型的,并不能改變已有類型的編譯方式。
至此,我們企圖壓縮asset catalog中png圖片的想法暫時就告終了。從這個過程中,我們能看出,Xcode對于png圖片的壓縮方式進行了很強的控制,它似乎不允許第三方開發者干預png圖片的壓縮過程。
3.1.2 使用webP替代png
壓縮實驗失敗后,我們仍然不甘心止步于此。由于開發者難以干預asset catalog內的圖片,一個自然的想法產生了:我們是否能廢棄asset catalog?
廢棄asset catalog可能能帶來以下兩個收益:
(1)可以考慮將png圖片切換到webP等其他格式
(2)廢棄Asset Catalog后,可以刪去2x的圖片,只保留3x的圖片。經過hook改造系統方法的實現,我們驗證了這個想法是可行的。
考慮到app從asset catalog中讀取圖片可能比從bundle中讀取圖片有更高的性能,所以在開發過程中,啟動階段的圖片依然被保留在了asset catalog中。最終10.5MB的asset.car文件被優化成了3.6MB的asset.car+3.6MB的零散資源文件,看起來減少了3.2MB,是一個比較可觀的數值。
app slicing
然而在這一系列優化過程中,我們僅僅關注了內部平臺構建出的安裝包的包大小,而忽視了app store中用戶看到的包大小值。實際上后者才是真正影響到轉換率等核心指標的關鍵。
經同事提醒,我們這樣的優化方法和蘋果提供的app slicing優化有沖突。實驗后我們發現,廢棄asset catalog事實上可能會導致包大小不減反增。
app slicing是iOS9增加的功能。當用戶從app store上下載app時,可以只下載適用于其設備的app架構版本和所需資源,從而減少app所占的空間。
如果開發者想要使用app slicing,只需要將資源文件用Asset Catalog管理,不需要做額外的任何事情。 因此,頭條app已經有了app slicing的效果,用3x的設備查看app store中的“今日頭條”,顯示的包大小比2x的設備大3MB(而沒有使用asset catalog的騰訊新聞,兩個手機顯示的包大小是一致的)
使用Xcode的archive方式構建的安裝包,可以在導出ipa時制定相應的設備,來測試app slicing功能。 嘗試后我們發現,對于2x的設備,廢棄asset catalog反而會導致安裝包增加1MB,而對于3x的設備,廢棄asset catalog能優化安裝包大小1MB。然而顯然,這樣的優化是得不償失的。
整個嘗試廢棄asset catalog的實驗花費了我們大約2周的時間,然而最后以失敗告終。但是這個案例給我們的優化工作帶來了一些反思:
(1)對優化量的衡量,應該站在用戶看到的角度上,而非開發人員看到的角度;
(2)有時蘋果已經為開發者尋求到了一個較優的解決方案,違背蘋果建議的best practise時需要更全面的評估影響
3.2 將圖片放置到云端
將部分圖片放置到云端,等到用戶需要時再去下載,這看起來也是一個優化安裝包大小的方法。我們對蘋果提供的On Demand Resources功能進行了嘗試,也自行開發了資源包下載邏輯。
3.2.1 On Demand Resource
蘋果從iOS 9開始引入了On Demand Resource功能,即一部分圖片可以被放置在蘋果的服務器上,不隨著app的下載而下載,直到用戶真正進入到某個頁面時才下載這些資源文件。
我們考慮可以讓某些業務僅在iOS 9及以后版本中可用,然后應用On Demand Resource來優化這些業務的資源。
經過了一段時間的開發實驗,一切都如同預期,當我們以為On Demand Resource是一個可行的思路時,我們卻發現了一個Xcode巨坑的問題:當工程需要支持iOS9以下系統時,Xcode會在打包完成上傳app store時失敗。On Demand Resource的想法只能擱置。
3.2.2 資源文件云端下載
由于On Demand Resource實驗失敗,我們自行開發了一套云端下載流程,并且對個別個別大圖(幾乎為全屏大小的圖片)進行了嘗試。首批圖片精簡后,安裝包大小減少了1.1MB。
云端下載的策略為:
(1)在若干時機嘗試下載zip圖片包,對zip包進行版本判斷,若云端有更新版本,則根據屏幕是3x還是2x,下載對應的zip包,解壓存入沙盒中;
(2)在讀取圖片時,首先從bundle中讀取,若失敗,則從沙盒中讀取,若依然失敗,則將該圖片當作一個網絡圖片進行請求,確保圖片能被展示。
經過線上測試,大約95%的場景下,用戶可以從沙盒中成功讀取圖片,剩下約5%場景下用戶會將圖片當作網絡圖片來請求。當然這個實驗結論會隨著圖片所在的頁面層級變化。
3.3 排查和清除冗余圖片
資源文件云端下載雖然是一個優化安裝包大小的有效思路,但多少對用戶體驗有一些影響。所以我們又將優化的重點放到了排查和清理冗余圖片上。最后的結果證明,排查和清理冗余圖片的確能帶來客觀有效的優化。
出去正常的排查冗余圖片的流程,我們還在不斷審視安裝包內容時收獲了意外的發現。整個排查和清除冗余圖片可以分為三個方向:
(1)常規的冗余圖片清理
(2)修復cocoapods帶來的圖片重復合并問題
(3)利用tint color精簡單色圖標
3.3.1 常規的冗余圖片清理
隨著業務迭代,有不少圖片成為了永遠也不會使用到的僵尸圖片。這些圖片往往占據著較大空間,對于冗余圖片的排查和清理是包大小優化中便捷而有效的一項優化內容。 頭條iOS端在三輪包大小優化中都進行了冗余圖片排查,每次都能清理出的圖片體現在ipa上的大小都在500KB以上,相對而言是比較可觀的數值。
我們主要使用一個開源的Mac app,LSUnusedResources,來進行冗余圖片的排查: https://github.com/tinymind/LSUnusedResources
這個app的原理是,對某一文件目錄下所有的源代碼文件進行掃描,用正則表達式匹配出所有的@"xxx"字符串(會根據不同類型的源代碼文件使用不同的匹配規則),形成“使用到的圖片的集合”,然后掃描所有的圖片文件名,查看哪些圖片文件名不在“使用到的圖片的集合”中,然后將這些圖片文件呈現在結果中。
對于頭條的工程而言,由于頭條在圖片讀取時有一些字符串拼接邏輯,所以直接使用這個開源工具有一些局限。因此我們修改了這個工具的算法部分,使之更好的適應我們的工程:
(1)考慮夜間模式_night后綴
(2)考慮ipad~ipad后綴
(3)考慮_press、_selected后綴
(4)LSUnusedResources對源代碼中字符串的匹配方式較為激進,容易誤判(最終體現為非冗余圖片被誤判為冗余圖片),故改為了保守的對@"(.+?)"的匹配。
為了保險,用這個工具跑出冗余圖片候選后,還需要依次在工程中搜索確認,才能刪除。
3.3.2 修復cocoapods帶來的圖片重復合并問題
頭條重度使用cocoapods進行庫管理。隨著平臺化的進行,越來越多的代碼被封裝成了pod庫,以庫的形式集成進工程中。在排查安裝包內資源文件的過程中,我們也發現了2個由cocoapod帶來的“圖片重復合入安裝包”的問題。這兩個問題的解決,也給安裝包大小優化帶來了700KB左右的優化。
png文件和asset catalog重復合入安裝包
在排查安裝包內容時,我們發現.app文件的最外層,有一些預期外的零散資源文件。頭條的資源文件絕大部分都是用asset catalog管理,僅有個別圖片以零散png的形式打入安裝包中。這些圖片的出現不符合預期。
經過排查,我們發現這些圖片來自于一個pod庫。而奇怪的是,這個pod庫的確是使用asset catalog進行資源文件管理的,為什么圖片還會以png的形式進入到安裝包中呢?
原來,這個pod庫在編寫podspec的時候,用了這樣的語句指定資源文件:
我們使用demo進行了測試,發現podspec中這樣書寫,會導致asset catalog中的圖片,既作為asset catalog被合并到主工程的asset.car中,也會作為png被拷貝到安裝包中。導致其中一套圖片白白占用了安裝包空間。
在這個例子中,使用通配符來指定pod庫中的資源文件顯然是不合理的,會帶來不可預期的陷阱。應該以白名單的形式明確指定哪些資源文件是pod庫中有效的資源文件。
cocoapods暴力合并工程內asset catalog問題
在更新另一個業務方的pod庫的時候,我們還發現了一個資源文件被重復合入安裝包的問題。
Pod庫在podspec中是這樣指定資源文件的:
在業務方自己的獨立app和pod庫的樣例工程中,這樣指定資源文件沒有任何異常。但是當這個pod庫接入到頭條app中時,我們卻發現包大小的增長超過了預期。
簡單排查發現,這個Image.xcassets中的圖片,既作為了一個單獨的asset.car被放入了名為MyPod的bundle中,又被合并到了主工程的asset.car中,而后者是預期之外的。這導致這些圖片在安裝包中存在了兩份。
究其原因,我們發現原來這是cocoapod的一個缺陷導致的。
在工程構建的最后一步,會執行一個Copy Pods Resources的步驟,該步驟就是執行一個Pods-NewsInHouse-resources.sh腳本,腳本內容在pod install的時候生成。 這個腳本的最末幾行有這樣的一個操作:
即如果工程符合某些條件,則找到工程目錄下所有的xcassets,使用xcode的actool工具將這些xcassets合并為一個assets.car文件。
這里合并的是“工程目錄下的所有xcassets”,也就是說,不管這個xcassets針對的是哪個target,是否被工程使用了,只要它在工程的某個子文件夾下,就會被打包進安裝包中。
顯然這樣的暴力合并可能導致安裝包莫名其妙增大、圖片資源莫名其妙沖突等問題。
暴力合并需要工程符合什么樣的條件?第一行的if語句列出了三個條件:
(1) WRAPPER_EXTENSION是一個環境變量,構建iOS app是一般為app,所以我們工程肯定符合
(2) xcrun —find actool查找xcode的環境中是否有actool工具,我們的工程肯定符合
(3) XCASSET_FILES是一個數組,其中有幾個元素取決于有多少個pod,將xcassets寫到了s.resoures中。這個條件目前頭條的工程符合,而業務方獨立app和樣例工程不符合,所以頭條的工程符合以上三個條件,該腳本會執行暴力合并步驟。
為什么cocoapods需要這樣暴力合并?
因為主工程的xcasset命名不規律,文件存儲位置不規律,cocoapods的開發者也找不到更好的方法來準確合并所有需要的xcassets文件,所以只能采取這種暴力的方式。
如何避免這樣的暴力合并?我們思考是否能通過制定pod庫接入規范來杜絕podspec中resource_bundles的指定方式,但顯然這樣的規范沒有什么合理性,也難以得到業務方的認同。
于是我們轉而思考是否能通過技術手段來填補cocoapod的缺陷?工程中的xcasset的確有無法規范命名、存儲位置不規律的問題,但是它們都屬于某個target,可以通過target來檢索到所有應該合并的xcasset文件。
目前我們執行的解決方案是:在build phase中,在執行copy pods resources之前,執行一個腳本,替換Pods-NewsInHouse-resources.sh腳本的某一行,用更合理的合并方式取代暴力合并。
替換掉的一行是:(這一行會找出工程根目錄下所有的xcassets)
替換為:(get_all_xcassets是我們寫的一個ruby腳本,這一行的作用是利用xcodeproj工具找出當前target的build phase中的copy bundle resources中的所有xcassets)
也就是說,替換后我們不再暴力合并工程根目錄下所有的xcassets,而只是合并當前target需要的xcassets。
回過頭,我們再來分析一下指定resource_bundles和指定resources的區別。
Resource_bundles是cocoapods 0.23.0加入的一個屬性,比起resources,cocoapods官方更推薦使用resource_bundles:
We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not by Xcode.
Cocoapods的文檔中提到了兩點:
(1)使用resource_bundles能大大減小命名沖突的概率
(2)使用resources資源文件是直接拷貝到app中的(對于xib,xcassets等,cocoapods會用腳本進行編譯),沒有經過Xcode的優化(此處指的應該是零散的png)
在構建過程中,使用resources的資源文件,會在構建的最后一步Pods-NewsInHouse-resource.sh中被拷貝到app中。 使用resource_bundles的資源文件,在構建pod時,就已經被合并到bundle中了,最后在Pods-NewsInHouse-resource.sh中這些bundle被拷貝到app中。 放在resouce_bundles中的資源文件,整個構建過程更符合Xcode的構建方式,能應用Xcode的優化,跟進Xcode的版本,所以一般情況下更推薦將資源文件放到resource_bundles中。
制定pod庫資源文件規范
經歷了兩次pod庫資源文件合并帶來的陷阱,我們認為有必要制定一個資源文件接入規范。
對于最低支持iOS7的pod:
我們推薦使用resource配合xcassets的方式來集成各個插件中的資源 具體的做法有:
1、Pod中的資源文件建議使用xcassets組織
(1)xcassets需要添加到podspec的resources中
(2)xcassets中的圖片名,必須使用前綴;(xcassets間的命名沖突會導致讀取的圖片不可預期)
2、如果pod中有資源文件沒有用xcassets組織
(1)這些資源文件必須放入resource_bundles中,禁止放入resource中;(resource_bundle中的資源在構建期能經過Xcode的優化,而resource中的資源不能)
對于最低支持iOS8的pod:
我們推薦使用resource_bundles配合xcassets的方式來集成各個插件中的資源文件。 具體的做法有:
1、Pod中的資源文件建議使用xcassets組織
(1)xcassets需要添加到podspec的resource_bundles中
(2)Pods中的代碼,在讀取圖片資源時,使用imageNamed:inBundle:compatibleWithTraitCollection:讀取(該方法最低支持iOS8),無法使用imageNamed:讀取
2、如果pod中有資源文件沒有用xcassets組織
(1)這些資源文件必須放入resource_bundles中,禁止放入resource中;(resource_bundle中的資源在構建期能經過Xcode的優化,而resource中的資源不能)
如果該pod不需要支持iOS7,則更支持使用后者方式,這樣做的優點有:
(1)各個pod管理各自的資源文件,不會有命名沖突的問題
(2)能利用蘋果的app slicing功能
(3)防止cocoapods暴力合并所有xcassets的引起其他潛在問題
3.3.3 利用tint color精簡單色圖標
在瀏覽了安裝包內所有的圖片文件后,我們產生了一個直觀的感受:由于頭條有日夜間模式,導致大部分圖標都切出了日間和夜間兩套圖標,而這兩套圖標的形狀是完全一致的,只是顏色有差異。
如果能結合tint color對單色圖標做一次精簡,對安裝包大小和圖標的管理都有正向的影響。
tint color是蘋果在iOS7推出的功能,我們可以讀取一個圖標,然后給它賦予一個color值,在手機屏幕上它就能顯示出相應的效果。tint color適用于對單色圖標進行著色,相比于其他精簡圖標的解決方案,tint color方便、可靠、擁有原生支持。
精簡圖標的工作需要各業務端共同參與,可以預計將消耗較大的人力成本。為了盡量減輕業務方的負擔,我們提前做了一些預備工作,包括篩選、色值抽取、色號匹配、分配到人等。這些工作均使用腳本完成。
最終我們篩選出了大約3MB、1500+張形狀重復的圖片,理想情況下可以精簡掉其中的一半。
最后我們將候選圖片以這樣的文件名輸出:
文件名中包含了精簡所需要的全部信息,便于業務方接入。
為了將圖片中的有效信息抽取出來放在文件名中,我們提前做了以下這些工作:
(1)獲取ipa內的全部圖片
使用工具 iOS Images Extractor可以幫助解壓asset.car文件,獲得ipa內全部圖片。
(2)篩選“形狀一致”的圖片
由于我們對圖像處理并沒有做深入研究,所以使用了一個拍腦袋想出來的樸素方法:獲取一張圖片所有像素點的alpha值,alpha值完全一致的兩張圖片,就是“形狀一樣”的圖片。
我們使用了ImageMagick這個工具抽取圖像的每個像素點值,然后對所有alpha值做md5計算。經過目測,使用這個方式來篩選形狀一致的圖片還是比較有效的。
(3)獲取單色圖片的色值
使用ImageMagick工具,抽取圖像每個像素點值,排除掉全透明的點,然后找到色值的眾數,則可以認為是該單色圖片的色值。
(4)獲取圖片的色號
擁有了色值之后,有些app可能就可以直接用色值來做后續開發了。但是頭條app中不允許使用色值,必須使用UI規范中的“色號”,比如“面1”、“字1”之類的。
同時我們希望矯正那些“有一點偏差”的色值。
下圖底色為標準色,而icon的顏色其實并不是標準色,有一點差,但是肉眼基本看不出來,可能是設計師在作圖時手抖了。這種情況下我們就需要做“矯正”。
這個問題也可以表述為:如何將一個色值匹配到與它最接近的標準色上?
對圖像沒有研究,經過一番google,我知道了這個命題的關鍵字叫做“color distance”,于是又一番google,得知了一些公式,比如:http://colormine.org/delta-e-calculator/
最后找了一個開源的工具:http://chir.ag/projects/ntc/
這是一個js的工具,能將一個色值匹配到與它最接近的某個顏色名稱上。
于是我直接將頭條的標準色色值給復制到了原碼里。于是這個腳本可以完成的工作是:輸入隨便一個色值,輸出與之最接近的頭條標準色色值。
最后,如何將標準色值再映射到“字x”、“面x”呢,這就需要找到一張圖的日間模式和夜間模式,然后用兩個標準色值去找色號名字。
(5)將圖片分配到人
精簡圖標的工作需要各業務方來推動,所以在做準備工作時,我們需要將每個圖標分配到各業務方。由于僅憑肉眼很難判斷一個圖標是屬于哪個業務的,所以我們使用了git log作為分發依據,以誰添加誰負責為原則。
對于指定的圖片名,我們首先使用mdfind命令找出它所在的路徑,然后讀取git log,查詢到該圖片的添加者,完成分類。
使用tint color著色,不僅能精簡掉形狀相同的夜間模式圖標,可能對日間模式圖標還能帶來優化空間。
在使用tint color著色后,單色圖片自身的顏色(RGB色值)便失去了意義。圖片提供的全部信息實際就只有alpha通道的信息。在這種情況下,考慮將圖片轉為灰度圖可以進一步縮減圖片體積。
整個tint color的接入工作還在進展過程中。
總結
任何優化項目都會經歷一個越來越難以突破的過程。在安裝包優化的過程中,我們也從單槍匹馬的挖掘優化點,到形成了監控和量化體系。優化一個app可以給一個app帶來轉化、留存上的收益,而總結出一套優化方法并推廣出去,則可以給更多的產品線帶來收益。
未來我們也會持續總結優化方法,形成方法論和工具,讓公司的其他產品也得到受益。甚至我們可以開發一套診斷腳本,一鍵得知某個app的可優化點。將優化工作推進到一個更高的層面。
最后,附上頭條安裝包大小優化的工作項和收益: