你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS微信安裝包瘦身

iOS微信安裝包瘦身

編輯:IOS開發基礎

1436446531174.jpg

前提

微信經過多次版本迭代,產生不少冗余代碼和無用資源。之前微信也沒有很好的手段知道哪個模塊增量多少。另外去年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文件了:

blob.png

每個LinkMap由3個部分組成,以微信為例:

1. Object files:

[ 0] linker synthesized

[ 1] /xxxx/WCPayInfoItem.o

[ 2] /xxxx/GameCenterFriendRankCell.o

[ 3] /xxxx/WloginTlv_0x168.o

...

第一部分列舉可執行文件裡所有.obj文件,以及每個文件的編號。

2. Sections:

blob.png

第二部分是可執行文件的段表,描述各個段在可執行文件中的偏移位置和大小。第一列是段的偏移量,第二列是段占用大小,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的數據類型

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挺影響性能的);等等

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。

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文件的分析,可以得知每個模塊可執行文件占用大小。再對比兩個版本,就知道業務模塊的增量大小。參考如下:

blob.png

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved