iOS APP安裝包瘦身

參考公眾號(hào):WeMobileDev

APP開發(fā)中,總會(huì)想要去盡可能的優(yōu)化項(xiàng)目,這是我們作為程序員最基本的追求之一。

而安裝包瘦身,是最能讓用戶感知的一類優(yōu)化。大部分APP對(duì)用戶來(lái)說(shuō)是非必需品,但是如果安裝包高達(dá)50多M,會(huì)嚇退一大波的用戶。

而且,現(xiàn)在的APP追求小而精,小程序的火熱也減少了傳統(tǒng)APP的使用量,安裝包瘦身迫在眉睫。

AppStore安裝包由資源和可執(zhí)行文件兩部分組成,安裝包瘦身也是從這兩部分入手。

一、資源瘦身

資源瘦身主要是去除無(wú)用資源或者壓縮資源。
資源主要包括圖片、音頻、視頻、多語(yǔ)言包、配置文件等。

無(wú)用資源指的是,項(xiàng)目中沒(méi)有被引用的資源,找到的辦法就是,去項(xiàng)目中搜索該文件名,圖片資源去掉@2x,@3x,沒(méi)有搜索到的就是無(wú)用資源。
當(dāng)然,資源名在項(xiàng)目中另外拼接的,特殊處理,所以一個(gè)APP最好有一個(gè)統(tǒng)一的拼接格式。

壓縮資源,一般指圖片的壓縮,圖片資源控制在80k左右(@3x的全屏圖片);
還有就是配置文件的壓縮,比如內(nèi)置的離線資源等。

二、Xcode's Link Map File

在講可執(zhí)行文件瘦身之前先介紹Xcode的LinkMap文件。LinkMap文件是Xcode產(chǎn)生可執(zhí)行文件的同時(shí)生成的鏈接信息,用來(lái)描述可執(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)庫(kù)、每個(gè)功能模塊代碼占用大小。這里要注意的地方是,由于__DATA.__bbs是代表未初始化的靜態(tài)變量,Size表示應(yīng)用運(yùn)行時(shí)占用的堆大小,并不占用可執(zhí)行文件,所以計(jì)算obj占用大小時(shí),要排除這個(gè)段的Size。

三、可執(zhí)行文件瘦身

回到我們的可執(zhí)行文件瘦身問(wèn)題,LinkMap文件可以幫助我們尋找優(yōu)化點(diǎn)。

1. 查找無(wú)用selector

以往C++在鏈接時(shí),沒(méi)有被用到的類和方法是不會(huì)編進(jìn)可執(zhí)行文件里。但Objctive-C不同,由于它的動(dòng)態(tài)性,它可以通過(guò)類名和方法名獲取這個(gè)類和方法進(jìn)行調(diào)用,所以編譯器會(huì)把項(xiàng)目里所有OC源文件編進(jìn)可執(zhí)行文件里,哪怕該類和方法沒(méi)有被使用到。

結(jié)合LinkMap文件的__TEXT.__text,通過(guò)正則表達(dá)式([+|-][.+\s(.+)]),我們可以提取當(dāng)前可執(zhí)行文件里所有objc類方法和實(shí)例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可執(zhí)行文件里引用到的方法名(UsedSelectorsAll),我們可以大致分析出SelectorsAll里哪些方法是沒(méi)有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系統(tǒng)API的Protocol可能被列入無(wú)用方法名單里,如UITableViewDelegate的方法,我們只需要對(duì)這些Protocol里的方法加入白名單過(guò)濾即可。

另外第三方庫(kù)的無(wú)用selector也可以這樣掃出來(lái)的。

2. 查找無(wú)用oc類

查找無(wú)用oc類有兩種方式,一種是類似于查找無(wú)用資源,通過(guò)搜索"[ClassName alloc/new"、"*ClassName "、"[ClassName class]"等關(guān)鍵字在代碼里是否出現(xiàn)。另一種是通過(guò)otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段來(lái)獲取當(dāng)前所有oc類和被引用的oc類,兩個(gè)集合相減就是無(wú)用oc類。

3. 掃描重復(fù)代碼

可以利用第三方工具simian掃描。南非支付copy代碼就是這樣被發(fā)現(xiàn)的。但除此成果之外,掃描出來(lái)的結(jié)果過(guò)多,重構(gòu)起來(lái)也不方便,不如砍功能需求效果好。

4. protobuf精簡(jiǎn)改造

protobuf是Google推出的一種輕量高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式,在微信用于網(wǎng)絡(luò)協(xié)議和本地文件序列化。但google默認(rèn)工具生成的代碼比較冗余,像序列化、反序列化、計(jì)算序列化大小等方法都生成在具體的pb類里,每個(gè)類的實(shí)現(xiàn)大同小異。通過(guò)代碼分析以及結(jié)合protobuf原理,要想把這些方法抽象到基類,派生類提供每個(gè)字段相關(guān)信息就夠了:

  • field number

  • field label, optional, required or repeated

  • wire type, double, float, int, etc

  • 是否packed

  • repeated的數(shù)據(jù)類型

<pre>

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;

</pre>

另外通過(guò)無(wú)用selector列表,發(fā)現(xiàn)不少pb類屬性的getter或setter沒(méi)有被使用。原先的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:,防止通過(guò)KVO訪問(wèn)屬性Crash

  • 做下性能優(yōu)化,如pb類在initialize做一次初始化,緩存屬性名的hash值,屬性的getter、setter方法的objcType等;屬性值不用std::map(屬性名->屬性值),而是改用數(shù)組;MRC代替ARC(有些時(shí)候ARC自動(dòng)添加的retain/release挺影響性能的);等等

<pre style="">

`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`

</pre>

把冗余代碼去掉后,整個(gè)類清爽多了。像GameResourceReq只有3個(gè)屬性的proto結(jié)構(gòu)體,類方法代碼行數(shù)由以前的127行變成現(xiàn)在的8行。protobuf精簡(jiǎn)改造中,精簡(jiǎn)類方法減少了可執(zhí)行文件8.8M,去掉類成員變量和類屬性改用@dynamic減少了2.5M。

<pre style="">

message GameResourceReq { required BaseRequest BaseRequest = 1; required int32 PropsCount = 2; repeated uint32 PropsIdList = 3[packed=true]; }

</pre>

<pre style="">

`// 老實(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`

</pre>

<pre style="">

`// 新實(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`

</pre>

5. 編譯選項(xiàng)優(yōu)化

  • Strip Link Product設(shè)成YES,WeChatWatch可執(zhí)行文件減少0.3M

  • Make Strings Read-Only設(shè)為YES,也許是因?yàn)槲⑿殴こ虖牡桶姹綳code升級(jí)過(guò)來(lái),這個(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è)問(wèn)題,假如ABC三個(gè)文件,AC文件支持了異常,B不支持,如果C拋了異常,在模擬器下A還是能捕獲異常不至于Crash,但真機(jī)下捕獲不了(有知道原因可以在下面留言:)。去掉異常后,Appstore后續(xù)幾個(gè)版本Crash率沒(méi)有明顯上升。個(gè)人認(rèn)為關(guān)鍵路徑支持異常處理就好,像啟動(dòng)時(shí)NSCoder讀取setting配置文件得要支持捕獲異常,等等

6. 其他可探索途徑

  • iOS8 Embed-Framework:提取WeChatWatch、ShareExtention和微信主工程的公共代碼,可執(zhí)行文件可以減少5M+,不過(guò)這特性需要最低版本iOS8才能用,iOS7設(shè)備啟動(dòng)會(huì)crash

  • iOS9 App Thinning:嚴(yán)格來(lái)說(shuō)App Thinning不會(huì)讓安裝包變小,但用戶安裝應(yīng)用時(shí),蘋果會(huì)根據(jù)用戶的機(jī)型自動(dòng)選擇合適的資源和對(duì)應(yīng)CPU架構(gòu)的二進(jìn)制執(zhí)行文件(也就是說(shuō)用戶本地可執(zhí)行文件不會(huì)同時(shí)存在armv7和arm64),安裝后空間占用更小

7. 建立監(jiān)控

通過(guò)對(duì)LinkMap文件的分析,可以得知每個(gè)模塊可執(zhí)行文件占用大小。再對(duì)比兩個(gè)版本,就知道業(yè)務(wù)模塊的增量大小。參考如下:

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • App安裝包是由資源和可執(zhí)行文件兩部分組成,安裝包瘦身也是從這兩部分進(jìn)行。 資源瘦身 1. 刪除無(wú)用的資源 工具:...
    Vinecnt閱讀 18,567評(píng)論 13 84
  • App安裝包是由資源和可執(zhí)行文件兩部分組成,安裝包瘦身也是從這兩部分進(jìn)行。資源瘦身 刪除無(wú)用的資源工具:LSUnu...
    RobinYu閱讀 633評(píng)論 0 0
  • 安裝包主要由兩部分組成,資源文件以及可執(zhí)行文件,瘦身主要從這兩部分入手: 一、資源文件瘦身 1、刪除無(wú)用資源 現(xiàn)在...
    無(wú)邪8閱讀 2,087評(píng)論 0 14
  • 摘要:以下列出了安裝包瘦身的無(wú)腦執(zhí)行流程,其中“奇技淫巧”部分為選做題 資源優(yōu)化 刪除無(wú)用圖片 使用LSUnuse...
    暖夏未眠丶閱讀 857評(píng)論 0 1
  • 曾看到過(guò)一句話這樣寫道:“你只有這一生,但如果你以正確的方式生活,一輩子也就足夠?!?2018年的9月,我將度過(guò)第...
    云風(fēng)越閱讀 685評(píng)論 0 1

友情鏈接更多精彩內(nèi)容