不斷的開發迭代,產品經理不斷的添加需求,引入的資源文件幾乎是只加不減,猛然回首,iOS包已經100多m,看來iOS瘦身迫在眉睫啊!!!!
iOS瘦身的好處
我們先來討論 iOS瘦身的好處,正所謂知其然知其所以然。iOS瘦身有哪些好處?
1. However, consider download times when determining your app’s size. Minimize the file’s size as much as possible, keeping in mind that there is a 100 MB limit for over-the-air downloads. Abnormally large build files are usually the result of storing data, such as images, inside the compiled binary itself instead of as a resource inside your app bundle. If you are compiling an image or large dataset into your binary, it would be best to split this data out into a resource that is loaded dynamically by your app.
1.首先對于新用戶當第一安裝你的iOS程序時,需要下載完整的一個.ipa文件。(注意不同于升級),相同環境下iOS包越小用戶的下載時間越少,這一點對于用戶體驗來說至關重要,特別是有些情況apple store訪問不太順利,用戶下載你的iOS包肯定是迫切想了解使用你的產品,如果用戶等半天(等待對于用戶總是漫長的),用戶會煩躁帶著這種心情去了解你的產品,第一印象肯定不爽!
2.回想起來你的app第一個版本,有可能才10幾m,才一年多的時間,你的app已經變成100m了,然而產品業務需求是不斷迭代的,如果不加以控制,后面只會越來越大!
3.可能絕大部分的原因,你的產品并非是強勢app(比如支付寶,qq,百度)所以你也就不能隨便耍流氓,用戶手機的存儲空間是有限的(特別是那些16g的iPhone),除了那些強勢app,你必須要在剩余不多的空間,爭取用戶不會刪除你的app(特別是那些工具類型app)!
4.less is more ,對于代碼和那些資源文件也是如此,無用的代碼和資源文件越多,就導致項目冗余越大,維護也越麻煩,比如很可能同一張圖片有好幾個不同命名,如果要改圖片了就要全部更換 !
知道了瘦身的好處接下來,我們就談論一下iOS該怎么瘦身
iOS該怎么瘦身 ?
在做任何相關優化之前,我們需要做一些權衡。通過權衡,可以知道把優化的重點集中在什么地方。我們上文提到,當第一安裝iOS程序時,需要下載完整的一個.ipa文件。實際上.ipa文件就是一個.zip結構。簡單的將后綴為.ipa文件修改為.zip,然后利用Finder將其解壓出來。右鍵單擊解壓出來的.app bundle,選擇顯示包內容,以查看里面的資源文件。通過該方法我們可以看到哪些文件占的空間最大。記住:.app bundle是經過壓縮的,并且有些文件的壓縮效果要比別的文件好,所以壓縮后的效果才是才是最重要的。不過一般情況下在壓縮前最大的文件,在壓縮后依舊是最大的文件。
而這些資源文件(包括圖片、聲音以及其它配置文件)通常占了ipa 很大部分,所以我首先針對資源文件優化。
(1).資源文件瘦身
a.刪除無用的圖片資源
一個項目開發的越久,添加的功能模塊就越多,相應的,也會慢慢引入更多的圖片資源。但是在移除一些不再使用的模塊時,開發者往往不會將對應的圖片資源一起刪除,因為圖片資源和源碼是分離的,長久以來,項目中就會出現大量沒有使用的圖片資源。刪除無用的資源一般來說 能減掉個2~3MB。 這個時候就要使用工具自動迅速找出工程中所有沒被使用的資源文件嘍(想想就知道了 ,用手工得多慢多累啊) ,工欲善其事,必先利其器。
我首先推薦的是https://github.com/tinymind/LSUnusedResources? 整個過程非常的快, 比shell腳本不知道方便到哪里去了, 為了照顧那些懶癌癥患者 我把使用方法也貼出來
使用方法如下
1.點擊 Browse.. 選擇一個文件夾;
2.點擊 Search 開始搜索;
3.等待片刻即可看到結果;
4.選中某些行,然后點擊 Delete 可以直接刪除資源
第二種方法就是你也可以用萬能的腳本 https://github.com/examplecode/unused-image/blob/master/unused-image.sh? ?&&? http://stackoverflow.com/questions/6113243/how-to-find-unused-images-in-an-xcode-project/6113449#6113449
b.對資源壓縮
首先 盡量使用8-bit的PNG圖片,比32-bit的圖片能減少4倍的壓縮率。由于8-bit的圖片支持最多256種不同的顏色,所以8-bit的圖片一般只應該用于一小部分的顏色圖片。例如灰度圖片最好使用8-bit。然后并不能事事都如意, 設計師提供的圖片資源往往都是直接從sketch中剪切后的資源,大小非常大,這個時候就需要對png進行無損壓縮,假設我們的項目中有30M的圖片,然后將它們有損壓縮到80%的質量,那么就可以減掉6MB左右。可以使用以下兩種方法進行圖片的壓縮:用的是ImageOptim工具和compress命令(具體怎么使用我不想寫了)。但是并不建議對資源做有損壓縮,因為有損壓縮通常壓縮后效果不盡人意需要設計一個個檢查。
c. BitCode
首先我們來介紹一下? BitCode 是啥?
Bitcode is an intermediate representation of a compiled program. Apps
you upload to iTunes Connect that contain bitcode will be compiled and
linked on the App Store. Including bitcode will allow Apple to
re-optimize your app binary in the future without the need to submit a
new version of your app to the store.
說的是bitcode是被編譯程序的一種中間形式的代碼。包含bitcode配置的程序將會在App store上被編譯和鏈接。bitcode允許蘋果在后期重新優化我們程序的二進制文件,而不需要我們重新提交一個新的版本到App store上。呀,真有這么高級?
LLVM是目前蘋果采用的編譯器工具鏈,Bitcode是LLVM編譯器的中間代碼的一種編碼,LLVM的前端可以理解為C/C++/OC/Swift等編程語言,LLVM的后端可以理解為各個芯片平臺上的匯編指令或者可執行機器指令數據,那么,BitCode就是位于這兩者直接的中間碼. LLVM的編譯工作原理是前端負責把項目程序源代碼翻譯成Bitcode中間碼,然后再根據不同目標機器芯片平臺轉換為相應的匯編指令以及翻譯為機器碼.這樣設計就可以讓LLVM成為了一個編譯器架構,可以輕而易舉的在LLVM架構之上發明新的語言(前端),以及在LLVM架構下面支持新的CPU(后端)指令輸出,雖然Bitcode僅僅只是一個中間碼不能在任何平臺上運行,但是它可以轉化為任何被支持的CPU架構,包括現在還沒被發明的CPU架構,也就是說現在打開Bitcode功能提交一個App到應用商店,以后如果蘋果新出了一款手機并CPU也是全新設計的,在蘋果后臺服務器一樣可以從這個App的Bitcode開始編譯轉化為新CPU上的可執行程序,可供新手機用戶下載運行這個App.
扯了這么多 ,推出Bitcode的好處是啥? 跟iOS瘦身啥關系?之前打包,可以運行在各個不同型號的iOS設備上,是因為在打包的時候,蘋果幫我們把app在各型號設備上運行所需要的“東西”一并全部打到包里了。假設我們在打包的時候,只把要運行的設備所需的“東西”打到包里,而不需要其他型號運行所需要的“東西”,這樣不就達到減小ipa大小的目的了么?BitCode就是來完成這個任務的中間件。
d.正確導入圖片的姿勢
圖片的導入方式有如下幾種:
1.加入到Assets.xcassets中
只支持png格式的圖片
圖片只支持[UIImage imageNamed]的方式實例化,但是不能從Bundle中加載
在編譯時,Images.xcassets中的所有文件會被打包為Assets.car的文件
2.CreateGroup
黃色文件夾圖標;Xcode中分文件夾,Bundle中所有所在都在同一個文件夾下,因此,不能出現文件重名的情況
可以直接使用[NSBundle mainBundle]作為資源路徑,效率高!
可以使用[UIImage imageNamed:]加載圖像
3.CreateFolderRefences
藍色文件夾;Xcode中分文件夾,Bundle中同樣分文件夾,因此,可以出現文件重名的情況
需要在[NSBundle mainBundle]的基礎上拼接實際的路徑,效率較差
不能使用[UIImage imageNamed:]加載圖
4.PDFs矢量圖(Xcode6+)
5.Bundle(包)中的圖片素材
那這不同的導入方式,會對打出的包的大小有影響么?
經過測試得知:CreateGroup、CreateFolderRefences兩種方式打出來的包,圖片都會直接放在.app文件中,所以打包前后,圖片的大小不會改變
而加入到Assets.xcassets中的方法則不同,打包后,在.app中會生成Assets.car文件來存儲Assets.xcassets中的圖片,并且文件大小方面也大大降低
所以,使用Assets.xcassets來管理圖片也可以達到ipa瘦身的效果~~
話說PDFs矢量圖呢 ,利用矢量圖能不能幫助iOS App減少整體空間?
iOS對矢量圖的支持其實只是一種方便開發者的選擇, 本質上在XCode編譯的階段矢量圖會自動生成對應Target的@1x,@2x和@3x的png格式圖像。在iOS實際運行中使用的圖片實際上已經是png格式的圖片了~
用簡單粗暴的實驗來對比說明, 步驟如下:
使用pdf原始文件編譯生成通用IPA
從生成的IPA文件中提取Asset.car文件
利用iOS Image Extractor提取Asset.car文件
將提取出來的@1x、@2x、@3x放置回工程, 并刪除原始pdf中重新編譯
對比步驟1生成的car文件和步驟4生成的car文件大小
結果如下:
在iOS8.3以下, 相同壓縮比例的條件下, 矢量圖是無法幫助App減少空間。但是在iOS8.3以上, 利用xcassets可以避免多余的資源圖片下載, 只下載對應的倍率的圖片。因此, 嚴格意義下, 利用矢量圖并不能幫助App節省空間(其實跟用Assets.xcassets的方式效果差不多)。但是pdf矢量圖使用起來非常的方便, 建議使用。iOS本質上并不支持矢量圖, 但是在編譯階段會將矢量圖轉化成目標設備對應的尺寸圖, 同時會利用xcassets的特性在iOS8.3以上設備下支持部分資源下載, 帶到包瘦身的效果。每次都要讓UI給多個尺寸的圖, 肯定沒有給一張方便吧? 當然, 前提是UI的童鞋是基于矢量圖工具制作的圖片的前提下~
簡單的iOS瘦身技巧講完了 ,我們來點兒稍微高級的,畢竟步子要一步一步走,邁步太大容易扯著蛋
(2).代碼級別的優化
比如 在項目里新建一個類,給它添加幾個方法,但不要在任何地方import它,build完項目后觀察linkmap,你會發現這個類還是被編譯進可執行文件了。這是因為object-c的runtime 性質,按C++的經驗,沒有被使用到的類和方法編譯器都會優化掉,不會編進最終的可執行文件,但object-c不一樣,因為object-c的動態特性,它可以通過類和方法名反射獲得這個類和方法進行調用,所以就算在代碼里某個類沒被使用到,編譯器也沒法保證這個類不會在運行時通過反射去調用,所以只要是在項目里的文件,無論是否又被使用到都會被編譯進可執行文件。又比如我們的項目里會引入很多第三方靜態庫,如果能知道這些第三方庫在可執行文件里占用的大小,就可以評估是否值得去找替代方案去掉這個第三方庫。
這個時候就要介紹一下LinkMap了,LinkMap文件是Xcode產生可執行文件的同時生成的鏈接信息,用來描述可執行文件的構造成分,包括代碼段(__TEXT)和數據段(__DATA)的分布情況。比如說可執行文件的構成是怎樣,里面的內容都是些什么,?
1、使用LinkMap文件對可執行文件安裝包進行分析
在xcode的設置中 Project->Build Settings->Write Link Map File為YES,并設置Path to Link Map File,build完后就可以在設置的路徑看到LinkMap文件了
注意:此時最好使用真機進行編譯,不然可能無法找到我們想要的文件。
在以下目錄可以看到LinkMap文件,如下:
/Users/chenxintao/Library/Developer/Xcode/DerivedData/AppName-fnpgyspdoyxnotbpoliocmwypkff/Build/Intermediates/AppName.build/Debug-iphoneos/AppName.build/AppName-LinkMap-normal-arm64.txt
LinkMap文件主要分為以下三部分:
1.1 Object files
整個可執行文件里包含的所有.O文件,前面的數字是這個.o文件的序號。樣式如下:
# Object files:
[0] linker synthesized
[1]/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.0.sdk/usr/lib/crt1.o
[2]/Users/bang/Library/Developer/Xcode/DerivedData/yishu-eyzgphknrrzpevagadjtwpzzeqag/Build/Intermediates/yishu.build/Debug-iphonesimulator/yishu.build/Objects-normal/i386/TKPFileInfo.o
...
[280] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANJob.o)
[281] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANWorker.o)
[282] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(MobClick.o)
[283] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANLaunch.o)
前面中括號里的是這個文件的編號,后面會用到,像項目里引用到靜態鏈接庫libMobClickLibrary.a里的目標文件都會在這里列出來。
1.2 Sections
接著是一個段表,描述各個段在最后編譯成的可執行文件中的偏移位置及大小,包括了代碼段(__TEXT,保存程序代碼段編譯后的機器碼)和數據段(__DATA,保存變量值)。樣式如下:
# Sections:
# Address? Size? ? Segment? Section
0x00002740 0x00273890 __TEXT __text
0x00275FD0 0x00000ADA __TEXT __symbol_stub
0x00276AAC 0x00001222 __TEXT __stub_helper
0x00277CCE 0x00019D9E __TEXT __objc_methname
0x00291A70 0x00012847 __TEXT __cstring
0x002A42B7 0x00001FC1 __TEXT __objc_classname
0x002A6278 0x000046A7 __TEXT __objc_methtype
0x002AA920 0x000061CE __TEXT __ustring
0x002B0AF0 0x00000764 __TEXT __const
0x002B1254 0x000028B8 __TEXT __gcc_except_tab
0x002B3B0C 0x00004EBC __TEXT __unwind_info
0x002B89C8 0x0003662C __TEXT __eh_frame
0x002EF000 0x00000014 __DATA __program_vars
0x002EF014 0x00000284 __DATA __nl_symbol_ptr
0x002EF298 0x0000073C __DATA __la_symbol_ptr
0x002EF9E0 0x000030A4 __DATA __const
0x002F2A84 0x00000590 __DATA __objc_classlist
0x002F3014 0x0000000C __DATA __objc_nlclslist
0x002F3020 0x0000006C __DATA __objc_catlist
0x002F308C 0x000000D8 __DATA __objc_protolist
0x002F3164 0x00000008 __DATA __objc_imageinfo
0x002F3170 0x0002BC80 __DATA __objc_const
0x0031EDF0 0x00003A30 __DATA __objc_selrefs
0x00322820 0x00000014 __DATA __objc_protorefs
0x00322834 0x000006B8 __DATA __objc_classrefs
0x00322EEC 0x00000394 __DATA __objc_superrefs
0x00323280 0x000037C8 __DATA __objc_data
0x00326A48 0x000096D0 __DATA __cfstring
0x00330118 0x00001424 __DATA __objc_ivar
0x00331540 0x00006080 __DATA __data
0x003375C0 0x0000001C __DATA __common
0x003375E0 0x000018E8 __DATA __bss
1.3 Symbols
可執行文件中各種symbol的大小,包括各個symbol的起始地址,占用大小,來自哪一個.o文件(使用之前提到的.o文件的序號)。樣式如下:
# Address ? Size ?File ? ?Name
0x00002740 0x0000003E [ 1] start
0x00002780 0x00000400 [ 2] +[TKPFileInfo parseWithDictionary:]
0x00002B80 0x00000030 [ 2] -[TKPFileInfo fileID]
...
計算某個.o文件在最終安裝包中占用的大小,主要是解析Object files和Symbols兩個部分,從Object files讀取出每個.o文件名和對應的序號,然后對Symbols中序號相同的文件的Size字段相加,即可得到每個.o文件在最終包的大小。 同樣首列是數據在文件的偏移地址,第二列是占用大小,第三列是所屬文件序號,對應上述Object files列表,最后是名字。例如第二行代表了文件序號為2(反查上面就是TKPFileInfo.o)的parseWithDictionary方法占用了1000byte大小。
我們看到在這個里面除了可以看到DATA字段與TEXT字段的大小外,它還會列出所有類對象下的成員函數與類函數。其實這點很重要,因為這樣我們就可以知道工程中所有實現的函數了。通過相應的正則表達式,我們就可以提取出函數內容,其正則表達式為[+|-][.+\s(.+)],然后我們通過另外一個強大的反編譯工具otool,可以提取出工程中所使用的函數列表(Used Selectors All)。
先說那什么是otool呢?
Otool可以提取并顯示ios下目標文件的相關信息,包括頭部,加載命令,各個段,共享庫,動態庫等等。它擁有大量的命令選項,是一個功能強大的分析工具,當然還可以做反匯編的工具使用。
說到Otool就不得不提到mach-O ,那什么是mach-O??
Mach-O格式全稱為Mach Object文件格式的縮寫,是mac上可執行文件的格式,類似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)。
Mach-o包含三個基本區域:
1.頭部(header structure)。Mach-o的頭部幫助內核迅速確定當前文件所支持的CPU架構。
2.加載命令(load command)。
3.段(segment)。可以擁有多個段(segment),每個段可以擁有零個或多個區域(section)。每一個段(segment)都擁有一段虛擬地址映射到進程的地址空間。
4.鏈接信息。一個完整的用戶級Mach-o文件的末端是鏈接信息。其中包含了動態加載器用來鏈接可執行文件或者依賴庫所需使用的符號表,字符串表等等。 如下圖左邊就是蘋果給出的mach-O格式的示意圖 ,第二個圖是我們使用machOView來分析某個可執行文件中的armv7的格式。可以看出他們兩者的關系是對應的。
我們是如何找出工程中所使用的函數列表的呢,其實就是使用命令字otool -V -s __DATA __objc_selrefs 項目.app/項目 | open -f。這里的項目地址指的是項目.app的路徑地址,在Xcode7中的路徑為
/Users/用戶名/Library/Developer/Xcode/DerivedData/項目名/Build/Products/Debug-iphonesimulator/項目名.app/項目名
另外一個要注意的是-V要大寫,因為大寫和小寫的命令是不一樣的。當然大家也可以試試把DATA __objc_selrefs改成TEXT __objc_classname看看有什么不一樣。
下面就聊一聊如何對可執行文件進行瘦身。
a.查找無用selector
結合LinkMap文件的__TEXT.__text,通過正則表達式([+|-][.+\s(.+)]),我們可以提取當前可執行文件里所有objc類方法和實例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可執行文件里引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll里哪些方法是沒有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系統API的Protocol可能被列入無用方法名單里,如UITableViewDelegate的方法,我們只需要對這些Protocol里的方法加入白名單過濾即可。
另外第三方庫的無用selector也可以這樣掃出來的。
b. 查找無用oc類
查找無用oc類有兩種方式,一種是類似于查找無用資源,通過搜索"[ClassName alloc/new"、"ClassName *"、"[ClassName class]"等關鍵字在代碼里是否出現。另一種是通過otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段來獲取當前所有oc類和被引用的oc類,兩個集合相減就是無用oc類。
c.掃描重復代碼
可以利用第三方工具simian掃描(怎么使用自己去搜)。掃描重復代碼,但維護成本過高,因為需要重構代碼,沒有刪除代碼來得直接(看自己的夜霧需求)。
(3).編譯選項優化
這個最有用的一個選項是Strip Linked Product / Deployment Postprocessing / Symbols Hidden by Default 在release版本應該設為yes
原理是打開這兩個選項后構建ipa會去除掉symbol符號,就是那些類名啊函數名啊啥的。這樣子的影響就是運行時你沒法進行線程回溯,符號都沒了回溯了也是亂碼。但是不會影響正常的崩潰日志生成和解析。在本機專門測試過,如果使用符號表來解析崩潰日志,則完全不受影響。
第二個 就是 Build Settings->Optimization Level有幾個編譯優化選項,release版應該選擇Fastest, Smalllest,這個選項會開啟那些不增加代碼大小的全部優化,并讓可執行文件盡可能小(不過默認就是如此)。
終于你也看完了這么多內容,你以為總算看完了,呵呵,你還是太年輕了,同志們注意了,我要開始裝逼了,
接下來我要把更高級點的 iOS瘦身(從方案來自滴滴大神的分享,說實在的這也算是代碼級別的瘦身,這不過這種方法發揮了極致)
眾所周知,代碼之間存在調用關系。假設iOS APP的主入口為-[UIApplication main],則所有開發者的源代碼(包括第三方庫)可分為兩類:存在一條調用路徑,使得代碼可以被主入口最終調用(稱此類代碼為被最終調用);不存在一條調用路徑,使得代碼最終不能被主入口調用(稱此類代碼為未被最終調用)。
假設有一個源代碼級別的分析工具(或編譯器),可以輔助分析代碼間的調用關系,這樣就使得分析最終被調用代碼成為可能,剩下的就是未被最終調用的代碼。
這種工具目前有成熟可用的嗎?答案是肯定的,就是clang插件。除可用于分析未被最終調用代碼外,clang還可輔助發現重復代碼。作為LLVM提供的編譯器前端,clang可將用戶的源代碼(C/C++/Objective-C)編譯成語言/目標設備無關的IR(Intermediate Representation)實現。其可提供良好的插件支持,容許用戶在編譯時,運行額外的自定義動作。(后來想想其實clang插件可以做更多的事情)
我們的目標是使用clang插件減少包大小。其原理是,針對目標工程,基于clang的插件特性,開發者可以編寫插件以分析所有源代碼。編譯過程中,將插件作為clang的參數載入并生成各種中間文件。編譯完成后,還需編寫一個工具去分析所有包含源碼的方法(包括用戶編寫,以及引入的第三方庫源代碼),檢查這些方法中哪些最終可被程序主入口調用,剩余即是疑似無用代碼。簡單的一個復查,移除那些確定無用的代碼,重新編譯,便可以有效去除無用的代碼從而減少包大小。
首先“如何編寫一個clang插件并集成到Xcode” (這個要不你們自己搜吧 實在篇幅命令很長 ,但是按照命令一步一步來很簡單的,沒啥技術含量)
第二“如何實現代碼級別的包瘦身” (代碼指的是OC中的形如-/+[Class method:\*]這種形式的代碼,調用關系典型如下:)
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor redColor]];
}
@end
則稱:-[ViewController viewDidLoad]調用了:
-[UIViewController viewDidLoad]
-[ViewController view](語法糖)
+[UIColor redColor]
-[UIView setBackgroundColor:]
我先緩緩啊 等會兒補充