概述
本文會從圖片和代碼兩個維度,來進(jìn)行包瘦身實踐。
圖片層面,可以優(yōu)化的點包括:
- 壓縮圖片
- 修改圖片格式
- 刪除無用圖片
- 刪除重復(fù)圖片
代碼層面,介紹查找并刪除 Objective-C 或 Swift 代碼的方法。
1 圖片優(yōu)化
1-1 壓縮圖片
圖片壓縮可分為有損壓損和無損壓損。
- 有損壓縮一般是以犧牲圖片的質(zhì)量為代價來進(jìn)行的。
- 無損壓縮則通過去除圖片中的無用數(shù)據(jù)
比如在 png 格式中,有兩種類型的數(shù)據(jù)塊:
- 必要的
關(guān)鍵數(shù)據(jù)塊 - 非必要的
輔助數(shù)據(jù)塊(相機信息、內(nèi)嵌縮略圖)
png 的無損壓縮,就是通過去除非必要的輔助數(shù)據(jù)塊來實現(xiàn)的。
這里推薦兩種壓縮方式:
tinypng
tinypng 屬于有損壓縮,壓縮率最高能達(dá)到 70% 以上。提供的是網(wǎng)站壓縮服務(wù)。
ImageOptim
ImageOptim 同時提供無損壓縮和有損選項,可以最大程度保證圖片的原始清晰度和細(xì)節(jié)。提供的是 Mac 軟件。
1-2 更改圖片格式(ing)
除了傳統(tǒng)使用的 png 圖片格式,我們還可以考慮選擇 WebP。
WebP 是一款由 Google 開源的圖片格式,其主要優(yōu)勢是壓縮率高。在無損壓縮模式下,大小比 png 格式少 26%。有損模式模式下,比 JPEG 圖片小 25-34%。
通過工具 cwebp 可以將其它格式的圖片轉(zhuǎn)為 WebP 格式。
// 通用寫法
cwebp [options] input_file -o output_file.webp
// 有損壓縮
cwebp -lossless original.png -o new.webp
// 無損壓縮
cwebp -lossless original.png -o new.webp
關(guān)于 WebP 在 iOS 中的應(yīng)用,我自己建了一個直接可編譯的 demo 工程,供參考,GitHub 鏈接。
如果想要對 WebP 有更多了解,請參看移動端圖片格式調(diào)研。
1-3 刪除無用圖片
這里推薦使用 GitHub 上的開源庫 LSUnusedResources,查找代碼工程中未使用的資源(包括圖片、mp3、mp4)。
下載下來的是一個 Objective-C 的 Mac 工程。運行代碼后會出現(xiàn)下面界面:

允許設(shè)置的參數(shù)包括:
- Project Path:工程目錄的路徑
- Exclude Folder:設(shè)置工程目錄的查找黑名單(比如 Pods 文件夾下內(nèi)容)
- Resource Suffix:想要查找的資源后綴
- 正則表達(dá)式設(shè)置:比如在 Objective-C 語言的 .m 文件中查找 @".?",Swift 文件里的 ".?"
- Ignore similar name:是否忽略名字類似的圖片,勾選后,即代表在代碼中出現(xiàn) tag_%d,tag_1.png 即被認(rèn)為使用過
其源代碼核心代碼如下:
// 將工程文件夾下,符合該后綴要求的文件全部找出來,并存在一個字典中
NSDictionary *dic = [[ResourceFileSearcher sharedObject] startWithProjectPath:projectPath excludeFolders:excludeFolders resourceSuffixs:resourceSuffixs];
// 查找源代碼文件中的所有字符串,并存儲到一個集合中
NSSet *set = [[ResourceStringSearcher sharedObject] startWithProjectPath:projectPath excludeFolders:excludeFolders resourceSuffixs:resourceSuffixs resourcePatterns:[self resourcePatterns]];
NSMutableArray *unusedResult = [NSMutableArray array];
for (NSString *name in resNames) {
// 如果集合 set 中,則說明該資源文件未被使用
if (![set containsObject:name]) {
[unusedResult append:dic[name]];
}
}
// unusedResult 即為所有未使用的資源文件信息
1-4 刪除重復(fù)圖片
重復(fù)圖片的查找問題,這里我們轉(zhuǎn)換為比對不同圖片 MD5 值的問題。
如果兩張圖片數(shù)據(jù)的 MD5 值相同,可以判定為相同圖片。
具體代碼如下:
import os
import sys
import hashlib
# 調(diào)用方式
# python repeat_image.py 目錄路徑
def find_repeat_image(sourceDir):
result = []
# 遍歷文件夾
for dirpath, _, filenames in os.walk(sourceDir):
md5list = {}
for filename in filenames:
path = os.path.join(dirpath, filename)
# 判斷是否是目錄
if os.path.isdir(path):
continue
# init md5
md5obj = hashlib.md5()
# open rb是讀取二進(jìn)制文件
fd = open(path, 'rb')
buff = fd.read()
md5obj.update(buff)
fd.close()
# 獲取小寫 md5 字符串
filemd5 = str(md5obj.hexdigest()).lower()
# 檢查該 md5 是否已經(jīng)存在
if filemd5 in md5list:
md5list[filemd5].add(path)
else:
md5list[filemd5] = set([path])
for key in md5list:
list = md5list[key]
# 超過 1,則說明有存在重復(fù)
if len(list) > 1:
result.append(list)
return result
# 調(diào)用方式
arr = find_repeat_image(sys.argv[1])
if len(arr) == 0:
print("無重復(fù)圖片")
else:
for repeat in arr:
print("-----------重復(fù)圖片有------------")
for item in repeat:
print(item)
2 代碼優(yōu)化
2-1 LinkMap & Mach-O
注:該方法只適用于 Objective-C
大致思路是從 LinkMap 獲取工程文件中所有類、方法信息,從 Mach-O 找到所有使用過的類、方法,兩者的差值即為未使用的代碼。
LinkMap
在 Xcode - Build Settings 中設(shè)置 Write Link Map File 為 YES,Path to Link Map File 設(shè)置為需要輸出的 txt 文件。
這里說一個小技巧,在 $(SRCROOT) 可以代表當(dāng)前工程的根目錄。
具體位置如下圖:

LinkMap 包含三部分:
- Object File 包含了.o 目標(biāo)文件和庫文件
- Section 包含了代碼段和數(shù)據(jù)段在 Mach-O 文件的偏移位置以及大小
- Symbols 包含了所有的方法、類、block
我們想要找的方法和類就在 Symbols 中。
Mach-O
Mach-O 文件是 Xcode 編譯成功后的產(chǎn)物,使用 Mach-OView 查看信息。

-
__objc_selrefs中列出了所有調(diào)用過的方法 -
__objc_classrefs中列出所有使用過的的類 -
__objc_superrefs中列出所有使用的父類
缺陷
- 無法找到 performSelector 方法調(diào)用的方法
- 需要人工進(jìn)行比對、查找,工作量比較大
2-2 運行時檢查類是否被使用過
注:該方法只適用于 Objective-C
思路:
我們知道在 objc 中類的 initialize 方法執(zhí)行時機是在首次向該類發(fā)送消息時。那么是不是就意味著在 objc 源碼會記錄是否初始化呢?
答案是有的,在 objc 源碼中,可以找到以下代碼:
#define RW_INITIALIZED (1<<29)
struct objc_class : objc_object {
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
}
因此在運行時盡可能跑完 App 所有場景后,如果某個類對象的 isInitialized 屬性仍然返回 NO,則可以判定該類沒有被使用到。
接下來遺留的問題是,objc 源碼的實現(xiàn)細(xì)節(jié)是對開發(fā)者屏蔽的,在代碼工程中如何訪問到 isInitialized 屬性呢?
解決辦法是參照 objc 源碼,創(chuàng)建對應(yīng)的數(shù)據(jù)結(jié)構(gòu)。然后進(jìn)行強轉(zhuǎn)訪問。
// 比如在源碼中有一個 objc_class,就仿照創(chuàng)建一個 zyy_objc_class
struct zyy_objc_class : zyy_objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
public:
class_rw_t* data() {
return bits.data();
}
zyy_objc_class* metaClass() {
return (zyy_objc_class *)((long long)isa & ISA_MASK);
}
};
檢查方法:
+ (BOOL)isObjInitialize {
zyy_objc_class *cls = (__bridge zyy_objc_class *)([UsedCodeClass class]);
class_rw_t *metaClassData = cls->metaClass()->data();
bool isInitialized = (metaClassData->flags) & RW_INITIALIZED;
return isInitialized;
}
2-3 查找 Swift 中未使用方法和類
推薦一個三方庫 periphery 可以查找 Swift 中未使用的類和方法。
不過要注意的是,因為 OC 的動態(tài)性,所以當(dāng) Swift 代碼暴露給 OC 時便被認(rèn)定為已使用的代碼。
periphery 是基于 SourceKit 實現(xiàn)的,做 Swift 開發(fā)的應(yīng)該也會熟悉另一款基于 SourceKit 實現(xiàn)的 Swiftlint。
SourceKit 提供的功能包括:源代碼轉(zhuǎn)換、語法高亮、排版、代碼自動補全、Swift OC 之間頭文件生成等。