起底 iOS 包瘦身

概述

本文會從圖片和代碼兩個維度,來進(jìn)行包瘦身實踐。

圖片層面,可以優(yōu)化的點包括:

  1. 壓縮圖片
  2. 修改圖片格式
  3. 刪除無用圖片
  4. 刪除重復(fù)圖片

代碼層面,介紹查找并刪除 Objective-C 或 Swift 代碼的方法。

1 圖片優(yōu)化

1-1 壓縮圖片

圖片壓縮可分為有損壓損和無損壓損。

  1. 有損壓縮一般是以犧牲圖片的質(zhì)量為代價來進(jìn)行的。
  2. 無損壓縮則通過去除圖片中的無用數(shù)據(jù)

比如在 png 格式中,有兩種類型的數(shù)據(jù)塊:

  1. 必要的關(guān)鍵數(shù)據(jù)塊
  2. 非必要的輔助數(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)下面界面:

LSUnusedResources 界面

允許設(shè)置的參數(shù)包括:

  1. Project Path:工程目錄的路徑
  2. Exclude Folder:設(shè)置工程目錄的查找黑名單(比如 Pods 文件夾下內(nèi)容)
  3. Resource Suffix:想要查找的資源后綴
  4. 正則表達(dá)式設(shè)置:比如在 Objective-C 語言的 .m 文件中查找 @".?",Swift 文件里的 ".?"
  5. 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 包含三部分:

  1. Object File 包含了.o 目標(biāo)文件和庫文件
  2. Section 包含了代碼段和數(shù)據(jù)段在 Mach-O 文件的偏移位置以及大小
  3. Symbols 包含了所有的方法、類、block

我們想要找的方法和類就在 Symbols 中。

Mach-O

Mach-O 文件是 Xcode 編譯成功后的產(chǎn)物,使用 Mach-OView 查看信息。

  1. __objc_selrefs 中列出了所有調(diào)用過的方法
  2. __objc_classrefs 中列出所有使用過的的類
  3. __objc_superrefs 中列出所有使用的父類

缺陷

  1. 無法找到 performSelector 方法調(diào)用的方法
  2. 需要人工進(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 之間頭文件生成等。

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

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