iOS微信安装包瘦身
前提
微信經過多次版本迭代,產生不少冗余代碼和無用資源。之前微信也沒有很好的手段知道哪個模塊增量多少。另外去年10月微信開始做ARC支持,目的是為了減少野指針帶來的Crash,但代價是可執行文件增大20%左右。而蘋果規定今年6月提交給Appstore的應用必須支持64位,32位和64位兩個架構的存在使得可執行文件增加了一倍多。安裝包大小優化迫在眉睫。
Appstore安裝包是由資源和可執行文件兩部分組成,安裝包瘦身也是從這兩部分進行。
資源瘦身
資源瘦身主要是去掉無用資源和壓縮資源,資源包括圖片、音視頻文件、配置文件以及多語言wording。無用資源是指資源在工程文件里,但沒有被代碼引用。檢查方法是,用資源關鍵字(通常是文件名,圖片資源需要去掉@2x @3x),搜索代碼,搜不到就是沒有被引用。當然,有些資源在使用過程中是拼接而成的(如loading_xxx.png),需要手工過濾。
資源壓縮主要對png進行無損壓縮,用的是ImageOptim工具和compress命令(需要安裝XQuartz-2.7.5.dm插件)。不建議對資源做有損壓縮,有損壓縮需要設計一個個檢查,通常壓縮后效果不盡人意。
Xcode's Link Map File
在講可執行文件瘦身之前先介紹Xcode的LinkMap文件。LinkMap文件是Xcode產生可執行文件的同時生成的鏈接信息,用來描述可執行文件的構造成分,包括代碼段(__TEXT)和數據段(__DATA)的分布情況。只要設置Project->Build Settings->Write Link Map File為YES,并設置Path to Link Map File,build完后就可以在設置的路徑看到LinkMap文件了:
每個LinkMap由3個部分組成,以微信為例:
1. Object files:
[ 0] linker synthesized
[ 1] /xxxx/WCPayInfoItem.o
[ 2] /xxxx/GameCenterFriendRankCell.o
[ 3] /xxxx/WloginTlv_0x168.o
...
第一部分列舉可執行文件里所有.obj文件,以及每個文件的編號。
2. Sections:
第二部分是可執行文件的段表,描述各個段在可執行文件中的偏移位置和大小。第一列是段的偏移量,第二列是段占用大小,Address(n)=Address(n-1)+Size(n-1);第三列是段類型,代碼段和數據段;第四列是段名字,如__text是可執行機器碼,__cstring是字符串常量。有關段的概念可參考蘋果官方文檔《OS X ABI Mach-O File Format Reference》
3. Symbols:
# Address Size File Name
0x100005A50 0x00000074 [ 1] +[WCPayInfoItem initialize]
...
0x10231C120 0x00000018 [ 1] literal string: I16@?0@"WCPayInfoItem"8
...
0x10252A41A 0x0000000E [ 1] literal string: WCPayInfoItem
...
第三部分詳細描述每個obj文件在每個段的分布情況,按第二部分Sections順序展示。例如序號1的WCPayInfoItem.o文件,+[WCPayInfoItem initialize]方法在__TEXT.__text地址是0x100005A50,占用大小是116字節。根據序號累加每個obj文件在每個段的占用大小,從而計算出每個obj文件在可執行文件的占用大小,進而算出每個靜態庫、每個功能模塊代碼占用大小。這里要注意的地方是,由于__DATA.__bbs是代表未初始化的靜態變量,Size表示應用運行時占用的堆大小,并不占用可執行文件,所以計算obj占用大小時,要排除這個段的Size。
可執行文件瘦身
回到我們的可執行文件瘦身問題,LinkMap文件可以幫助我們尋找優化點。
1. 查找無用selector
以往C++在鏈接時,沒有被用到的類和方法是不會編進可執行文件里。但Objctive-C不同,由于它的動態性,它可以通過類名和方法名獲取這個類和方法進行調用,所以編譯器會把項目里所有OC源文件編進可執行文件里,哪怕該類和方法沒有被使用到。
結合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也可以這樣掃出來的。
2. 查找無用oc類
查找無用oc類有兩種方式,一種是類似于查找無用資源,通過搜索"[ClassName alloc/new"、"ClassName *"、"[ClassName class]"等關鍵字在代碼里是否出現。另一種是通過otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段來獲取當前所有oc類和被引用的oc類,兩個集合相減就是無用oc類。
3. 掃描重復代碼
可以利用第三方工具simian掃描。南非支付copy代碼就是這樣被發現的。但除此成果之外,掃描出來的結果過多,重構起來也不方便,不如砍功能需求效果好。
4. protobuf精簡改造
protobuf是Google推出的一種輕量高效的結構化數據存儲格式,在微信用于網絡協議和本地文件序列化。但google默認工具生成的代碼比較冗余,像序列化、反序列化、計算序列化大小等方法都生成在具體的pb類里,每個類的實現大同小異。通過代碼分析以及結合protobuf原理,要想把這些方法抽象到基類,派生類提供每個字段相關信息就夠了:
-
field number
-
field label, optional, required or repeated
-
wire type, double, float, int, etc
-
是否packed
-
repeated的數據類型
| 1 2 3 4 5 6 7 8 9 10 11 12 | typedef?struct?{ ????Byte?_fieldNumber; ????Byte?_fieldLabel; ????Byte?_fieldType; ????BOOL?_isPacked; ????int?_enumInitValue; ????union?{ ????????__unsafe_unretained?NSString*?_messageClassName; ????????__unsafe_unretained?Class?_messageClass;?//?ClassName對應的Class ????????IsEnumValidFunc?_isEnumValidFunc;?//?檢測枚舉值是否合法函數指針 ????}; }?PBFieldInfo; |
另外通過無用selector列表,發現不少pb類屬性的getter或setter沒有被使用。原先的pb類屬性是用@synthesize修飾,編譯器會自動生成getter和setter。如果不想編譯器生成,則要用@dynamic。甚至我們可以把pb類的成員變量去掉。做法如下:
-
基類增加id類型數組ivarValues(參考了objc_class結構體ivars做法),用于存放對象的屬性值。對象屬性值統一用oc對象表示,如果類型是基礎類型(primitive,如int、float等),則用NSValue存
-
重載methodSignatureForSelector:方法,返回屬性getter、setter的方法簽名
-
重載forwardInvocation:方法,分析invocation.selector類型。如果是getter,從ivarValues獲取屬性值并設置為invocation的returnValue;如果是setter,從invocation第二個argument獲取屬性值,并存放到ivarValues里
-
重載setValue:forUndefinedKey:、valueForUndefinedKey:,防止通過KVO訪問屬性Crash
-
做下性能優化,如pb類在initialize做一次初始化,緩存屬性名的hash值,屬性的getter、setter方法的objcType等;屬性值不用std::map(屬性名->屬性值),而是改用數組;MRC代替ARC(有些時候ARC自動添加的retain/release挺影響性能的);等等
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class?PBClassInfo?{ public: ????PBClassInfo(Class?cls,?PBFieldInfo*?fieldInfo); ????~PBClassInfo(); public: ????unsigned?int?_numberOfProperty; ????std::string*?_propertyNames; ????size_t*?_propertyNameHashes; ????std::string*?_getterObjCTypes; ????std::string*?_setterObjCTypes; ????PBFieldInfo*?_fieldInfos; }; @interface?WXPBGeneratedMessage?()?{ ????uint32_t?_has_bits_[3];?//?最多96個屬性,表示屬性是否有賦值 ????int32_t?_serializedSize; ????PBClassInfo*?_classInfo; ????id*?_ivarValues; } -?(NSMethodSignature*)?methodSignatureForSelector:(SEL)?aSelector; -?(void)?forwardInvocation:(NSInvocation*)?anInvocation; -?(void)?setValue:(id)?value?forUndefinedKey:(NSString*)?key; -?valueForUndefinedKey:(NSString*)?key; @end |
把冗余代碼去掉后,整個類清爽多了。像GameResourceReq只有3個屬性的proto結構體,類方法代碼行數由以前的127行變成現在的8行。protobuf精簡改造中,精簡類方法減少了可執行文件8.8M,去掉類成員變量和類屬性改用@dynamic減少了2.5M。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | message?GameResourceReq?{ ????required?BaseRequest?BaseRequest?=?1; ????required?int32?PropsCount?=?2; ????repeated?uint32?PropsIdList?=?3[packed=true]; } //?老實現 @implementation?GameResourceReq @synthesize?hasBaseRequest; @synthesize?baseRequest; @synthesize?hasPropsCount; @synthesize?propsCount; @synthesize?mutablePropsIdListList; @dynamic?propsIdList; -?(id)?init?{...} -?(void)?SetBaseRequest:(BaseRequest*)?value?{...} -?(void)?SetPropsCount:(int32_t)?value?{...} -?(NSArray*)?propsIdListList?{...} -?(NSMutableArray*)propsIdList?{...} -?(void)setPropsIdList:(NSMutableArray*)?values?{...} -?(BOOL)?isInitialized?{...} -?(void)?writeToCodedOutputStream:(PBCodedOutputStream*)?output?{...} -?(int32_t)?serializedSize?{...} +?(GameResourceReq*)?parseFromData:(NSData*)?data?{...} -?(GameResourceReq*)?mergeFromCodedInputStream:(PBCodedInputStream*)?input?{...} -?(void)?addPropsIdList:(uint32_t)?value?{...} -?(void)?addPropsIdListFromArray:(NSArray*)?values?{...} @end //?新實現 @implementation?GameResourceReq PB_PROPERTY_TYPE?baseRequest; PB_PROPERTY_TYPE?opType; PB_PROPERTY_TYPE?brandUserName; +?(void)?initialize?{ ??static?PBFieldInfo?_fieldInfoArray[]?=?{ ????{1,?FieldLabelRequired,?FieldTypeMessage,?NO,?0,?._messageClassName?=?STRING_FROM(BaseRequest)}, ????{2,?FieldLabelRequired,?FieldTypeInt32,?NO,?0,?0}, ????{3,?FieldLabelRepeated,?FieldTypeUint32,?NO,?0,?0}, ??}; ??initializePBClassInfo(self,?_fieldInfoArray); } @end |
5. 編譯選項優化
-
Strip Link Product設成YES,WeChatWatch可執行文件減少0.3M
-
Make Strings Read-Only設為YES,也許是因為微信工程從低版本Xcode升級過來,這個編譯選項之前一直為NO,設為YES后可執行文件減少了3M
-
去掉異常支持,Enable C++ Exceptions和Enable Objective-C Exceptions設為NO,并且Other C Flags添加-fno-exceptions,可執行文件減少了27M,其中__gcc_except_tab段減少了17.3M,__text減少了9.7M,效果特別明顯。可以對某些文件單獨支持異常,編譯選項加上-fexceptions即可。但有個問題,假如ABC三個文件,AC文件支持了異常,B不支持,如果C拋了異常,在模擬器下A還是能捕獲異常不至于Crash,但真機下捕獲不了(有知道原因可以在下面留言:)。去掉異常后,Appstore后續幾個版本Crash率沒有明顯上升。個人認為關鍵路徑支持異常處理就好,像啟動時NSCoder讀取setting配置文件得要支持捕獲異常,等等
6. 其他可探索途徑
-
iOS8 Embed-Framework:提取WeChatWatch、ShareExtention和微信主工程的公共代碼,可執行文件可以減少5M+,不過這特性需要最低版本iOS8才能用,iOS7設備啟動會crash
-
iOS9 App Thinning:嚴格來說App Thinning不會讓安裝包變小,但用戶安裝應用時,蘋果會根據用戶的機型自動選擇合適的資源和對應CPU架構的二進制執行文件(也就是說用戶本地可執行文件不會同時存在armv7和arm64),安裝后空間占用更小
7. 建立監控
通過對LinkMap文件的分析,可以得知每個模塊可執行文件占用大小。再對比兩個版本,就知道業務模塊的增量大小。參考如下:
總結
以上是生活随笔為你收集整理的iOS微信安装包瘦身的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 51单片机50个实例代码_【附代码】51
- 下一篇: 热血街头Java,下载_我爱法语 V3.