在我們提交安裝包到App Store的時候,如果安裝包過大,有可能會收到類似如下內容的一封郵件:
收到這封郵件的時候,意味着安裝包在App Store上下載的時候,有的設備下載的安裝包大小會超過100M。對於超過100M的安裝包,只能在WIFI環境下下載,不能直接通過4G網絡進行下載。
在這里,我們提交App Store的安裝包大小為67.6MB,在App Store上顯示的下載大小和實際下載下來的大小,我們通過下表做一個對比:
iPhone型號
|
系統
|
AppStore 顯示大小
|
下載到設備大小
|
---|---|---|---|
iPhone6 | 10.2.1 | 91.5MB | 88.9MB |
iPhone6 | 10.1.1 | 91.5MB | 88.9MB |
iPhone6 | 9.3.5 | 91.5MB | 84.8MB |
iPhone 5 | 9.2 | 91.5MB | 84.8MB |
iPhone6 plus | 10.0.2 | 95.7MB | 93.2MB |
iPhone7 plus | 10.3.0 | 95.7MB | 93.2MB |
iPhone5C | 9.2 | 83.9MB | 76MB |
iPhone5S | 7.1.1 | 147MB | 144MB |
iPhone5C | 7.1.2 | 147MB | 未知 |
iPhone5C 越獄 | 8.1.1 | 83.9MB | 144MB |
從上表可以看到:
- 在 iOS 9 系統以上的手機上,App Store 上的大小都是做了 App Thinning 操作的。
- 在 iOS 9 以上系統的基礎上,plus 手機在 AppStore size 上比其他手機大了 4.2MB,猜測是因為 @3x 圖的原因。
- iOS 9 和 iOS 10 雖然在 AppStore 顯示的包大小一致,但是最終下載到手機上,大小有區別。
- iOS 9 以下的手機,是直接下載整個安裝包的。
【App Thinning】:對於iOS應用來說,應用瘦身僅支持最新版本的iTunes,以及運行iOS 9.0或者更高系統的設備,否則的話,App Store將會為用戶分發統一的安裝包。iOS 9 在發布時隱含一個 Bug , App Thinning ( App 瘦身)無法正確運作。隨着 iOS 9.0.2 的發布,此 Bug 已被修復, App 瘦身終於可以運作如常。從 App Store 下載 App 時請謹記這點。App Thinning 會自動檢測用戶的設備類型(即型號名稱)並且只下載當前設備所適用的內容。換句話說,如果使用的是 iPad Mini 1(1x分辨率且非 retina 顯示屏)那么只會下載 1x分辨率所使用的文件。更強大和更高分辨率的 ipad(如iPad Mini 3或 4)所使用的資源將不會被下載。因為用戶僅需下載自己當前使用的特定設備所需的內容,這不僅加快了下載速度,還節約了設備的存儲空間。
在郵件內容中,蘋果建議刪除一些無用的執行代碼或資源文件。下面我們分別從這兩方面來分析安裝包瘦身的一些方法和工具。
1.資源文件
資源文件包括圖片、聲音、配置文件、文本文件(例如rtf文件)、xib(在安裝包中后綴名為nib)、storyboard等。對於聲音、配置文件、文本文件這三類資源文件,一般在安裝包中數量不多,可自行在工程中根據實際情況,進行刪除或保留。聲音文件過大的話,可以考慮用如下命令做壓縮:
//tritone.caf為聲音文件 afconvert -f AIFC -d ima4 tritone.caf
xib和storyboard文件實際上是一個xml文件,如果某個頁面沒有使用,可直接刪除。這里主要說一下對圖片資源的處理方式。
對圖片資源類文件,一般采取的方法是這幾種:
- 刪除無用的資源文件;
- 對資源文件進行壓縮;
- 變更圖片文件的導入方式;
- 處理1x圖片。
1.1刪除無用的資源文件
首先,使用python腳本搜索工程中沒有使用的圖片資源,腳本代碼示例如下:
#!/bin/sh PROJ=`find . -name '*.xib' -o -name '*.[mh]'` for png in `find . -name '*.png'` do name=`basename $png` if ! grep -qhs "$name" "$PROJ"; then echo "$png is not referenced" fi done
但上面的腳本具有如下缺點:不夠智能,不夠通用,速度太慢,結果不正確。
在這里推薦使用工具LSUnusedResources。它在腳本的基礎上,做了兩個改進:
- 提高匹配速度。LSUnusedResources不是對每個資源文件名都做一次全文搜索匹配,因為加入項目的資源太多,這里會導致性能快速下降。它只是針對源碼、Xib、Storyboard 和 plist 等文件,先全文搜索其中可能是引用了資源的字符串,然后用資源名和字符串做匹配。
- 優化匹配結果。比如說腳本會把大量實際上有使用的資源,當做未使用的資源輸出(例如拼接的圖片名稱),而LSUnusedResources不會。
接下來,打開工具LSUnusedResources,點擊“Browse...”按鈕,選擇工程所在目錄,點擊"Search"按鈕,即可開始搜索,如下圖所示:
搜索結果出來之后,選中某行,點擊“Delete”按鈕即可直接刪除資源。
1.2對資源文件進行壓縮
壓縮工具有很多,這里介紹兩個好用的:
- 無損壓縮工具ImageOptiom(推薦)。這是一款非常好的圖片壓縮工具,可以進行無損壓縮,能夠對 png 和 jpeg 圖片文件進行優化,它能找到最佳的壓縮參數(在設置中可以設置壓縮比例,80% 及以上是無損壓縮,推薦使用),並通過消除不必要的信息(如文件的 EXIF 標簽和顏色配置文件等),優化后達到減小文件大小的效果。
- 有損壓縮工具TinyPNG。它使用聰明的有損壓縮技術,能有效減少PNG文件的大小。通過選擇性地降低圖像中顏色的數量,需要更少的字節來存儲數據。
【建議】:對於較大尺寸的圖片,可以和設計溝通,在不失真和影響效果的前提下,使用TinyPNG進行壓縮;較小尺寸的圖片,建議使用ImageOptiom。
1.3變更圖片文件的導入方式
我們都知道,圖片資源的導入方式有如下幾種:
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文件夾 |
打包后的Assets.car文件夾 |
第一次 | 32.7MB |
16.3MB |
第二次 | 33.5MB | 26.1MB |
從表格數據可以看到,使用Assets.xcassets來管理圖片也可以達到ipa瘦身的效果。
值得留意的是,在將圖片資源移到Assets.xcassets管理的時候,一般情況下會自動生成與圖片名稱相同的,比如loading@2x.png和loading@3x.png會自動放置到一個同名的loading文件夾中。然而有一些不規則命名的圖片,會出現一些奇怪的問題:
- 圖片名稱為ios-f2-8-004的圖片,放到Images.xcassets中,會自動生成調用的圖片名是ios-f2-8-4,最后一位的004,被替換成4,然而在類文件中引用的是[UIImage imageNamed:@"ios-f2-8-004.png"],這樣會找不到圖片;
- 圖片名稱為ios-f6-的圖片,放到Images.xcassets中,會自動生成調用的圖片名是ios-f6,這樣也會找不到圖片。
因此在移動的時候,一定要細致對比。
1.4處理1x圖片
我們知道,iPhone設備目前主要有四種尺寸:3.5英寸、4英寸、4.7英寸、5.5英寸,對於這幾個尺寸的設備,我們來看一下具體的設備型號和屏幕相關信息:
機型 | 屏幕寬高(point) | 渲染像素(pixel) | 物理像素(pixel) | 屏幕對角線長度(英寸) | 屏幕模式 |
iPhone 2G, 3G, 3GS | 320 * 480 | 320 * 480 | 320 * 480 | 3.5(163PPI) | 1x |
iPhone 4, 4s | 320 * 480 | 640 * 960 | 640 * 960 | 3.5 (326PPI) | 2x |
iPhone 5, 5s | 320 * 568 | 640 * 1136 | 640 * 1136 | 4 (326PPI) | 2x |
iPhone 6, 6s, 7 | 375 * 667 | 750 * 1334 | 750 * 1334 | 4.7 (326PPI) | 2x |
iPhone 6 Plus, 6s Plus, 7 Plus | 414 * 736 | 1242 * 2208 | 1080 * 1920 | 5.5 (401PPI) | 3x |
iPhone X | 375 * 812 | 1125 * 2436 | 1125 * 2436 | 3x |
對於上表中的幾個概念,這里做一下說明:
- Points: 是iOS開發中引入的抽象單位,稱作點。開發過程中所有基於坐標系的繪制都是以 point 作為單位,在iPhone 2G,3G,3GS的年代,point 和屏幕上的像素是完全一一對應的,即 320 * 480 (points), 也是 320 * 480 (pixels);
- Rendered Pixels: 渲染像素, 以 point 為單位的繪制最終都會渲染成 pixels,這個過程被稱為光柵化。基於 point 的坐標系乘以比例因子可以得到基於像素的坐標系,高比例因子會使更多的細節展示,目前的比例因子會是 1x,2x,3x
- Physical Pixels: 物理像素,就是設備屏幕實際的像素。
- Physical Device: 設備屏幕的物理長度,使用英寸作為單位。比如iPhone 4屏幕是3.5英寸,iPhone 5 是4英寸,iphone 6是4.7英寸,這里的數字是指手機屏幕對角線的物理長度。實際上會是Physical Pixels的像素值(而不是Rendered Pixels的像素值)會渲染到該屏幕上, 屏幕會有 PPI(pixels-per-inch) 的特性,PPI 的值告訴你每英寸會有多少像素渲染。
- 屏幕模式: 描述的是屏幕中一個點有多少個 Rendered Pixels 渲染,對於2倍屏(又稱 Retina 顯示屏),會有 2 * 2 = 4 個像素的面積渲染,對於3倍屏(又稱 Retina HD 顯示屏),會有 3 * 3 = 9 個像素的面積渲染。
在實際的開發中,所有控件的坐標以及控件大小都是以點為單位的,假如屏幕上需要展示一張 20 * 20 (單位:point)大小的圖片,那么設計師應該怎么給圖呢?這里就會用到屏幕模式的概念,如果屏幕是 2x,那么就需要提供 40 * 40 (單位: pixel)大小的圖片,如果屏幕是 3x,那么就提供 60 * 60 大小的圖片,且圖片的命名需要遵守以下規范:
- Standard:
<ImageName><device_modifier>.<filename_extension>
- High resolution:
<ImageName>@2x<device_modifier>.<filename_extension>
- High HD resolution:
<ImageName>@3x<device_modifier>.<filename_extension>
其中:
- ImageName: 圖片名字,根據場景命名
- device_modifier: 可選,可以是
~ipad
或者~iphone
, 當需要為 iPad 和 iPhone 分別指定一套圖時需要加上此字段 - filename_extension: 圖片后綴名,iOS中使用 png 圖片
2x屏幕的設備會自動加載 xxx@2x.png 命名的圖片資源,3x屏幕的設備會自動加載 xxx@3x.png 的圖片。從友盟統計數據可以看到,現在基本沒有 1x屏幕的設備了,所以可以不用提供這個分辨率的圖片。
至於開發中,技術人員和設計人員關於設計和切圖的工作流程和規范,可以參看知乎上的這篇文章介紹。
2.Mach-O 可執行文件
我們用 Xcode 構建一個程序的過程中,會把源文件 (.m
和 .h
) 文件轉換為一個可執行文件。這個可執行文件中包含的字節碼會被 CPU (iOS 設備中的 ARM 處理器或 Mac 上的 Intel 處理器) 執行。對於這個可執行文件,我們可以用工具MachOView來查看。
2.1MachOView
Mach-O為Mach Object文件格式的縮寫,是mac上可執行文件的格式,類似於windows上的PE格式 (Portable Executable )或 linux上的elf格式 (Executable and Linking Format)。Mach-O文件分為這幾類:
- Executable:應用的主要二進制;
- Dylib Library:動態鏈接庫;
- Static Library:靜態鏈接庫;
- Bundle:不能被鏈接的Dylib,只能在運行時使用dlopen( )加載,可當做macOS的插件;
- Relocatable Object File :可重定向文件類型。
對於這幾種類型的Mach-O文件,我們可以使用MachOView進行查看。MachOView是一個開源的工具,源碼在GitHub上:https://github.com/gdbinit/MachOView,感興趣的可以研究一下。
下面我們用MachOView來打開一個靜態鏈接庫文件看看,了解Mach-O文件的結構:
首先,我們來看一下“Fat Header”里面的內容:它是對各種架構文件的組裝,可以看到每種類型的CPU架構信息,從上圖可以看到支持的架構,圖中顯示的支持ARM_V7 、i386 、 X86_64、ARM_64。
接下來我們點開一個Static Library看看:
從上圖可以看到,Static Library有很多.o文件,每個.o文件都對應一個類編譯后的文件,展開查看“Mach Header”信息,可以看到每個類的CPU架構信息、Load Commands數量 、Load Commands Size 、File Type等信息。
當然,我們也可以在Xcode中,開啟編譯選項Write Link Map File,編譯之后來查看可執行文件的全貌。
2.2LinkMap
LinkMap文件是Xcode產生可執行文件的同時生成的鏈接信息,用來描述可執行文件的構造成分,包括代碼段(__TEXT)和數據段(__DATA)的分布情況。
在Xcode中,選擇XCode -> Target -> Build Settings -> 搜map -> 把Write Link Map File選項設為YES,並指定好linkMap的存儲位置,如下圖所示:
編譯后,到編譯目錄里找到該txt文件,文件名和路徑就是上述的Path to Link Map File。這個LinkMap里展示了整個可執行文件的全貌,列出了編譯后的每一個.o目標文件的信息(包括靜態鏈接庫.a里的),以及每一個目標文件的代碼段,數據段存儲詳情。下面來簡單分析一下這個文件的結構。
2.2.1目標文件列表
打開LinkMap文件,首先看到的就是編譯后的每一個.o目標文件的信息,如下圖所示:
前面中括號里的是這個文件的編號,后面會用到。包括工程中用到的庫和Framework,都會在這里列出來。
2.2.2段表
接着是一個段表,描述各個段在最后編譯成的可執行文件中的偏移位置及大小,包括了代碼段(__TEXT,保存程序代碼段編譯后的機器碼)和數據段(__DATA,保存變量值)。
首列是數據在文件的偏移位置,第二列是這一段占用大小,第三列是段類型,代碼段和數據段,第四列是段名稱。
每一行的數據都緊跟在上一行后面,如第二行__stubs的地址0x1000099AC就是第一行__text的地址0x1000051B4加上大小0x000047F8,整個可執行文件大致數據分布就是這樣。
這里可以清楚看到各種類型的數據在最終可執行文件里占的比例,例如__text表示編譯后的程序執行語句,__data表示已初始化的全局變量和局部靜態變量,__bss表示未初始化的全局變量和局部靜態變量,__cstring表示代碼里的字符串常量,等等。
2.2.3符號表(Symbols)
Symbols 是對 Sections 進行了再划分,這里會描述所有的 methods、ivar 和字符串,以及它們對應的地址、大小、文件編號信息。
同樣首列是數據在文件的偏移地址,第二列是占用大小,第三列是所屬文件序號,對應2.2.1中的文件編號,最后是名字。
例如第70行代表了文件序號為3(反查上面就是GofObject.o)的gofName方法占用了48byte大小。
計算某個.o文件在最終安裝包中占用的大小,主要是解析目標文件和符號表兩個部分,從目標文件讀取出每個.o文件名和對應的序號,然后對Symbols中序號相同的文件的Size字段相加,即可得到每個.o文件在最終包的大小。
2.3編譯過程
在上面兩節中,我們初步接觸了可執行文件的內容,本節我們來分析一下編譯過程,以便更深入的熟悉可執行文件。
2.3.1編譯器
Xcode 的默認編譯器是Clang,Clang 的功能是首先對 Objective-C 代碼做分析檢查,然后將其轉換為低級的類匯編代碼:LLVM Intermediate Representation(LLVM 中間表達碼)。接着 LLVM 會執行相關指令將 LLVM IR 編譯成目標平台上的本地字節碼,這個過程的完成方式可以是即時編譯 (Just-in-time),或在編譯的時候完成。
LLVM是一個模塊化和可重用的編譯器和工具鏈技術的集合,Clang 是 LLVM 的子項目,是 C、C++ 和 Objective-C 編譯器,目的是提供驚人的快速編譯,比 GCC 快3倍,其中的 clang static analyzer 主要是進行語法分析、語義分析和生成中間代碼,當然這個過程會對代碼進行檢查,出錯的和需要警告的會標注出來。LLVM 核心庫提供一個優化器,對流行的 CPU 做代碼生成支持。lld 是 Clang / LLVM 的內置鏈接器,clang 必須調用鏈接器來產生可執行文件。
LLVM 的優點主要得益於它的三層式架構。 第一層支持多種語言作為輸入(例如 C, ObjectiveC, C++ 和 Haskell),第二層是一個共享式的優化器(對 LLVM IR 做優化處理),第三層是許多不同的目標平台(例如 Intel, ARM 和 PowerPC)。在這三層式的架構中,如果想要添加一門語言到 LLVM 中,那么可以把重要精力集中到第一層上,如果想要增加另外一個目標平台,那么沒必要過多的考慮輸入語言。
目前LLVM包含的主要子項目包括:
- LLVM Core:包含一個現在的源代碼/目標設備無關的優化器,一集一個針對很多主流(甚至於一些非主流)的CPU的匯編代碼生成支持。
- Clang:一個C/C++/Objective-C編譯器,致力於提供令人驚訝的快速編譯,極其有用的錯誤和警告信息,提供一個可用於構建很棒的源代碼級別的工具.
- dragonegg:gcc插件,可將GCC的優化和代碼生成器替換為LLVM的相應工具。
- LLDB:基於LLVM提供的庫和Clang構建的優秀的本地調試器。
- libc++、libc++ ABI: 符合標准的,高性能的C++標准庫實現,以及對C++11的完整支持。
- compiler-rt:針對
__fixunsdfdi
和其他目標機器上沒有一個核心IR(intermediate representation)對應的短原生指令序列時,提供高度調優過的底層代碼生成支持。 - OpenMP: Clang中對多平台並行編程的runtime支持。
- vmkit:基於LLVM的Java和.NET虛擬機實
- polly: 支持高級別的循環和數據本地化優化支持的LLVM框架。
- libclc: OpenCL標准庫的實現
- klee: 基於LLVM編譯基礎設施的符號化虛擬機
- SAFECode:內存安全的C/C++編譯器
- lld: clang/llvm內置的鏈接器
【說明】:從功能的角度來說,微觀的LLVM可以認為是一個編譯器的后端,而Clang是一個編譯器的前端。關於Clang和LLVM的關系,可以看一下這篇文章。
從一個簡單的例子開始:
#include <stdio.h> #define YEAR 2017 int main(int argc, const char * argv[]) { printf("Hello, %d!\n", YEAR); return 0; }
在編譯一個源文件時,編譯器的處理過程分為幾個階段。要想查看編譯 main.m 源文件需要幾個不同的階段,我們可以讓通過 clang 命令觀察:
clang -ccc-print-phases main.m
結果如下:
0: input, "main.m", objective-c 1: preprocessor, {0}, objective-c-cpp-output 2: compiler, {1}, ir 3: backend, {2}, assembler 4: assembler, {3}, object 5: linker, {4}, image 6: bind-arch, "x86_64", {5}, image
從結果可以看到,從源文件到可執行文件,經過了這么幾個過程:預處理、編譯、匯編、鏈接,最終生成可執行文件。
當程序執行時,操作系統將可執行文件拷貝到內存中。那么我們的程序最終是怎么執行的呢?程序的執行是在進程中進行的,程序轉化為進程大致分為這么幾個步驟:
- 1.內核將程序讀入內存,為程序鏡像分配內存空間。程序鏡像的內存布局分為如下部分(可通過size指令查看代碼段、數據段、BSS段的大小以及這3個段大小之和的十進制和十六進制表示):
- 代碼段:即機器碼,只讀,可共享(多個進程共享代碼段);
- 數據段:儲存已被初始化了的靜態數據;
- BSS段(未初始化的數據段):儲存未始化的靜態數據;
- 堆:儲存動態分配的內存;
- 棧:儲存函數調用的上下文,動態數據。
- 2.內核為該進程分配進程標志符(PID)。
- 3.內核為該進程保存PID及相應的進程狀態信息。
經過上面幾個步驟,操作系統向內核數據結構中添加了適當的信息,並為運行程序代碼分配了必要的資源之后,程序就變成了進程。下面我們來分析編譯的幾個階段。
2.3.2預處理
執行指令,我們看一下預處理階段都做了哪些事情:
clang -fmodules -E main.m | open -f
【說明】:目前預處理中引入了模塊 - modules功能,這使預處理變得更加的高級。
通過上面的指令,我們看一下輸出的結果:
# 1 "main.m" # 1 "<built-in>" 1 # 1 "<built-in>" 3 # 342 "<built-in>" 3 # 1 "<command line>" 1 # 1 "<built-in>" 2 # 1 "main.m" 2 @import Darwin.C.stdio; /* clang -E: implicit import for "/usr/include/stdio.h" */ int main() { printf("Hello, %d!\n", 2017); return 0; }
從結果可以看到,預處理階段,會進行宏的替換,頭文件的導入,以及類似#if的處理。
在Xcode中,可以通過這樣的方式查看任意文件的預處理結果:Product -> Perform Action -> Preprocess。如下圖所示:
在預處理完成之后,會進行詞法分析,這里會把代碼切成一個個 Token,比如大小括號,等於號還有字符串等。我們可以通過指令看一下詞法分析:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
輸出結果如下:
annot_module_include '#include <stdio.h> #define YEAR 2017 int main() { printf("Hello, %d!\n", YEAR); // NSLog(@"hello, %@", @"world"); return 0; } ' Loc=<main.m:9:1> int 'int' [StartOfLine] Loc=<main.m:12:1> identifier 'main' [LeadingSpace] Loc=<main.m:12:5> l_paren '(' Loc=<main.m:12:9> r_paren ')' Loc=<main.m:12:10> l_brace '{' [LeadingSpace] Loc=<main.m:12:12> identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:13:5> l_paren '(' Loc=<main.m:13:11> string_literal '"Hello, %d!\n"' Loc=<main.m:13:12> comma ',' Loc=<main.m:13:26> numeric_constant '2017' [LeadingSpace] Loc=<main.m:13:28 <Spelling=main.m:11:14>> r_paren ')' Loc=<main.m:13:32> semi ';' Loc=<main.m:13:33> return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:15:5> numeric_constant '0' [LeadingSpace] Loc=<main.m:15:12> semi ';' Loc=<main.m:15:13> r_brace '}' [StartOfLine] Loc=<main.m:16:1> eof '' Loc=<main.m:16:2>
然后進行語法分析,驗證語法是否正確,然后將所有節點組成抽象語法樹 AST:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
結果如下:
TranslationUnitDecl 0x7fbdb7020cd0 <<invalid sloc>> <invalid sloc> |-TypedefDecl 0x7fbdb7021218 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128' | `-BuiltinType 0x7fbdb7020f40 '__int128' |-TypedefDecl 0x7fbdb7021278 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128' | `-BuiltinType 0x7fbdb7020f60 'unsigned __int128' |-TypedefDecl 0x7fbdb7021308 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *' | `-PointerType 0x7fbdb70212d0 'SEL *' | `-BuiltinType 0x7fbdb7021180 'SEL' |-TypedefDecl 0x7fbdb70213e8 <<invalid sloc>> <invalid sloc> implicit id 'id' | `-ObjCObjectPointerType 0x7fbdb7021390 'id' | `-ObjCObjectType 0x7fbdb7021360 'id' |-TypedefDecl 0x7fbdb70214c8 <<invalid sloc>> <invalid sloc> implicit Class 'Class' | `-ObjCObjectPointerType 0x7fbdb7021470 'Class' | `-ObjCObjectType 0x7fbdb7021440 'Class' |-ObjCInterfaceDecl 0x7fbdb7021518 <<invalid sloc>> <invalid sloc> implicit Protocol |-TypedefDecl 0x7fbdb7021878 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag' | `-RecordType 0x7fbdb7021680 'struct __NSConstantString_tag' | `-Record 0x7fbdb70215e0 '__NSConstantString_tag' |-TypedefDecl 0x7fbdb7021908 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *' | `-PointerType 0x7fbdb70218d0 'char *' | `-BuiltinType 0x7fbdb7020d60 'char' |-TypedefDecl 0x7fbdb78049e8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]' | `-ConstantArrayType 0x7fbdb7804990 'struct __va_list_tag [1]' 1 | `-RecordType 0x7fbdb7804800 'struct __va_list_tag' | `-Record 0x7fbdb7021958 '__va_list_tag' |-ImportDecl 0x7fbdb7805230 <main.m:9:1> col:1 implicit Darwin.C.stdio |-FunctionDecl 0x7fbdb78052b8 <line:12:1, line:16:1> line:12:5 main 'int ()' | `-CompoundStmt 0x7fbdb8861140 <col:12, line:16:1> | |-CallExpr 0x7fbdb88610a0 <line:13:5, col:32> 'int' | | |-ImplicitCastExpr 0x7fbdb8861088 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay> | | | `-DeclRefExpr 0x7fbdb7805798 <col:5> 'int (const char *, ...)' Function 0x7fbdb78053c0 'printf' 'int (const char *, ...)' | | |-ImplicitCastExpr 0x7fbdb88610f0 <col:12> 'const char *' <BitCast> | | | `-ImplicitCastExpr 0x7fbdb88610d8 <col:12> 'char *' <ArrayToPointerDecay> | | | `-StringLiteral 0x7fbdb8861000 <col:12> 'char [12]' lvalue "Hello, %d!\n" | | `-IntegerLiteral 0x7fbdb8861038 <line:11:14> 'int' 2017 | `-ReturnStmt 0x7fbdb8861128 <line:15:5, col:12> | `-IntegerLiteral 0x7fbdb8861108 <col:12> 'int' 0 `-<undeserialized declarations>
2.3.3編譯
我們可以用下面的命令讓 clang
輸出匯編代碼:
clang -S -o - main.m | open -f
結果如下:
//.section 指令指定接下來會執行哪一個段 .section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 12 //.globl 指令說明 _main 是一個外部符號。這就是我們的 main() 函數。這個函數對於二進制文件外部來說是可見的,因為系統要調用它來運行可執行文件。 .globl _main //.align 指令指出了后面代碼的對齊方式。在我們的代碼中,后面的代碼會按照 16(2^4) 字節對齊,如果需要的話,用 0x90 補齊。 .p2align 4, 0x90 //main 函數的頭部: //_main 函數真正開始的地址。這個符號會被 export。二進制文件會有這個位置的一個引用。 _main: ## @main //.cfi_startproc 指令通常用於函數的開始處。CFI 是調用幀信息 (Call Frame Information) 的縮寫。這個調用 幀 以松散的方式對應着一個函數。當開發者使用 debugger 和 step in 或 step out 時,實際上是 stepping in/out 一個調用幀。在 C 代碼中,函數有自己的調用幀,當然,別的一些東西也會有類似的調用幀。.cfi_startproc 指令給了函數一個 .eh_frame 入口,這個入口包含了一些調用棧的信息(拋出異常時也是用其來展開調用幀堆棧的)。這個指令也會發送一些和具體平台相關的指令給 CFI。它與后面的 .cfi_endproc 相匹配,以此標記出 main() 函數結束的地方。 .cfi_startproc ## BB#0: //ABI ( 應用二進制接口 application binary interface) 指定了函數調用是如何在匯編代碼層面上工作的。在函數調用期間,ABI 會讓 rbp 寄存器 (基礎指針寄存器 base pointer register) 被保護起來。當函數調用返回時,確保 rbp 寄存器的值跟之前一樣,這是屬於 main 函數的職責。pushq %rbp 將 rbp 的值 push 到棧中,以便我們以后將其 pop 出來。 pushq %rbp Ltmp0: //和.cfi_offset %rbp, -16一起,會輸出一些關於生成調用堆棧展開和調試的信息。我們改變了堆棧和基礎指針,而這兩個指令可以告訴編譯器它們都在哪兒,或者更確切的,它們可以確保之后調試器要使用這些信息時,能找到對應的東西。 .cfi_def_cfa_offset 16 Ltmp1: .cfi_offset %rbp, -16 //把局部變量放置到棧上 movq %rsp, %rbp Ltmp2: .cfi_def_cfa_register %rbp //將棧指針移動 16 個字節,也就是函數會調用的位置 subq $16, %rsp //leaq 會將 L_.str 的指針加載到 rdi 寄存器中。 leaq L_.str(%rip), %rdi movl $2017, %esi ## imm = 0x7E1 movl $0, -4(%rbp) //把使用來存儲參數的寄存器數量存儲在寄存器 al 中 movb $0, %al //調用 printf() 函數 callq _printf //下面的代碼將 ecx 寄存器設置為 0,並把 eax 寄存器的值保存至棧中,然后將 ect 中的 0 拷貝至 eax 中。ABI 規定 eax 將用來保存一個函數的返回值 xorl %esi, %esi movl %eax, -8(%rbp) ## 4-byte Spill movl %esi, %eax //把堆棧指針 rsp 上移 32 字節 addq $16, %rsp //把之前存儲至 rbp 中的值從棧中彈出來 popq %rbp //返回調用者, ret 會讀取出棧的返回地址 retq .cfi_endproc //.section 指令指出下面將要進入的段 .section __TEXT,__cstring,cstring_literals //L_.str 標記運行在實際的代碼中獲取到字符串的一個指針 L_.str: ## @.str //.asciz 指令告訴編譯器輸出一個以 ‘\0’ (null) 結尾的字符串。 .asciz "Hello, %d!\n" .section __DATA,__objc_imageinfo,regular,no_dead_strip L_OBJC_IMAGE_INFO: .long 0 .long 64 //.subsections_via_symbols 指令是靜態鏈接編輯器使用的 .subsections_via_symbols
關於匯編指令的資料,可以在 蘋果的 OS X Assembler Reference 中進行查看和學習。
在Xcode中,可以通過這樣的方式查看任意文件的匯編輸出結果:Product -> Perform Action -> Assemble。如下圖所示:
2.3.4匯編器
匯編器將可讀的匯編代碼轉換為機器代碼。它會創建一個目標對象文件,一般簡稱為 對象文件。這些文件以 .o
結尾。如果用 Xcode 構建應用程序,可以在工程的 derived data 目錄中,Objects-normal
文件夾下找到這些文件。
我們也可以通過如下指令來生成:
clang -fmodules -c main.m -o main.o
2.3.5鏈接器
鏈接器解決了目標文件和庫之間的鏈接。 如上面的匯編代碼:
callq _printf
printf()
是 libc 庫中的一個函數。無論怎樣,最后的可執行文件需要能需要知道 printf()
在內存中的具體位置:例如,_printf
的地址符號是什么。鏈接器會讀取所有的目標文件 (此處只有一個) 和庫 (此處是 libc),並解決所有未知符號 (此處是 _printf
) 的問題。然后將它們編碼進最后的可執行文件中 (可以在 libc 中找到符號 _printf
),接着鏈接器會輸出可以運行的執行文件。
這里我們講一個復雜點的可執行文件。
//main.m #import "GofClass.h" int main() { GofClass *gofClass = [[GofClass alloc] init]; [gofClass doSomethingWithName:@"寫代碼"]; return 0; } //GofClass.h #import <Foundation/Foundation.h> @interface GofClass : NSObject /** 做某項事情 @param workName 事情名稱 */ - (void)doSomethingWithName:(NSString *)workName; @end //GofClass.m #import "GofClass.h" @implementation GofClass - (void)doSomethingWithName:(NSString *)workName { NSLog(@"開始%@", workName); } @end
通過如下指令來分別生成各個類的目標文件,並最終生成可執行文件:
clang -fmodules -c main.m -o main.o clang -fmodules -c GofClass.m -o GofClass.o //生成可執行文件 clang main.o GofClass.o -o GofMachOFile
可執行文件和目標文件都有一個符號表,這個符號表規定了它們的符號。如果我們用 nm(1)
工具觀察一下 main.o
目標文件,可以看到如下內容:
//指令 xcrun nm -nm main.o //結果 (undefined) external _OBJC_CLASS_$_GofClass (undefined) external ___CFConstantStringClassReference (undefined) external _objc_msgSend 0000000000000000 (__TEXT,__text) external _main 00000000000000b0 (__TEXT,__ustring) non-external l_.str
external _OBJC_CLASS_$_GofClass是GofClass Objective-C 類的符號。該符號是 undefined, external 。External 的意思是指對於這個目標文件該類並不是私有的,相反,non-external
的符號則表示對於目標文件是私有的。我們的 helloworld.o
目標文件引用了類 Foo
,不過這並沒有實現它。因此符號表中將其標示為 undefined。
同樣的我們也看一下GofClass.o:
//指令 xcrun nm -nm GofClass.o //結果 (undefined) external _NSLog (undefined) external _OBJC_CLASS_$_NSObject (undefined) external _OBJC_METACLASS_$_NSObject (undefined) external ___CFConstantStringClassReference (undefined) external __objc_empty_cache 0000000000000000 (__TEXT,__text) non-external -[GofClass doSomethingWithName:] 0000000000000030 (__TEXT,__ustring) non-external l_.str 0000000000000070 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_GofClass 00000000000000b8 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_GofClass 00000000000000d8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_GofClass 0000000000000120 (__DATA,__objc_data) external _OBJC_METACLASS_$_GofClass 0000000000000148 (__DATA,__objc_data) external _OBJC_CLASS_$_GofClass
接下來我們看一下可執行文件:
//指令 xcrun nm -nm GofMachOFile //結果 (undefined) external _NSLog (from Foundation) (undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation) (undefined) external _OBJC_METACLASS_$_NSObject (from CoreFoundation) (undefined) external ___CFConstantStringClassReference (from CoreFoundation) (undefined) external __objc_empty_cache (from libobjc) (undefined) external _objc_msgSend (from libobjc) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100000ea0 (__TEXT,__text) external _main 0000000100000f10 (__TEXT,__text) non-external -[GofClass doSomethingWithName:] 0000000100001140 (__DATA,__objc_data) external _OBJC_METACLASS_$_GofClass 0000000100001168 (__DATA,__objc_data) external _OBJC_CLASS_$_GofClass
可以看到所有的 Foundation 和 Objective-C 運行時符號依舊是 undefined,不過現在的符號表中已經多了如何解析它們的信息,例如在哪個動態庫中可以找到對應的符號。
在運行時,動態鏈接器 dyld
也可以解析這些 undefined 符號,並指向它們在 Foundation 中的實現等。
當構建一個程序時,將會鏈接各種各樣的庫。它們又會依賴其他一些 framework 和 動態庫。需要加載的動態庫會非常多。而對於相互依賴的符號就更多了。可能將會有上千個符號需要解析處理,這將花費很長的時間:一般是好幾秒鍾。
為了縮短這個處理過程所花費時間,在 OS X 和 iOS 上的動態鏈接器使用了共享緩存,共享緩存存於 /var/db/dyld/
。對於每一種架構,操作系統都有一個單獨的文件,文件中包含了絕大多數的動態庫,這些庫都已經鏈接為一個文件,並且已經處理好了它們之間的符號關系。當加載一個 Mach-O 文件 (一個可執行文件或者一個庫) 時,動態鏈接器首先會檢查 共享緩存 看看是否存在其中,如果存在,那么就直接從共享緩存中拿出來使用。每一個進程都把這個共享緩存映射到了自己的地址空間中。這個方法大大優化了 OS X 和 iOS 上程序的啟動時間。
2.4可執行文件的瘦身
上面介紹了可執行文件的結構以及整個編譯過程,介紹這些內容是為了對可執行文件以及它的來歷有一個了解,下面我們分析一下對可執行文件的瘦身都有哪些方法。
我們知道,可執行文件是由我們編寫的代碼產生的。通過腳本分析前面說的LinkMap文件,我們可以更加清晰的知道具體的某個類在可執行文件中的大小。
#!usr/bin/python ## -*- coding: UTF-8 -*- # #使用簡介:python linkmap.py XXX-LinkMap-normal-xxxarch.txt 或者 python linkmap.py XXX-LinkMap-normal-xxxarch.txt | open -f #使用參數-g會統計每個模塊.o的統計大小 # __author__ = "zmjios" __date__ = "2017-05-05" import os import re import shutil import sys class SymbolModel: file = "" size = 0 def verify_linkmapfile(args): if len(sys.argv) < 2: print("請輸入linkMap文件") return False path = args[1] if not os.path.isfile(path): print("請輸入文件") return False file = open(path) content = file.read() file.close() #查找是否存在# Object files: if content.find("# Object files:") == -1: print("輸入linkmap文件非法") return False #查找是否存在# Sections: if content.find("# Sections:") == -1: print("輸入linkmap文件非法") return False #查找是否存在# Symbols: if content.find("# Symbols:") == -1: print("輸入linkmap文件非法") return False return True def symbolMapFromContent(): symbolMap = {} reachFiles = False reachSections = False reachSymblos = False file = open(sys.argv[1]) for line in file.readlines(): if line.startswith("#"): if line.startswith("# Object files:"): reachFiles = True if line.startswith("# Sections:"): reachSections = True if line.startswith("# Symbols:"): reachSymblos = True else: if reachFiles == True and reachSections == False and reachSymblos == False: #查找 files 列表,找到所有.o文件 location = line.find("]") if location != -1: key = line[:location+1] if symbolMap.get(key) is not None: continue symbol = SymbolModel() symbol.file = line[location + 1:] symbolMap[key] = symbol elif reachFiles == True and reachSections == True and reachSymblos == True: #'\t'分割成三部分,分別對應的是Address,Size和 File Name symbolsArray = line.split('\t') if len(symbolsArray) == 3: fileKeyAndName = symbolsArray[2] #16進制轉10進制 size = int(symbolsArray[1],16) location = fileKeyAndName.find(']') if location != -1: key = fileKeyAndName[:location + 1] symbol = symbolMap.get(key) if symbol is not None: symbol.size = symbol.size + size file.close() return symbolMap def sortSymbol(symbolList): return sorted(symbolList, key=lambda s: s.size,reverse = True) def buildResultWithSymbols(symbols): results = ["文件大小\t文件名稱\r\n"] totalSize = 0 for symbol in symbols: results.append(calSymbol(symbol)) totalSize += symbol.size results.append("總大小: %.2fM" % (totalSize/1024.0/1024.0)) return results def buildCombinationResultWithSymbols(symbols): #統計不同模塊大小 results = ["庫大小\t庫名稱\r\n"] totalSize = 0 combinationMap = {} for symbol in symbols: names = symbol.file.split('/') name = names[len(names) - 1].strip('\n') location = name.find("(") if name.endswith(")") and location != -1: component = name[:location] combinationSymbol = combinationMap.get(component) if combinationSymbol is None: combinationSymbol = SymbolModel() combinationMap[component] = combinationSymbol combinationSymbol.file = component combinationSymbol.size = combinationSymbol.size + symbol.size else: #symbol可能來自app本身的目標文件或者系統的動態庫 combinationMap[symbol.file] = symbol sortedSymbols = sortSymbol(combinationMap.values()) for symbol in sortedSymbols: results.append(calSymbol(symbol)) totalSize += symbol.size results.append("總大小: %.2fM" % (totalSize/1024.0/1024.0)) return results def calSymbol(symbol): size = "" if symbol.size / 1024.0 / 1024.0 > 1: size = "%.2fM" % (symbol.size / 1024.0 / 1024.0) else: size = "%.2fK" % (symbol.size / 1024.0) names = symbol.file.split('/') if len(names) > 0: size = "%s\t%s" % (size,names[len(names) - 1]) return size def analyzeLinkMap(): if verify_linkmapfile(sys.argv) == True: print("**********正在開始解析*********") symbolDic = symbolMapFromContent() symbolList = sortSymbol(symbolDic.values()) if len(sys.argv) >= 3 and sys.argv[2] == "-g": results = buildCombinationResultWithSymbols(symbolList) else: results = buildResultWithSymbols(symbolList) for result in results: print(result) print("***********解析結束***********") if __name__ == "__main__": analyzeLinkMap()
執行腳本之后,輸出結果示例如下:
從結果看到,不僅是我們編寫的類的大小可以統計出來,第三方的也可以。在實際工程中,我們可以對一些可執行文件中過大的第三方庫,思考其存在的必要性,對於不需要存在或者有替換方案的,可以考慮替換或刪除。
2.4.1清理無用代碼--AppCode
AppCode是一種智能的Objective-C集成開發環境,由專業的開發收費IDE的公司Jetbrains開發,具有這些特點:
- 最好的代碼助手:IDE深入的了解代碼結構,編輯器能提供准確的代碼實現選擇。通過代碼生成節省了不必要的輸入,減少了日常任務。
- 可靠的代碼重構:安全、准確和可靠的代碼重構允許我們隨時修改和提升代碼質量。
- 快速項目導航:通過類繼承可以從方法導航到它的聲明或使用處,或者直接從一個文件鏈接到另一個文件。支持即時跳轉到項目中的任何文件、類、標號處,或者查看標號的實際使用者,並不僅僅是文本匹配那么簡單。
- 代碼質量追蹤:支持對Objective-C、C、C++、JavaScript、CSS、HTML、XML和Xpath等進行動態代碼分析。AppCode能讓您避免潛在的錯誤,提示您哪些代碼可以改善。此外,它還集成了Clang Static Analyzer。
- 強大的代碼調試器:使用便攜調試器中靈活的斷點、窗口、框架視圖和求值表達式調整您的應用或單元測試。
- 無縫集成:AppCode完美地集成大部分流行的版本控制系統,如Git, Mercurial、Perforce等,還集成了Kiwi測試框架、Dash和成分文檔工具以及很多問題追蹤器,提供與Xcode100%的互操作性。
在這里,我們可以用它的inspect code來掃描無用代碼,包括無用的類、函數、宏定義、value、屬性等,而safe delete功能使得刪除一些由於runtime被調用到的代碼時更加安全智能。掃描結果示例:
【說明】:如果工程很大,這個掃描的時間可能會比較長。我們現在的工程中,大概有2700個類,掃描時間在一個半小時。
2.4.2清理無用類
實際上,在2.4.1的掃描結果中,包含無用類,但2.4.1的掃描時間會比較長,另外掃描出來的內容也較多。如果只是需要清理無用類的話,可以用如下腳本:
#!/usr/bin/env python # 使用方法:python py文件 Xcode工程文件目錄 # -*- coding:UTF-8 -*- import sys import os import re if len(sys.argv) == 1: print '請在.py文件后面輸入工程路徑' sys.exit() projectPath = sys.argv[1] print '工程路徑為%s' % projectPath resourcefile = [] totalClass = set([]) unusedFile = [] pbxprojFile = [] def Getallfile(rootDir): for lists in os.listdir(rootDir): path = os.path.join(rootDir, lists) if os.path.isdir(path): Getallfile(path) else: ex = os.path.splitext(path)[1] if ex == '.m' or ex == '.mm' or ex == '.h': resourcefile.append(path) elif ex == '.pbxproj': pbxprojFile.append(path) Getallfile(projectPath) print '工程中所使用的類列表為:' for ff in resourcefile: print ff for e in pbxprojFile: f = open(e, 'r') content = f.read() array = re.findall(r'\s+([\w,\+]+\.[h,m]{1,2})\s+',content) see = set(array) totalClass = totalClass|see f.close() print '工程中所引用的.h與.m及.mm文件' for x in totalClass: print x print '--------------------------' for x in resourcefile: ex = os.path.splitext(x)[1] if ex == '.h': #.h頭文件可以不用檢查 continue fileName = os.path.split(x)[1] print fileName if fileName not in totalClass: unusedFile.append(x) for x in unusedFile: resourcefile.remove(x) print '未引用到工程的文件列表為:' writeFile = [] for unImport in unusedFile: ss = '未引用到工程的文件:%s\n' % unImport writeFile.append(ss) print unImport unusedFile = [] allClassDic = {} for x in resourcefile: f = open(x,'r') content = f.read() array = re.findall(r'@interface\s+([\w,\+]+)\s+:',content) for xx in array: allClassDic[xx] = x f.close() print '所有類及其路徑:' for x in allClassDic.keys(): print x,':',allClassDic[x] def checkClass(path,className): f = open(path,'r') content = f.read() if os.path.splitext(path)[1] == '.h': match = re.search(r':\s+(%s)\s+' % className,content) else: match = re.search(r'(%s)\s+\w+' % className,content) f.close() if match: return True ivanyuan = 0 totalIvanyuan = len(allClassDic.keys()) for key in allClassDic.keys(): path = allClassDic[key] index = resourcefile.index(path) count = len(resourcefile) used = False offset = 1 ivanyuan += 1 print '完成',ivanyuan,'共:',totalIvanyuan,'path:%s'%path while index+offset < count or index-offset > 0: if index+offset < count: subPath = resourcefile[index+offset] if checkClass(subPath,key): used = True break if index - offset > 0: subPath = resourcefile[index-offset] if checkClass(subPath,key): used = True break offset += 1 if not used: str = '未使用的類:%s 文件路徑:%s\n' %(key,path) unusedFile.append(str) writeFile.append(str) for p in unusedFile: print '未使用的類:%s' % p filePath = os.path.split(projectPath)[0] writePath = '%s/未使用的類.txt' % filePath f = open(writePath,'w+') f.writelines(writeFile) f.close()
同樣的工程,這個腳本執行速度大概是三分鍾,結果如下:
2.4.3清理無用方法
以往C++在鏈接時,沒有被用到的類和方法是不會編進可執行文件里。但Objctive-C不同,由於它的動態性,它可以通過類名和方法名獲取這個類和方法進行調用,所以編譯器會把項目里所有OC源文件編進可執行文件里,哪怕該類和方法沒有被使用到。
結合LinkMap文件的__TEXT.__text,通過正則表達式([+|-][.+\s(.+)]),我們可以提取當前可執行文件里所有objc類方法和實例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可執行文件里引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll里哪些方法是沒有被引用的(SelectorsAll-UsedSelectorsAll)。
2.4.4編譯選項優化
- Build Settings->Optimization Level:release版應該選擇Fastest, Smalllest,這個選項會開啟那些不增加代碼大小的全部優化,並讓可執行文件盡可能小。
- Build Settings->Strip Debug Symbols During Copy: release版應該設置為YES,可以去除不必要的調試符號。
- Build Settings->Symbols Hidden by Default:release版應該設置為YES,會把所有符號都定義成”private extern”。
2.4.5其他項
- 類/方法命名長度 :從LinkMap可以發現每個類和方法名都在__cstring段里都存了相應的字符串值,所以類和方法名的長短也是對可執行文件大小是有影響的,原因還是Objective-C的動態特性,因為需要通過類/方法名反射找到這個類/方法進行調用,Objective-C對象模型會把類名,方法名列表都保存下來。實際上這部分占用的長度比較小,中型項目也就幾百K,可以忽略。
- ARC->MRC:ARC代碼會在某些情況多出一些retain和release的指令,例如調用一個方法,它返回的對象會被retain,退出作用域后會被release,MRC就不需要,匯編指令變多,機器碼變多,可執行文件就變大了。ARC對可執行文件大小的影響幾乎都是在代碼段,通過實驗,結論是ARC大概會使代碼段增加10%的size,考慮代碼段占可執行文件大約有80%,估計對整個可執行文件的影響會是8%。但考慮到它的可實施性,可以忽略。
3.參考資料