干貨!京東商城iOS App瘦身實踐

吳雙熔 王啟啟· 2019-12-18
本文來自 京東零售技術 ,作者 吳雙熔 王啟啟
隨著業務的快速增加,商城App的大小也在迅速增加,一度超過了300M。安裝包大小的不斷增加對App下載成本,推廣效率產生了比較大的影響。從2018年9月份我們對商城App開始了為期二期的專項瘦身工作:一期從V7.2.0-V7.5.2版本,共計瘦身46M(設備:iPhone X,iOS12)。為了進一步減小包大小,同時為了建立長效機制,從今年5月份開始了第二期的專項優化工作,二期優化從最高的V8.1.0版本的272M到現在的V8.4.0的214.4M共計已經完成瘦身57.6M,當然二期優化還在繼續推進中。瘦身工作非常不易,在本次安裝包瘦身過程中我們遇到了不少坑,同時也積累了些經驗,在此分享給大家。

優化方向

01 優化指標

itunes connect上有兩種包大小顯示:“Download Size”,“Install Size”?!癉ownload Size”即下載包大小,超過150M需要使用無線網下載的限制就是這個大?。ìF在已經放寬到200M);“Install Size”即安裝后占用的磁盤空間大小,在appstore上顯示的也是這個大小,用戶往往會誤認為這是下載安裝包消耗的流量大小。所以一開始我們就將“Install Size”作為了優化指標?!癐nstall Size”減小后,“Download Size”自然也會減小。

02 優化方向

ipa 解壓后JD4iPhone.app主要的成分如下:
  • Frameworks,動態庫存放路徑;
  • PlugIns,插件存放路徑,如today extension;
  • Mach-O,可執行文件;
  • Assets.car,Asset Catalog編譯產物;
  • react.bundle,內置的ReactNative業務;
  • bundle,主要存放資源文件;
  • 其他文件;

這些都是包大小的主要影響因素,優化工作都是圍繞這些元素進行,后續的監控點也主要是這些元素。

優化措施

01 資源文件

在一期優化前通用包(包含多種指令集,分辨率圖片,多種機型通用的包)中的圖片在 13500張左右,二期優化開的V8.1.0 版本通用包中的的圖片還有 11000 張以上。資源文件的數量非常之多,使用場景異常復雜。資源文件的優化自然成了我們花費精力最多的地方,我們需要一整套的方案去應對接下來的優化。

1.1 資源文件的歸屬
在優化之前,我們需要將不同的資源文件歸屬到對應的模塊,落實到對應的負責人。得益于商城App高度的模塊化,資源文件的歸屬、甚至獲取模塊代碼大小變得很簡單:
  • 根據資源文件在本地工程project.pbxproj所在模塊的路徑和ipa包中的資源文件進行匹配。
  • 分析 linkmap 文件,獲取各個靜態庫組件代碼部分在可執行文件中的占比。

1.2 單個資源文件大小統計
以往,我們常常以為本地開發工程中的資源文件大小就是最后安裝包中的大小,將本地圖片壓縮一下就可以瘦身。而經過本次瘦身實踐發現其實并不是這樣:很多圖片在本地和安裝包中的大小差異非常之大,往往相差幾倍,甚至有幾十倍的。通過調研知道 Apple 為了在優化 iPhone 設備讀取 png 圖片速度,將 png 轉換成 CgBI 非標準的 png 格式:
  • extra critical chunk (CgBI)
  • byteswapped (RGBA -> BGRA) pixel data, presumably for high-speed direct blitting to the framebuffer
  • zlib header, footer, and CRC removed from the IDAT chunk
  • premultiplied alpha (color' = color * alpha / 255)

蘋果的優化對于大多數應用來說都是包大小的負優化,商城也不例外。所以簡單的壓縮(有損,無損)處理并不能達到很好的瘦身效果。而經過測試,以下文件會被負優化:
  • 放在根目錄下png格式的圖片。
  • 放在Asset Catalog中的png,jpg格式的圖片,其中jpg會轉成png。

放在根目錄下的jpg,bundle中的png不會被優化,這個規律也在后續優化中起到了重要作用。
終上所述,我們決定使用安裝包中的資源文件來統計大小。

1.2.1 Asset Catalog中的文件大小計算
Assest.car 做為 Asset Catalog 的編譯產物,我們怎么獲取到car文件中的圖片大小呢?在以前查iOS9,P3格式圖片問題的時候我們用過蘋果提供的assetutil工具,使用assetutil就能獲取到圖片信息描述:
sudo xcrun --sdk iphoneos assetutil --info xxx/Assets.car > xxx/Assets.json
Assets.json 中的圖片詳細數據如下:
  {
    "AssetType" : "Image",
    "BitsPerComponent" : 8,
    "ColorModel" : "RGB",
    "Colorspace" : "srgb",
    "Compression" : "lzvn",
    "Encoding" : "ARGB",
    "Idiom" : "universal",
    "Image Type" : "kCoreThemeOnePartScale",
    "Name" : "1001", //xxx.imageset 的文件名
    "NameIdentifier" : 11584,
    "Opaque" : false,
    "PixelHeight" : 48,
    "PixelWidth" : 72,
    "RenditionName" : "[email protected]",//工程文件中的實際圖片名
    "Scale" : 3,
    "SHA1Digest" : "E34FCAC314E26DE7FF30442AA33E436B242AA4BA",
    "SizeOnDisk" : 800,//占用的磁盤大小,Asset Catalog中的圖片編譯后的大小取該值。
    "Template Mode" : "automatic"
  },

最終我們使用SizeOnDisk 字段來獲取圖片大小。使用SizeOnDisk計算精度很高(所有圖片的SizeOnDisk相加和car文件大小誤差在1M以內)。

對于 Assets.car 的分析,還有個小插曲:我們最開始是使用 cartool 導出圖片,然后統計圖片大小。分享模塊在更換雙十一大促氛圍兜底圖后,因為部分活動圖片大于了32KB(微博分享縮略限制不能超過32KB),觸發調用兜底圖分享邏輯,分享兜底失敗,最后定位是因為 Apple 的負優化,將原大小為22KB,負優化后部分設備上大于了32KB,但是cartool導出的大小為18KB。定位后發現:一個Asset Catalog的圖片在Assets.car中實際上根據不同設備有4張對應的圖片,大小不同,但名字相同。而cartool解壓出的圖片大小為其中某一張,這樣大小計算就不太準確了。所以放棄原先使用cartool,改用assetutil。

1.2.2 其他的資源文件大小計算
其他的文件大小計算方式相對簡單,蘋果的APFS文件系統的最小存儲單元為4KB,即使只有幾十字節大小的文件,占用的空間也是4KB。對于安裝包里面的獨立文件我們使用4KB對齊的方式進行大小計算,有些大點的文件磁盤占用空間并不是4的整數倍,但大小相近,影響不大:
Math.ceil(size/4000.0)*4,size為文件實際大小,單位字節;
在 MB、KB、Byte 之間的換也是對齊 Apple 使用的是 1000 而不是 1024(即1MB = 1000 KB = 1000*1000 Byte)。

1.3 模塊所用資源文件大小,數量統計
在介紹模塊的資源文件大小統計前,先簡單介紹下 App Slicing ,在我們將ipa包提交到iTunes connect,App store會針對不同的設備,系統制作成不同的精簡版app:可執行文件,動態庫根據不用的指令集,Asset Catalog中的資源文件根據不同的屏幕分辨率進行分發,最終做到按需下載,如下圖。

image.png

關于App Slicing 的內容詳細敘述,感興趣的可以查看App Thinning in Xcode。
App Slicing只會對在Asset Catalog的資源文件進行分發,而放在根目錄,bundle中的資源文件不會分發,所以在統計模塊所使用資源文件之前我們需要注意到這個特性。如果以通用包來統計模塊使用的資源文件大小、數量,其實并不能真正反映此模塊對整個安裝包大小的影響。所以我們決定使用單個設備來衡量資源文件使用情況。目前我們選擇iPhone X,iOS11設備做為參考標準。
要統計單個設備的資源文件使用情況,一個方式是使用adhoc包導出支持單個設備的安裝包統計,不過這樣的方式需要每次集成后都需要單獨打包,因為現在ci并不會出支持單個設備的包。后來我們發現assetutil除了可以導出car文件信息之外,還可以從通用包car文件導出指定設備的car文件,入參較多,經過嘗試iPhone X的如下:
sudo xcrun --sdk iphoneos assetutil --idiom phone --subtype 570 --scale 3 --display-gamut srgb --graphicsclass MTL2,2 --graphicsclassfallbacks MTL1,2:GLES2,0 --memory 1 --hostedidioms car,watch xxx/Assets.car -o xxx/thinning_assets.car
從上文知道car以外的資源文件不會分發,獲取指定設備的car文件后我們就可以計算出模塊所用資源文件大小,數量。

1.4 資源文件優化

1.4.1 大資源文件優化
上文提到了蘋果對圖片的負優化,大圖經過負優化后對安裝包的大小影響更大,動輒幾百K,甚至上M。這也是一期優化通過改造 Assets.car 中的 183 張圖片能優化了近 30M 原因,千萬不要將大圖隨意拖到工程中。
結合上文中負優化規律,改造處理方案如下:
  • 刪除無用或者可以使用其他方案替換的圖片;
  • 優先轉網絡下載,使用默認圖/純色兜底,如樓層背景圖;
  • 不能轉下載的使用壓縮過的jpg格式圖片。
  • 不能使用jpg的圖片經過壓縮后( 主要是tinypng有損壓縮)后放到 bundle 中使用。

二期優化開始,對大資源的處理不在局限于 Assets.car 中的大圖(大于50KB),對于放在 bundle 中的大圖、音視頻、模型文件針對這部分大文件,逐一梳理后并針對性處理,收益很高。

1.4.2 無用圖片篩查
現有基于源碼檢測無用圖片的原理:根據各種類型的源文件,通過正則表達式獲取使用的圖片集合,掃描獲取所有圖片名集合,取所有圖片集合和使用圖片集合差值獲取無用圖片集合;
但是源碼的方式在商城App中并不不適用,因為商城App的各個模塊是以二進制的形式集成的,而我們并沒有所以模塊的源碼權限。既然掃描源碼的路走不通,又不能放任不管,我們反其道而行通過安裝包來掃描無用圖片:
通過分析安裝包中使用圖片可分為三類文件:
  • 可執行文件;
  • 可讀文件(.plist、.js、.html);
  • 不可讀文件(.nib、.storyboardc);

可執行文件通過 otool -v -s __TEXT __cstring 獲取可執行文件中的 __TEXT.__cstring 段。__cstring 包含了可執行文件中的字符串常量(源碼中的 @“xxx” 字符串);
不可讀文件 .nib 和 .storyboardc 分別是 xib 和 storyboard 的構建產物。ibtool 是xib 和 stroyborad 的編譯工具,通過 man 查看 ibtool 的具體使用方法發現:--flatten NO --compile 組合使用的時候可以生成可運行、可執行的 .nib 和 stroyboardc 文件。
可執行性文件、不可讀文件確定處理方法后開發工具篩選,思路如下:
1)針對不同的文件,使用 otool、正則和直接讀取將獲取到的內容拼接成引用圖片的超字符 str,遍歷所有圖片名是否被 str 包含;
2)如果包含直接過濾;
3)如果不包含,再判斷是否是 image_%ld 相似圖片過濾;
4)開發人員確定無誤后刪除,存在部分字符串拼接被誤掃的,添加白名單過濾。
最后篩選出無用圖 196 張刪除,總大小約1.5M。無用圖片掃描是個長期工作,我們也會郵件定期推送。

1.4.3 轉下載
在進行大圖,無用圖片處理的同時,我們也給出了便于本地圖片轉下載的方案,基本功能如下:
? 模塊內內置默認配置文件,支持對不同分辨率的機型加載對應的圖片。
  • {
        "imageId1": {
            "3x": "url1",
            "2x": "url2"
        },
        "imageId2": {
            "3x": "url3",
            "2x": "url4"
        }
       .
       .
       .
    }
  • 支持圖片url的在線更新。
  • 支持基于cdn的圖片降質、webp壓縮。

哪些圖片適合轉下載?
  • 功能性引導圖
  • 背景圖:如樓層背景,頁面背景。
  • 標簽,提示類的圖片。
  • 其他入口較深的圖片。

1.4.4 iconfont
即使經過了圖片轉下載,無用圖片刪除,但是工程中的圖片數量還是極為可觀,其中各種各樣的icon圖標占了不少的數量。為了進一步減少圖片數量,我們引入了iconfont方案, iconfont優點:
  • 矢量,縮放不失真。
  • 可以設置顏色。
  • 接入成本低,不需要引入額外的類庫。

iconfont 可以解決因為icon大小,顏色不同而重新切圖的窘境。從京東內部的quark平臺了解到目前已經可以很好的支持iconfont,我們在一個模塊就找到了55個icon并且成功轉成了iconfont。不難看出iconfont是一個能減少圖片數量的好方案。

1.4.5 規范與監控
為了建立長效機制,我們擬出了資源文件使用規范,同時也搭建了資源分析系統,來跟蹤各模塊的資源使用情況,主要功能如下:
  • 安裝包大小,資源文件大小數量,包成分的展示;
  • 各模塊(JDReact,動態庫,靜態庫)模塊資源文件使用情況的記錄展示;
  • 各模塊排名,不同版本間的對比;
  • 違反資源文件使用規范模塊的郵件觸達(開發中);
  • 根據各模塊的資源文件使用情況,動態給出優化建議(開發中);
02 可執行文件/動態庫

2.1 動態庫
由于iOS8對可執行文件__Text字段60M限制,商城App多個版本逼近60M大關,為了不影響發版,有不少模塊以動態庫的形式集成到工程中(只有2個庫會啟動時加載,其他均為使用時加載,不影響啟動速度)。以優化前的V8.1.0 版本為例,共計19個動態庫,總大小超過了100M(包含arm64,armv7架構)。所以動態庫的優化成了瘦身工作的重要組成部分。那么怎么給動態庫瘦身呢?

  • 梳理動態庫使用方,精簡代碼
目前商城App中動態庫主要有三類:1)公共基礎庫,這類庫在代碼上的優化點并不多,因為基礎庫可能被多個App使用;2)可執行文件限制被迫轉的動態庫,這類庫是基于已有基礎庫打包的,已經完成代碼共享,優化點也不多。但是我們也梳理出來個別下線業務;3)第三方公司提供的庫,這類庫中雖然有很多的重復代碼,但是推動修改成本較大。經過各種嘗試雖然有所減少,但是精簡代碼的方式整體收益并不好,未能達到我們的預期,我們需要其他的瘦身方式。
  • strip

經過調研,我們了解到有兩種去除動態庫多余符號(符號表等)的方式:
1)在鏈接時去除,即在動態庫工程中Other Linker Flags中添加-s參數,經過測試:不管是在啟動時加載,還是手動方式加載動態庫都沒問題。于是準備使用這個方案。然而,在執行的時候發現了一個嚴重的問題:加了此參數后,不能生成完整的dsym文件,這會影響崩潰后符號的解析。于是此方案作罷。
2)使用strip -x命令處理動態庫。因為是對動態庫產物進行處理,所以不會對dsym產生影響,經過測試,strip后的動態庫,也可以使用dsym文件找到符號。于是我們嘗試在工程中添加腳本統一處理工程中的動態庫。在添加腳本的時候遇到個問題:動態庫被拷貝到沙盒的時候會簽名,而我們的strip操作發生在這個后面。在debug環境下,加載動態庫的時候會提示簽名后動態庫被修改的錯誤。而在release導出包的時候會重新對動態庫進行簽名。所以在release下不會有問題。最終,我們修改了腳本,只在release環境下,執行strip操作:
if [ $CONFIGURATION == Release ]; then 
    strip -x dylib路徑 
fi
經過strip處理后共計減少28M(arm64+armv7),瘦身效果明顯。

2. 2 無用類/方法
無用類通過 otool 逆向Mach-O文件 __DATA.__objc_classlist段和__DATA.__objc_classrefs 段獲取所有 OC 類和被引用的類,兩個集合差值為無用類集合,結合 nm -nm 得到地址和對應類名符號化無用類類名;根據商城的限制做過濾,規則如下:
  • otool 逆向 __DATA.__objc_nlclslist 獲取實現 load 方法的類過濾(RN與原生的橋接類、Swizzle Method 類);
  • 通過 otool 逆向 __TEXT.__cstring 獲取所有字符串常量,過濾通過 NSClassFromString 調用的;
  • 子類實例化,父類沒有實例化,父類不會出現在中 __objc_classrefs,通過 otool -oV 逆向出類的繼承關系,過濾出子類被實例化(NSClassFromString 調用),父類沒有實例化(NSClassFromString 調用)的類;
  • 過濾使用 Plist 文件引用的類;

無用方法 通過 otool 逆向 __DATA.__objc_selrefs 段獲取使用到的方法,通過 otool -oV 獲取實現的所有方法取差值。然后過濾掉 setter、getter、系統方法和協議、自定義的協議、sel 調用。
結合 linkMap 映射出無用的方法和類歸屬的組件,并且初步量化大小,如下所示:

image.png

因為基礎組件中的無用方法和類,不能確定是否被非商城的 App 使用,只能對業務組件優化,考慮到涉及組件眾多,并且收益和工程量不成正比,并且刪除方法風險比較大,將無用方法和類優化的優先級降低。

2.6 內置的ReactNative業務
JDReact提供了預置和后裝兩種發布方式,而為了用戶體驗,大部分業務模塊都選擇使用預置包的方式。時間一長,文件的數量就越來越多。由于文件系統的4K對齊,對包大小的影響也是非常大。對內置的ReactNative業務優化如下:
? 推動流量相對較低的模塊(三級及三級以上頁面)轉后裝方式;
? 根據資源文件使用規范,推動業務整改;
這部分的工作量主要在和業務方的溝通,經過部分模塊轉后裝后,瘦身效果也是很明顯。

03 插件

在ipa包中我們也注意到了PlugIns目錄,這里主要存放一些插件,比如today extension,share extension等,雖然這些插件在整個ipa包中的大小占比不大,但是我們還是決定梳理下有沒有優化點。梳理后發現這些插件對于一些基礎類庫(網絡框架,圖片加載框架等)的使用都是以拷貝代碼的方式加到工程中。我們知道這些類庫完全可以和主app共享,因為主app中這些庫是以動態庫的形式使用的。經過優化后,成功的將today extension的大小減少了0.9M(嗯~蚊子雖小...)。

04 Xcode配置

除了以上提到的優化點,我們也對Xcode對包大小優化的一些相關配置做了嘗試:

4.1 Link-Time Optimization(LTO)
蘋果在WWDC2016對LTO的介紹如下:
What is Link-Time Optimization (LTO)? Maximize runtime performance by optimizing at link-time Inline functions across source files Remove dead code Enable powerful whole program optimizations.
通過修改Build Settings中的Link-Time Optimization=Incremental,測試后ipa包減少4M,后續經過進一步驗證后可以打開。

4.2 Compress PNG Files & Remove Text Metadata From PNG Fils
上文提到的 負優化使png格式圖片增大,那么能否關閉負優化?在嘗試將 Compress PNG Files 設置為 NO 對包大小沒有任何影響,想放棄又不甘心,通過創建新的 Demo 工程測試,通過查看 Build 日志發現是通過 copypng 將原 png 圖片復制到構建產物根目錄的,幸運的是 copypng 不是一個可執行文件,而是一個由 perl 編寫的腳本。copypng部分源碼如下:

#!/usr/bin/perl


my $PNGCRUSH = `xcrun -f pngcrush`;
chomp $PNGCRUSH;


my $compress = 0;
my $stripPNGText = 0;
my @FILES = ();


# Gather command line options.
while( @ARGV ) {
    $_ = shift @ARGV;
    next if ( $_ eq "" );
    if ( $_ =~ /-strip-PNG-text/ ) {
        $compress = 1;
        $stripPNGText = 1;
        next;
    }
    if ( $_ eq "-compress" ) {
        $compress = 1;
        next;
    }
}
my @args;
if ( $compress ) {
    @args = ( $PNGCRUSH, "-q", "-iphone", "-f", "0" );
    if ( $stripPNGText ) {
        push ( @args, "-rem", "text" );
    }
    push ( @args, $SRCFILE, $DSTFILE );
} else {
    @args = ( "cp", "$SRCFILE", "$DSTFILE" );
}

其中 Compress PNG Files 和 Remove Text Metadata From PNG Fils 分別對應入參為 -compress 和 -strip-PNG-text??吹皆创a,即使我們不懂 perl 也應該明白了。為什么 Compress PNG Files 設置為 NO,不能取消負優化,要想取消根目錄下的負優化,需要將 Compress PNG Files 和 Remove Text Metadata From PNG File 都設置為 NO 才能取消。

測試將 Compress PNG Files 和 Remove Text Metadata From PNG File 設置為 NO 之后安裝包優化 1.6M。
同時也探究其對 Assets.car 的影響,通過對比是取消負優化對 Assets.car 的編譯工具 actool 的影響,取消后沒有 --compress-pngs 的入參。
--compress-pngs PNGs copied into iOS targets will be processed using pngcrush to optimize reading the images on iOS devices. This has no effect for images that wind up in the compiled CAR file, as it only affects PNG images copied in to the output bundle.
在驗證了 Compress PNG Files 和 Remove Text Metadata From PNG File 對 actool 是否有入參 --compress-pngs 的關系或后,我們也驗證了對大小的影響,結論是取消負優化,不會影響 Assets.car 的大小。
在二期優化過程中通過梳理根目錄下的圖片,現在只剩下 AppIcon 和 Launch Iamge,對于 Launch Iamge 我們通過 Launch Screen Storyboard 只保留一份啟動圖優化包大小,不考慮取消負優化。同時通過創建一個新工程只給 Asset Catalog 中添加圖片, 查看 Build Settings 是沒有這兩項配置項,可是 Build 日志 actool 也是有 --compress-pngs 的入參。相信 Apple 已經給我們做了最佳的選擇。

4.3 Asset Catalog Compiler 之 Optimization
對于 Optimization 中的 space 的優化,在一期就想通過灰度驗證是否有其他影響,如果沒有影響后啟用,因為那時啟用精簡包可以優化十幾M,在后面重點開始優化 Assets.car 后,考慮到啟用之后可能會消極的優化 Assets.car 就擱置,到目前商城最新版 8.4.0 以 iPhoneX 的 adhoc 包數據對比重點優化 Assets.car 后,啟用也只在 iOS12 以下 1.1M 的影響。

image.png

Apple 在 iOS 12 Optimizing App Assets。space 也不準備啟用,還是那句話相信 Apple 已經給我們做了最佳的選擇。

4.4 蘋果給的“驚喜”
在最近的一個版本集成后,通用包中的car文件減少7.9M,這當然有處理無用圖片,大圖的原因,但是以我們對數據的預估影響應該沒這么大。同時將數據導入到資源分析系統后發現很多模塊的資源文件大小大幅度減小,這很異常。打開car文件發現了原因:因為在最近的一個版本,商城App放棄了iOS8。支持iOS9以上后Xcode打包的時候會將部分小圖合并成類似雪碧圖的文件(進一步說明Asset Catalog中不要放大圖),如下圖:

image.png

這本來是件好事,可是對我們的文件大小統計產生了影響:圖片被合并后通過assetutil獲取到的圖片大小不對了。無奈,我們需要解決這個問題。經過嘗試,我們將本地cocoapods對資源文件的編譯版本改成iOS8,資源文件的統計恢復到了之前的版本,間接解決統計的問題。

最后

以上為商城App包大小優化的做的嘗試和主要措施,希望對大家有幫助。當然后面我們還有不少優化工作,比如商城App已經放棄了iOS8,那么我們有足夠的空間將部分動態庫轉成靜態庫;iconfont對模塊化工程的支持(各模塊獨立使用ttf);大圖經過壓縮,但是還是不小,后續考慮轉成webp格式等。隨著業務的快速發展,包大小的增加在所難免。但是在整個業務迭代過程中,我們更加需要遵守更加合理的設計規范,資源文件使用規范,業務準入準出原則來標準化產研過程。這樣我們的安裝包大小才能得到較好的控制,畢竟優化過程也是相當痛苦的。

參考鏈接:

[1]App Thinning in Xcode:

https://developer.apple.com/videos/play/wwdc2015/404/

[2]Optimizing App Assets:

https://developer.apple.com/videos/play/wwdc2018/227/

[3]iOS瘦包常見方式梳理:

https://mp.weixin.qq.com/s/J_XYpIfDeeWJBlk9sRQMAA

[4] iPhone安裝包的優化:

https://mp.weixin.qq.com/s/t4EVvXJWqX-MFuVy7T-hPQ

[5] CgBI:

http://iphonedevwiki.net/index.php/CgBI_file_format

海南飞鱼彩票怎么玩