iOS安裝包瘦身的那些事兒


在我們提交安裝包到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.參考資料

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM