long time no see,最近在總結一些平(應)常(付)用(面)到(試)的知識點,今天就跟大家聊了聊App體積優化這個事兒。 1.為什麼要做體積瘦身 別問!問就是為了應付面試。 哈哈,開個玩笑。大家生活中都會遇到一個場景,在某個需要緊急打開App的時候,發現使用的App半天打不開!WTF!而 ...
long time no see,最近在總結一些平(應)常(付)用(面)到(試)的知識點,今天就跟大家聊了聊App體積優化這個事兒。
1.為什麼要做體積瘦身
別問!問就是為了應付面試。
哈哈,開個玩笑。大家生活中都會遇到一個場景,在某個需要緊急打開App的時候,發現使用的App半天打不開!WTF!而另外一款相同功能的App卻可以瞬間打開。哪個App能夠輓留更多的用戶就不言而喻了吧!
借用某個游戲裡邊人物的一句話:"時間就是金錢,我的朋友!"
2.我們都能幹什麼?
下邊我們先查看一個的思維導圖:
思維導圖已經總結目前我已經知道的並且可以落地的優化方式。
如圖所示APP體積優化包括兩部分:資源瘦身和代碼瘦身。
下麵我將使用APPReduction這個簡單的demo實地操作一下。需要的同學可以到gayhub下載一下。
3. 具體實施
3.1 資源瘦身
-
刪除資源
因為曠日持久的業務代碼堆砌,工程內很可能會堆積許多無用的圖片,而這些圖片卻能實實在在的增加App的體積。而我們完全可以藉助工具LSUnusedResources進行資源文件的刪除工作。
在RedutionViewController
中,configImageTest
方法中你會找到image圖片的代碼調用
- (void)configImageTest{ [UIImage imageNamed:@"cooker"]; [UIImage imageNamed:@"driver"]; [UIImage imageNamed:@"function"]; [UIImage imageNamed:@"header"]; // [UIImage imageNamed:[@"think_"stringByAppendingFormat:@"005"]]; // [UIImage imageNamed:[@"think_"stringByAppendingFormat:@"006"]]; // [UIImage imageNamed:[@"think_"stringByAppendingFormat:@"007"]]; }
當我們使用LSUnusedResources
進行圖片資源檢察的時候會發現未使用的圖片。
但是這裡需要註意最好進行一下手動的檢察避免出現誤刪的情況,並且如果代碼僅僅是註釋掉,程式並不會認為資源是廢棄的。
-
壓縮資源
請以合理且合法的方式多跟UI多談談,在切圖片的時候請按需要切圖,不必要每一張都是高清無碼,在可接受的範圍內可以壓縮資源圖片!而且筆者認為所有跟圖片相關的改變質量的問題都需要經手UI,切記不要自己瞎搞。
-
大圖片資源
不經常用到的大圖資源可以採取下載的方式載入到APP上,不是非要打包到ipa裡邊!能跟產品聊聊的問題別死磕代碼
3.2 代碼瘦身
當我們的App被打包成ipa的時候,代碼會被打包成一個一個個的.o文件,而這些.o文件組成了MachO,而系統在編譯MachO文件的時候會生成一個附帶的文件LinkMap。
3.2.1 LinkMap
-
LinkMap的組成
LinkMap由Object File、Section、Symbol三部分組成,描述了工程所有代碼的信息。可以根據這些信息針對性的去優化空間。
-
LinkMap的獲取
1.在XCode中開啟編譯選項
Write Link Map File \nXCode -> Project -> Build Settings -> 把Write Link Map File
設置為YES2.在XCode中開啟編譯選項
Write Link Map File \nXCode -> Project -> Build Settings -> 把Path to Link Map File
的地方設置好地址3.運行項目在地址位置將生成.txt的文件
-
LinkMap的分析
1.藉助工具LinkMap解析工具,我們可以分析每個類占用的大小
2.針對性的進行代碼的體積的優化,比如三方庫占用空間巨量,有沒其他的替代方案。在取捨兩個相同庫的時候也可以根據體積的比重做出取捨。
看到這裡我們已經可以從巨集觀的角度上獲取到需要優化哪些部分的代碼,但是微觀角度哪些是無用的類哪些是無用的方法,需要我們進一步從MachO的層面上去分析。
3.2.2 MachO分析
MachO文件可以說是App編譯之後的最重要的部分,通過MachOView這個軟體我們可以更加直觀看到MachO的組成。如果你的MachOView運行的時候出現崩潰請按照這篇文章進行修改。
- MachO的組成
__objc_selrefs:記錄了幾乎所有被調用的方法
__objc_classrefs和__objc_superrefs:記錄了幾乎所有使用的類
__objc_classlist:工程里所有的類的地址
-
刪除無用的類
MachO文件中
__objc_classrefs
段記錄里了引用類的地址,__objc_classlist
段記錄了所有類的地址,我們可以認為取兩者的差值就可以獲得未使用類的地址,然後進行符號化,就可以取得未使用類的信息。大家可以使用classunref這個工具實現未使用類的查找。
如果對實現感興趣的同學可以拜讀大佬的文章iOS代碼瘦身實踐:刪除無用的類
-
刪除未使用的方法
當我們將無用的類刪除完畢之後,在已經使用的類裡邊很有可能依然會有未使用的方法。
前邊我們已經提到過LinkMap中保存了工程的信息,而我們所有已經被包含到項目中的方法可以通過LinkMap獲取。
在classunref的啟發下,筆者利用python實現了未使用方法的自動化方式
因為py實在不太熟悉加上筆者比較懶,請大家忽略一些語法、介面設計不規範等等的問題
1.使用指令
grep '[+|-]\[.*\s.*\]' xxx-linkMap.txt
指令我們得到所有被包含到工程到項目中的代碼// 獲取所有的方法 def method_readRealization_pointers(linkMapPath,path): # all method lines = os.popen("grep '[+|-]\[.*\s.*\]' %s" % linkMapPath).readlines() // 需要忽略的方法 lines = method_ignore(lines,path); pointers = set() for line in lines: line = line.split('-')[-1].split('+')[-1].replace("\n","") line = line.split(']')[0] line = str("%s]"%line) pointers.add(line) if len(pointers) == 0: exit('Finish:method_readRealization_pointers null') print("Get all method linkMap pointers...%d"% len(pointers)) return pointers
2.考慮到大家項目中使用了大量的三方庫,而三方庫的方法有許多並未使用,所以通過
method_ignore
方法進行忽略,這樣獲取的差值的集合中就不會包括三方庫的未使用方法def method_ignore(lines,path): print("Get method_ignore...") effective_symbols = set() // 獲取所有需要忽略類名的首碼(例如YYModel 會以首碼YY的方式作出忽略) prefixtul = tuple(class_allIgnore_Prefix(path,'','')) getPointer = set() // 此處是為了忽略Setter Getter方法 for line in lines: classLine = line.split('[')[-1].upper() methodLine = line.split(' ')[-1].upper() if methodLine.startswith('SET'): endLine = methodLine.replace("SET","").replace("]","").replace("\n","").replace(":","") print("methodLine:%s endLine:%s"%(methodLine.lower(),endLine.lower())) if len(endLine) != 0: getPointer.add(endLine) getPointer = list(set(getPointer)) getterTul = tuple(getPointer) for line in lines: classLine = line.split('[')[-1].upper() methodLine = line.split(' ')[-1].upper() if (classLine.startswith(prefixtul)or methodLine.startswith(prefixtul) or methodLine.startswith('SET') or methodLine.startswith(getterTul)): continue effective_symbols.add(line) if len(effective_symbols) == 0: exit('Finish:method_ignore null') return effective_symbols;
3.使用指令
otool -v -s __DATA__objc_selrefs
指令我們可以得到所有已經被實現的方法def method_selrefs_pointers(path): # all use methods lines = os.popen('/usr/bin/otool -v -s __DATA __objc_selrefs %s' % path).readlines() pointers = set() for line in lines: line = line.split('__TEXT:__objc_methname:')[-1].replace("\n","").replace("_block_invoke","") pointers.add(line) print("Get use method selrefs pointers...%d"% len(pointers)) return pointers
3.利用步驟1和步驟2的差值可以獲取到還未使用的方法。
def method_remove_Realization(selrefsPointers,readRealizationPointers): if len(selrefsPointers) == 0: return readRealizationPointers if len(readRealizationPointers) == 0: return null methodPointers = set() for readRealizationPointer in readRealizationPointers: newReadRealizationPointer = readRealizationPointer.split(' ')[-1].replace("]","") methodPointers.add(newReadRealizationPointer) unUsePointers = methodPointers - selrefsPointers; dict = {} for unUsePointer in unUsePointers: dict[unUsePointer] = unUsePointer for readRealizationPointer in readRealizationPointers: newReadRealizationPointer = readRealizationPointer.split(' ')[-1].replace("]","") if dict.has_key(newReadRealizationPointer): dict[newReadRealizationPointer] = readRealizationPointer str = dict[newReadRealizationPointer] return list(dict.values())
感興趣的同學可以在這裡下載到修改版的 ONLClassMethodUnref
3.2.3 AppCode
如果你的工程不夠巨大,藉助AppCode這個工具的靜態分析也可以查找到未使用的代碼。方法極為簡單打開AppCode->選擇Code->點擊Inspect Code---等待靜態分析
但是筆者還是在這裡需要作出提醒,即使你用到了上邊的所有方式,基於動態語言的特性,我們仍然不能夠找出所有未使用的代碼,並且在刪除代碼的時候仍然需要小心翼翼!切勿多刪!記得做回歸測試!
參考資料
2.MachOView運行的時候出現崩潰請按照這篇文章進行修改