一、iOS App大小限制變化
1、App下載大小的限制變化
-
Apple不斷放寬在蜂窩網路下,從AppStore下載App的大小限制,大App們的大小都逼近或超過200MB,甚至突破250MB;
-
2013年9月,iOS 7正式版後,蜂窩網路下App下載大小的限制,從 50 MB 提升至 100 MB;
-
2017年9月,iOS11正式版本後,限制從 100 MB 提升至 150 MB,並在2019年5月下旬,將 150 MB"默默"放寬到200MB;
-
2019年9月,iOS13正式版本後,直接放開了蜂窩網路下App下載大小的限制,主要流量夠用,隨便下;
-
2020年4月1號了,我們日常用得比較多的App中,微信/手淘/美團App大小突破250MB;滴滴/抖音/快手App大小突破200MB;支付寶App逼近200MB(190MB+),美團外賣App一股清流,才115MB+。
2、可執行檔案大小的限制變化
-
根據Apple的稽核要求,上傳App Store的ipa的可執行檔案有大小限制,這裡的可執行檔案大小不是指二進位制(Mach-O)檔案大小,而是指二進位制(Mach-O)檔案中
__TEXT
部分的大小。 -
IOS 7版本之前, 二進位制檔案中所有
__TEXT
部分總和不得超過80MB; -
iOS 7.X 至 iOS 8.X,二進位制檔案中,每個 Architecture Slice(架構片段)中的
__TEXT
部分不得超過60MB- Architecture Slice是針對特定架構的胖二進位制佈局檔案的一部分。例如,一個胖二進位制檔案可能會包含針對 32 位和 64 位架構的片段。
-
iOS 9.0之後,二進位制檔案中所有
__TEXT
部分的總和不超過500 MB;具體可參考最大構建版本檔案大小 -
2020年4月1號了,幾乎所有的iOS App相容的最低版本都是iOS 9起步,如:微信/美團/美團外賣iOS App最低支援iOS10,支付寶/手淘/滴滴/抖音/快手iOS App最低支援iOS 9。
3、總結
- 隨著4G普及,5G到來,流量費用大大降低、Apple放開了對App大小方面的限制、iOS使用者升級系統意願高等因素,iOS開發者對包大小可以鬆口氣,如不必很擔心超過二進位制
__TEXT
部分的限制,可以優先業務迭代,有人力的情況下,再去做包瘦身; - 如果追求App的更高品質,在競品中拔得頭籌,還是需要在包大小方面花很大功夫;一般來說,ROI最高的是無用資源(主要是圖片)的清理,其次是二進位制檔案大小的優化;二進位制檔案大小的優化一個是靠優化編譯器選項,一個是清理無用的類、函式和程式碼塊等。
- 網路上有很多類似的包優化的部落格可以參考,本文就不說了,本文主要介紹
Mach-O檔案
周邊的知識:Mach-O檔案本身、 分析工具和Link Map File等。
二、Mach-O檔案簡介
1、概述
Mach-O
格式全稱為Mach Object檔案格式的縮寫,是MacOS或者iOS上可執行的程式格式,類似於Windows上的PE格式 (Portable Executable),linux上的ELF格式 (Executable and Linking Format)。Mach-O檔案
的分類有如下5類:- Executable:應用的可執行檔案
- Dylib Library:動態連結庫(又稱DSO或DLL)
- Static Library:靜態連結庫
- Bundle:不能被連結的Dylib,只能在執行時使用dlopen( )載入,可當做macOS的外掛
- Relocatable Object File :可重定向檔案型別
2、Mach-O檔案的組成
Mach-O檔案主要包括三部分內容: Header(頭部)、Load Commands(載入命令)、Data(資料區)
-
Header(頭部),指明瞭 CPU 架構、大小端序、檔案型別、Load Commands 個數等一些基本資訊,Headers 能幫助校驗 Mach-O 合法性和定位檔案的執行環境,64位架構為例,Header結構定義如下:
struct mach_header_64 { uint32_t magic; /* mach magic number identifier 魔數,用於快速確認該檔案用於64位還是32位 */ cpu_type_t cputype; /* cpu specifier,CPU**型別,比如 arm */ cpu_subtype_t cpusubtype; /* machine specifier,對應的具體型別,比如arm64、armv7 */ uint32_t filetype; /* type of file,檔案型別,比如可執行檔案、庫檔案、Dsym檔案,demo中是2 `MH_EXECUTE`,代表可執行檔案*/ uint32_t ncmds; /* number of load commands 載入命令條數 */ uint32_t sizeofcmds; /* the size of all the load commands 所有載入命令的大小 */ uint32_t flags; /* flags 標誌位 */ uint32_t reserved; /* reserved 保留欄位 */ }; 複製程式碼
-
Load Commands(載入命令),包含 Mach-O 裡命令型別資訊,名稱和二進位制檔案的位置;以64位架構為例,Load Commands結構定義如下:
struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* cmd是Load commands的型別,LC_SEGMENT_64代表將檔案中64位的段對映到程式的地址空間*/ uint32_t cmdsize; /* includes sizeof section_64 structs 代表load command的大小 */ char segname[16]; /* segment name */ uint64_t vmaddr; /* memory address of this segment 段的虛擬記憶體地址 */ uint64_t vmsize; /* memory size of this segment 段的虛擬記憶體大小 */ uint64_t fileoff; /* file offset of this segment 段在檔案中偏移量 */ uint64_t filesize; /* amount to map from the file 段在檔案中的大小 */ vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */ uint32_t nsects; /* number of sections in segment 標示了Segment中有多少secetion */ uint32_t flags; /* flags */ }; 複製程式碼
- 載入命令告訴載入器如何處理二進位制資料,有些命令是由核心處理的,有些是由動態連結器處理的;LC_SEGMENT_64
和
LC_SEGMENT` 是載入的主要命令, 他們指導核心來設定程式的記憶體空間;
- 載入命令告訴載入器如何處理二進位制資料,有些命令是由核心處理的,有些是由動態連結器處理的;LC_SEGMENT_64
-
Data(資料區)由Segment 的資料組成,是
Mach-O
中 佔比最多的部分,有程式碼有資料,比如符號表。Data 共三個 Segment:__TEXT
(包含執行程式碼以及其他只讀資料)、__DATA
(程式資料,該段可寫)、__LINKEDIT
(包含連結器使用的符號以及其他表)。-
其中,
__TEXT
和 __DATA
對應一個或多個 Section,__LINKEDIT
沒有 Section,需要配合LC_SYMTAB
來解析 symbol table 和 string table。這些裡面是 Mach-O 的主要資料。 -
以64位架構為例,Section的結構定義如下:
struct section_64 { /* for 64-bit architectures */ char sectname[16]; /* name of this section 比如_text、stubs */ char segname[16]; /* segment this section goes in 該section所屬的segment,比如__TEXT*/ uint64_t addr; /* memory address of this section 該section在記憶體的起始位置 */ uint64_t size; /* size in bytes of this section 該section的大小*/ uint32_t offset; /* file offset of this section 該section的檔案偏移*/ uint32_t align; /* section alignment (power of 2) 位元組大小對齊*/ uint32_t reloff; /* file offset of relocation entries 重定位入口的檔案偏移 */ uint32_t nreloc; /* number of relocation entries 需要重定位的入口數量 */ uint32_t flags; /* flags (section type and attributes) 包含section的type和attributes*/ uint32_t reserved1; /* reserved (for offset or index) */ uint32_t reserved2; /* reserved (for count or sizeof) */ uint32_t reserved3; /* reserved */ }; 複製程式碼
-
備註:
__TEXT
代表的是Segment,小寫的__text
代表 Section
-
3、FatFile/FatBinary
FatFile/FatBinary
直譯“胖二進位制”,是一個由不同的編譯架構的Mach-O產物合成的集合體。一個架構的Mach-O
只能在相同架構的機器或者模擬器上用,為了支援不同架構需要一個集合體。- 這裡的架構是指CPU的指令集,iOS裝置使用的是ARM處理器,ARM支援的指令集有兩類:32位ARM指令集(armv7|armv7s)和 64位ARM指令集(arm64和arm64e)
- 此外,還有i386(32位)和x86_64(64位)這兩個Mac處理器的指令集,iOS模擬器沒有執行ARM指令集,執行在iOS模擬器上的App需要支援i386 or x86_64的指令集。
三、分析Mach-O的基礎命令
1、lipo命令
-
管理Fat File的工具, 可以檢視CPU架構, 提取特定架構,整合和拆分庫檔案
-
常用的方法如下:
# 【查】看胖二進位制支援的CPU架構列表 lipo -info xxxx.a/xxxx.framework/xxxx # 【拆】從胖二進位制中提取特定CPU架構的二進位制 lipo lxx.a -thin cpu_type(armv7s/arm64等) -output xx_cpu_type.a # 【合】整合成Fat檔案 lipo -create xxxx1 xxxx2 -output xxxxfat #【刪】移除掉特定的cpu架構的檔案 lipo -remove cpu_type(armv7s/arm64等) xxxx -output xxxx 複製程式碼
2、ar命令
-
常用來建立、修改庫,從庫中提出單個模組。
-
常使用ar命令解壓.a檔案,但是如果直接解壓第三方SDK的.a檔案(如微信SDK),會遇到
xxx.a is a fat file (use libtool(1) or lipo(1) and ar(1) on it)
的錯誤。 -
這是因為這類.a檔案是一個胖二進位制,包含了多個CPU架構,需要先使用lipo檔案來提取特定的CPU架構的二進位制檔案,使用如下:
# 拆分出個arm64架構的二進位制 lipo xx. a -thin arm64 -output xx_arm64.a # 解壓.a檔案 ar -x xx_arm64.a 複製程式碼
3、nm命令
-
被用於顯示二進位制目標檔案的符號表(display name list (symbol table))
-
常用的方法如下:
# 得到Mach-O中的程式符號表 nm path # 目標檔案的所有符號 nm -nm path 複製程式碼
4、grep命令
-
用來判斷是否包含字串
-
常用的方法如下:
# 檢查是否包含xxx字串: grep -r "xxx” path 複製程式碼
四、otool工具使用簡介
otool
(object file displaying tool),可以對指定目標檔案或者庫檔案以特定的方法解析顯示,是分析Mach-O檔案的利器。(一般安裝了Xcode,預設安裝了otool)
1、檢視Mach-O的header
otool -h app_name.app/app_name
複製程式碼
- header資訊包括:magic、cputype、cpusubtype、caps、filetype、ncmds、sizeofcmds和flags
2、檢視Mach-O的load commands
otool -l app_name.app/app_name
複製程式碼
- 資訊主要包括Mach-O 裡命令型別資訊,名稱和二進位制檔案的位置。
3、檢視Mach-O依賴的動態庫
otool -L app_name.app/app_name
複製程式碼
- 動態庫資訊包括:動態庫名稱、當前版本號、相容版本號
4、檢視Mach-O檔案的加密資訊
otool -l app_name.app/app_name | grep crypt
複製程式碼
- 執行結果中cryptid有 0(未加密)和1(加密) 兩個取值
5、檢視Mach-O檔案中所有類和引用類(地址)
# 獲取所有類的地址
otool -v -s __DATA __objc_classlist app_name.app/app_name
# 獲取所有引用類的地址
otool -v -s __DATA __objc_classrefs app_name.app/app_name
複製程式碼
- 可以利用這兩個結果的差值,然後進行符號化,就可以得到未被引用的類資訊。不過,需要注意的是:未引用的類不等於未使用的類,一些實際使用(動態呼叫等)也可能被誤認為是未使用的類。
6、擴充套件:MachOView工具
- 使用
otool
固然方便,但是也可以使用MachOView工具來檢視Mach-O檔案,更加直觀,很方便看到 Mach-O檔案header、 load commands等資訊,具體使用見Mach-O檔案瀏覽器---MachOView - MachOView的工具介面左上角有一個 RAW、RVA 的選項。
- RAW 就是指該位元組相對於檔案開始部分的絕對偏移,檔案頭部的地址是從0x000開始的。
- RVA 是相對於某個基地址的偏移,也就是整體的絕對偏移值再加上某個基地址,檔案頭部的地址是從某個值(基地址)開始的。
五、class-dump工具使用簡介
1、概述
- class-dump用來dump
Mach-O
檔案的class資訊;它利用OC語言的Runtime特性,將儲存在Mach-O檔案中的標頭檔案資訊提取出來,並生成對應的.h檔案。 - 逆向中也常用到class-dump這個工具
2、下載和安裝
- 從 Class-dump地址 下載最新的dmg檔案
- 開啟dmg檔案,將其中的class-dump拷貝到目錄中,比如
$HOME/custom-tool/bin
目錄下 - 開啟~/.bash_profile檔案:vi ~/.bash_profile,在檔案最上方加一行:
export PATH=$HOME/custom-tool/bin/:$PATH
,然後儲存並退出 - 執行
source ~/.bash_profile
; - 至此,class-dump工具生效。
3、使用
-
獲取
ipa
檔案,修改字尾名為.zip
,解壓後,獲取Payload
檔案中的app檔案; -
需要注意的是,從App Store下載的app檔案都是經過加密的,可執行檔案被加上了一層外殼,class-dump無法直接作用於這樣的檔案。需要使用其它方式將外殼破壞才可以。
-
將app檔案放到指定目錄下,進入該目錄,執行如下命令
# 匯出Mach-O標頭檔案(標頭檔案內容按名字排序) class-dump -H Mach-O檔案路徑 -o 標頭檔案存放目錄 複製程式碼
- -H 表示要生成標頭檔案
- -o用於制定標頭檔案的存放目錄
-
補充統計檔案和資料夾數的命令
# 檢視某個檔案下的檔案個數,包括子檔案裡的 ls -lR|grep "^-"|wc -l # 檢視某檔案下的資料夾的個數,包括子資料夾裡的 ls -lR|grep "^d"|wc -l 複製程式碼
六、Link Map File
1、概述
- 原始碼經過編譯階段,每個類會生成對應的.o檔案(目標檔案);然後在連結階段,把.o檔案和動態庫連結在一起,最終生成可執行檔案;
- Linkmap是iOS編譯過程的中間產物,記錄了二進位制檔案的佈局,裡面記錄了可執行檔案的路徑、CPU架構、目標檔案、符號等資訊。
- 通過Link Map File可以瞭解記憶體分段、分割槽、分析可執行檔案中類或庫佔用空間(可以知道App瘦身)
- Link Map File可以設定
工程->Build Setting->Write Link Map File
為YES,Build後生成Link Map File檔案的功能;還可以通過設定Path to Link Map File
,指定Link Map File
存放的路徑。
2、Link Map File的重要組成
-
Path & Arch:Path是可執行檔案的路徑,Arch是架構型別。
# Path: /Users/xxx/Library/Developer/Xcode/DerivedData/..../app_name.app/app_name # Arch: arm64 複製程式碼
-
Object Files:生成二進位制用到的link單元(包括.o檔案和dylib庫)的路徑和檔案編號;通過類編號可以對應到具體的類。在後面的Symbols部分,我們會用到類編號。
# Object files: [ 0] linker synthesized [ 1] /Users/xxxx/Library/Developer/Xcode/DerivedData/..../AppDelegate.o [ 2] /Users/xxxx/Library/Developer/Xcode/DerivedData/..../main.o # ... 複製程式碼
-
Sections: 記錄Mach-O中每個Segment/section的地址範圍。Mach-O中有三類的Segement,Segement劃分成了不同的Section,不同的Section儲存著不同的資訊:Segement主要有三類:
__TEXT
、__DATA
和__LINKEDIT
__TEXT
包含 Mach header,被執行的程式碼和只讀常量(如C 字串),只讀可執行__DATA
包含全域性變數,靜態變數等,可讀寫__LINKEDIT
包含包含了載入程式的『後設資料』,比如函式的名稱和地址,只讀。
# 第一列是Section起始位置,第二列是Section佔用記憶體大小,第三列是Segment型別,第四列是Section型別。 # Sections: # Address Size Segment Section 0x100002780 0x0129617D __TEXT __text 0x1012988FE 0x000015E4 __TEXT __stubs # ... 複製程式碼
-
Symbols: 按順序記錄每個符號的地址範圍
# Symbols: // __text程式碼區 # Address Size File Name 0x100002780 0x00000450 [ 2] -[UIButton(SSEdgeInsets) setImageUpTitleDownWithSpacing:] 0x100002BD0 0x00000070 [ 2] _UIEdgeInsetsMake # ... 複製程式碼
- 根據
Address
確定分佈的區域,如__TEXT段的__text區
(儲存著程式碼),__TEXT段的__objc_methname區
(儲存著方法名)、__DA他的__objc_classlist區
(儲存所有的類)等; - 根據
Address
,還可以通過符號表找到對應出具體的方法名Name
(方法名越長,最終佔用的記憶體也越大) - 根據
File
編號找到程式碼屬於哪個類; __objc_classlist區
的size值都是8,區域裡儲存的值都是一個指標,指向了類的虛擬地址。
- 根據
3、功能
- 分析二進位制中類和庫大小:在Symbols部分,我們可以把類編號相同的size加起來,可以計算出類的大小;將同一個庫中類大小統計在一起,可以計算庫的大小。現成分析工具LinkMap
- 找到未引用的類:利用
_objc_classname
(所有類名)和__objc_classrefs
(引用到的類)的差集找到未引用的類(未引用的類未必是未使用的類) - 找到未引用的方法:_
objc_methname
(所有的方法)和__objc_selrefs
(引用的方法)的差別,找到未引用的方法(未引用的方法未必是未使用的方法) - Link Map File還有很多可挖掘的用處
歷史文章
iOS App瘦身小記 -- 基本給出了App瘦身一些建議
PNG圖片原理二三事 -- 基本介紹了PNG原理,然後就對App瘦身中圖片壓縮佛繫了
文件參考
Apple 將 iOS AppStore 下載限制從 150M 提高至 200M