iOS微信安装包瘦身
前提
微信經(jīng)過多次版本迭代,產(chǎn)生不少冗余代碼和無用資源。之前微信也沒有很好的手段知道哪個(gè)模塊增量多少。另外去年10月微信開始做ARC支持,目的是為了減少野指針帶來的Crash,但代價(jià)是可執(zhí)行文件增大20%左右。而蘋果規(guī)定今年6月提交給Appstore的應(yīng)用必須支持64位,32位和64位兩個(gè)架構(gòu)的存在使得可執(zhí)行文件增加了一倍多。安裝包大小優(yōu)化迫在眉睫。
Appstore安裝包是由資源和可執(zhí)行文件兩部分組成,安裝包瘦身也是從這兩部分進(jìn)行。
資源瘦身
資源瘦身主要是去掉無用資源和壓縮資源,資源包括圖片、音視頻文件、配置文件以及多語言wording。無用資源是指資源在工程文件里,但沒有被代碼引用。檢查方法是,用資源關(guān)鍵字(通常是文件名,圖片資源需要去掉@2x @3x),搜索代碼,搜不到就是沒有被引用。當(dāng)然,有些資源在使用過程中是拼接而成的(如loading_xxx.png),需要手工過濾。
資源壓縮主要對(duì)png進(jìn)行無損壓縮,用的是ImageOptim工具和compress命令(需要安裝XQuartz-2.7.5.dm插件)。不建議對(duì)資源做有損壓縮,有損壓縮需要設(shè)計(jì)一個(gè)個(gè)檢查,通常壓縮后效果不盡人意。
Xcode's Link Map File
在講可執(zhí)行文件瘦身之前先介紹Xcode的LinkMap文件。LinkMap文件是Xcode產(chǎn)生可執(zhí)行文件的同時(shí)生成的鏈接信息,用來描述可執(zhí)行文件的構(gòu)造成分,包括代碼段(__TEXT)和數(shù)據(jù)段(__DATA)的分布情況。只要設(shè)置Project->Build Settings->Write Link Map File為YES,并設(shè)置Path to Link Map File,build完后就可以在設(shè)置的路徑看到LinkMap文件了:
每個(gè)LinkMap由3個(gè)部分組成,以微信為例:
1. Object files:
[ 0] linker synthesized
[ 1] /xxxx/WCPayInfoItem.o
[ 2] /xxxx/GameCenterFriendRankCell.o
[ 3] /xxxx/WloginTlv_0x168.o
...
第一部分列舉可執(zhí)行文件里所有.obj文件,以及每個(gè)文件的編號(hào)。
2. Sections:
第二部分是可執(zhí)行文件的段表,描述各個(gè)段在可執(zhí)行文件中的偏移位置和大小。第一列是段的偏移量,第二列是段占用大小,Address(n)=Address(n-1)+Size(n-1);第三列是段類型,代碼段和數(shù)據(jù)段;第四列是段名字,如__text是可執(zhí)行機(jī)器碼,__cstring是字符串常量。有關(guān)段的概念可參考蘋果官方文檔《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
...
第三部分詳細(xì)描述每個(gè)obj文件在每個(gè)段的分布情況,按第二部分Sections順序展示。例如序號(hào)1的WCPayInfoItem.o文件,+[WCPayInfoItem initialize]方法在__TEXT.__text地址是0x100005A50,占用大小是116字節(jié)。根據(jù)序號(hào)累加每個(gè)obj文件在每個(gè)段的占用大小,從而計(jì)算出每個(gè)obj文件在可執(zhí)行文件的占用大小,進(jìn)而算出每個(gè)靜態(tài)庫、每個(gè)功能模塊代碼占用大小。這里要注意的地方是,由于__DATA.__bbs是代表未初始化的靜態(tài)變量,Size表示應(yīng)用運(yùn)行時(shí)占用的堆大小,并不占用可執(zhí)行文件,所以計(jì)算obj占用大小時(shí),要排除這個(gè)段的Size。
可執(zhí)行文件瘦身
回到我們的可執(zhí)行文件瘦身問題,LinkMap文件可以幫助我們尋找優(yōu)化點(diǎn)。
1. 查找無用selector
以往C++在鏈接時(shí),沒有被用到的類和方法是不會(huì)編進(jìn)可執(zhí)行文件里。但Objctive-C不同,由于它的動(dòng)態(tài)性,它可以通過類名和方法名獲取這個(gè)類和方法進(jìn)行調(diào)用,所以編譯器會(huì)把項(xiàng)目里所有OC源文件編進(jìn)可執(zhí)行文件里,哪怕該類和方法沒有被使用到。
結(jié)合LinkMap文件的__TEXT.__text,通過正則表達(dá)式([+|-][.+\s(.+)]),我們可以提取當(dāng)前可執(zhí)行文件里所有objc類方法和實(shí)例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可執(zhí)行文件里引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll里哪些方法是沒有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系統(tǒng)API的Protocol可能被列入無用方法名單里,如UITableViewDelegate的方法,我們只需要對(duì)這些Protocol里的方法加入白名單過濾即可。
另外第三方庫的無用selector也可以這樣掃出來的。
2. 查找無用oc類
查找無用oc類有兩種方式,一種是類似于查找無用資源,通過搜索"[ClassName alloc/new"、"ClassName *"、"[ClassName class]"等關(guān)鍵字在代碼里是否出現(xiàn)。另一種是通過otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段來獲取當(dāng)前所有oc類和被引用的oc類,兩個(gè)集合相減就是無用oc類。
3. 掃描重復(fù)代碼
可以利用第三方工具simian掃描。南非支付copy代碼就是這樣被發(fā)現(xiàn)的。但除此成果之外,掃描出來的結(jié)果過多,重構(gòu)起來也不方便,不如砍功能需求效果好。
4. protobuf精簡改造
protobuf是Google推出的一種輕量高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式,在微信用于網(wǎng)絡(luò)協(xié)議和本地文件序列化。但google默認(rèn)工具生成的代碼比較冗余,像序列化、反序列化、計(jì)算序列化大小等方法都生成在具體的pb類里,每個(gè)類的實(shí)現(xiàn)大同小異。通過代碼分析以及結(jié)合protobuf原理,要想把這些方法抽象到基類,派生類提供每個(gè)字段相關(guān)信息就夠了:
-
field number
-
field label, optional, required or repeated
-
wire type, double, float, int, etc
-
是否packed
-
repeated的數(shù)據(jù)類型
| 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對(duì)應(yīng)的Class ????????IsEnumValidFunc?_isEnumValidFunc;?//?檢測(cè)枚舉值是否合法函數(shù)指針 ????}; }?PBFieldInfo; |
另外通過無用selector列表,發(fā)現(xiàn)不少pb類屬性的getter或setter沒有被使用。原先的pb類屬性是用@synthesize修飾,編譯器會(huì)自動(dòng)生成getter和setter。如果不想編譯器生成,則要用@dynamic。甚至我們可以把pb類的成員變量去掉。做法如下:
-
基類增加id類型數(shù)組ivarValues(參考了objc_class結(jié)構(gòu)體ivars做法),用于存放對(duì)象的屬性值。對(duì)象屬性值統(tǒng)一用oc對(duì)象表示,如果類型是基礎(chǔ)類型(primitive,如int、float等),則用NSValue存
-
重載methodSignatureForSelector:方法,返回屬性getter、setter的方法簽名
-
重載forwardInvocation:方法,分析invocation.selector類型。如果是getter,從ivarValues獲取屬性值并設(shè)置為invocation的returnValue;如果是setter,從invocation第二個(gè)argument獲取屬性值,并存放到ivarValues里
-
重載setValue:forUndefinedKey:、valueForUndefinedKey:,防止通過KVO訪問屬性Crash
-
做下性能優(yōu)化,如pb類在initialize做一次初始化,緩存屬性名的hash值,屬性的getter、setter方法的objcType等;屬性值不用std::map(屬性名->屬性值),而是改用數(shù)組;MRC代替ARC(有些時(shí)候ARC自動(dòng)添加的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個(gè)屬性,表示屬性是否有賦值 ????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 |
把冗余代碼去掉后,整個(gè)類清爽多了。像GameResourceReq只有3個(gè)屬性的proto結(jié)構(gòu)體,類方法代碼行數(shù)由以前的127行變成現(xiàn)在的8行。protobuf精簡改造中,精簡類方法減少了可執(zhí)行文件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]; } //?老實(shí)現(xiàn) @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 //?新實(shí)現(xiàn) @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. 編譯選項(xiàng)優(yōu)化
-
Strip Link Product設(shè)成YES,WeChatWatch可執(zhí)行文件減少0.3M
-
Make Strings Read-Only設(shè)為YES,也許是因?yàn)槲⑿殴こ虖牡桶姹綳code升級(jí)過來,這個(gè)編譯選項(xiàng)之前一直為NO,設(shè)為YES后可執(zhí)行文件減少了3M
-
去掉異常支持,Enable C++ Exceptions和Enable Objective-C Exceptions設(shè)為NO,并且Other C Flags添加-fno-exceptions,可執(zhí)行文件減少了27M,其中__gcc_except_tab段減少了17.3M,__text減少了9.7M,效果特別明顯。可以對(duì)某些文件單獨(dú)支持異常,編譯選項(xiàng)加上-fexceptions即可。但有個(gè)問題,假如ABC三個(gè)文件,AC文件支持了異常,B不支持,如果C拋了異常,在模擬器下A還是能捕獲異常不至于Crash,但真機(jī)下捕獲不了(有知道原因可以在下面留言:)。去掉異常后,Appstore后續(xù)幾個(gè)版本Crash率沒有明顯上升。個(gè)人認(rèn)為關(guān)鍵路徑支持異常處理就好,像啟動(dòng)時(shí)NSCoder讀取setting配置文件得要支持捕獲異常,等等
6. 其他可探索途徑
-
iOS8 Embed-Framework:提取WeChatWatch、ShareExtention和微信主工程的公共代碼,可執(zhí)行文件可以減少5M+,不過這特性需要最低版本iOS8才能用,iOS7設(shè)備啟動(dòng)會(huì)crash
-
iOS9 App Thinning:嚴(yán)格來說App Thinning不會(huì)讓安裝包變小,但用戶安裝應(yīng)用時(shí),蘋果會(huì)根據(jù)用戶的機(jī)型自動(dòng)選擇合適的資源和對(duì)應(yīng)CPU架構(gòu)的二進(jìn)制執(zhí)行文件(也就是說用戶本地可執(zhí)行文件不會(huì)同時(shí)存在armv7和arm64),安裝后空間占用更小
7. 建立監(jiān)控
通過對(duì)LinkMap文件的分析,可以得知每個(gè)模塊可執(zhí)行文件占用大小。再對(duì)比兩個(gè)版本,就知道業(yè)務(wù)模塊的增量大小。參考如下:
總結(jié)
以上是生活随笔為你收集整理的iOS微信安装包瘦身的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 51单片机50个实例代码_【附代码】51
- 下一篇: Mac 微信防撤回免认证登录多开插件:T