iOS APP包分析工具

架構師修行手冊發表於2023-11-24

介紹

分享一款用於分析iOSipa包的指令碼工具,使用此工具可以自動掃描發現可修復的包體積問題,同時可以生成包體積資料用於檢視。這塊工具我們團隊內部已經使用很長一段時間,希望可以幫助到更多的開發同學更加效率的最佳化包體積問題。

工具下載地址

背景

APPAnalyze工具最早誕生主要是為了解決以下包體積管理的問題:

對於定位下沉市場的APP來講,包體積是一個非常重要的效能指標,包體積過大會影響使用者下載APP的意願。但是在早期我們缺少一些手段幫助我們更高效的去進行包體積管理。

自動發現問題

  • 提升效率- 人工排查問題效率低,對於常見的問題儘可能自動掃描出來。並且對於元件化工程來講,很多外部元件是透過Framework方式提供,沒有倉庫原始碼許可權用於分析包體積問題。

  • 流程化- 形成自動化的質量流程,新增到CI流水線自動發現包體積問題。

資料指標量化

  • 包體積問題- 提供資料化平臺檢視每個元件的包體積待修復問題

  • 包體積大小- 提供資料化平臺檢視每個元件的包體積佔比,包括總大小,單個檔案二進位制大小和每個資源大小。可以針對不同的APP版本進行元件化粒度的包體積資料對比,更方便檢視每個版本的元件大小增量。

實現方式

我們選擇了不依賴原始碼而是直接掃描二進位制庫的方式來實現這個能力,總體的執行流程一下:
執行流程

提示:基於元件化工程的掃描方式內部支援,只是暫時不對外開放。

使用指南

安裝

無需安裝。透過下載連結直接下載終端可執行命令檔案APPAnalyzeCommand到本地即可使用。

APPAnalyzeCommand 下載地址

使用

$ /Users/Test/APPAnalyzeCommand --help
OPTIONS:
  --version <version>     當前版本 1.0.0
  --output <output>       輸出檔案目錄。必傳引數
  --config <config>       配置JSON檔案地址。非必傳引數
  --ipa <ipa>             ipa.app檔案地址。必傳引數
  -h, --help              Show help information.





執行

開啟終端程式直接執行以下shell指令,即可生成ipa的包體積資料以及包體積待修復問題。

提示:不能直接使用AppStore的包,AppStore的包需要砸殼。建議儘量使用XCodeDebug的包。

/Users/Test/APPAnalyzeCommand --ipa ipas/JDAPP/JDAPP.app --output ipas/JDAPP





提示:如果提示permission denied沒有許可權,執行sudo chmod -R 777 /Users/a/Desktop/ipas/APPAnalyzeCommand即可。

生成產物

生成產物
指令執行完成以後,會在ouput引數指定的資料夾生成APPAnalyze資料夾。具體檔案介紹如下:

包體積資訊

  • app_size.html- 展示ipa每個framework的包體積資料,可直接用瀏覽器開啟。

提示:按照主程式和動態庫進行粒度劃分

app_size.html

  • framework_size.html- 展示單個framework所有的包體積資料,二級頁面不要直接開啟

framework_size.html

提示:XCode生成Assets.car時會將一些小圖片拼接成一張PackedAssetImage的大圖片。

  • package_size.json-ipa包體積 JSON 資料

包體積待修復問題

  • app_issues.html- 展示ipa每個framework的包體積待修復問題數量,可直接用瀏覽器開啟。

提示:按照主程式和動態庫進行粒度劃分
app_issues.html

  • framework_issues.html- 展示單個framework所有的待修復問題詳細資料,二級頁面不要單獨開啟

framework_issues.html

  • issues.json-ipa待修復包體積問題 JSON 資料

提示:json資料可用於搭建自己的資料平臺,擴充套件更多的能力。例如檢視不同APP版本以及支援多個APP版本對比等。

規則介紹

規則

包體積

未使用的類

定義了類沒有被使用到,包含ObjC類和Swift類。

掃描規則

  • 沒有查到到對應的ObjC類被引用

  • 沒有被當做父類使用

  • 沒有使用的字串和類名一致

  • 沒有被當做屬性型別使用

  • 沒有被建立或呼叫方法

  • 沒有實現+load方法

可選的修復方式

  • 移除未使用的類

  • Swift類如果只是用了static方法考慮修改成Enum型別

  • 如果只是在型別轉換時使用了也會檢測出是未使用的類,例如(ABCClass *)object;。建議檢查是否真的有沒有到相關類後刪除

  • 對於ObjC,如果只是作為方法引數型別使用也會被檢測出是未使用的類。建議刪除相關方法即可。

提示:刪除類相對是一種安全的行為,因為刪除後如果有被使用到會產生編譯時錯誤。雖然有做字串呼叫的掃描過濾,不過還是建議檢查是否可能被Runtime動態建立呼叫

未使用的ObjC協議

定義了ObjC協議沒有被類使用

掃描規則

  • 對應的協議沒有被類引用

可選的修復方式

  • 移除未使用的協議

Bundle內多Scale圖片

Bundle內同一張圖片包含多個Scale會導致更大的包體積。

掃描規則

  • 同一個Bundle記憶體在同名但是scale不同的圖片。例如a@2x.png/a@3x.png

可選的修復方式

  • 移除Scale更低的圖片

大資源

檔案大小超過一定大小的即為大資源,預設為20KB

掃描規則

  • 某個檔案超過設定的大資源限額

可選的修復方式

  • 移除資源動態下發

  • 使用更小的資料格式,例如使用更小的圖片格式

重複的資原始檔

存在多個同樣的重複檔案。

掃描規則

  • 多個檔案MD5一致即判定為重複檔案。

可選的修復方式

  • 移除多餘的檔案

未使用的類Property屬性

ObjC類中定義的屬性沒有被使用到。

掃描規則

  • 對應的屬性沒有被呼叫 set/get 方法,同時也沒有被_的方式使用

  • 不是來自實現協議的屬性

  • 不是來自Category的屬性

  • 不存在字串使用和屬性名一致

可選的修復方式

  • 移除對應的屬性

  • 如果是介面協議的屬性,需要新增類實現此介面

注意事項

  • 可能存在部分動態使用的場景,需要進行一定的檢查。例如一些繼承NSObject的資料模型類,可能存在屬性沒有被直接使用到,但是可能會被傳喚成JSON作為引數的情況。例如後臺下發的資料模型

未使用的ImageSet/DataSet

包含的Imageset/DataSet並沒有被使用到。

掃描規則

  • 未檢測到和Imageset同樣名字的字串使用

可選的修復方式

  • 移除ImageSet/DataSet

注意事項

  • 某些Swift程式碼中使用的字串不能被發現所以會被當做未使用。

  • 使用字串拼接的名字作為imageset的名字。

  • 被合成到PackedAssetImage裡的Imageset不能被掃描出來

未使用的ObjC方法

定義的ObjCCategory 方法並未被使用到。

掃描規則

  • 不存在和此方法一樣的方法名使用

  • 不存在使用的字串和方法名一致

  • 不是來自父類或Category的方法

  • 不是來自實現介面的方法

  • 不是屬性 set/get 方法

可選的修復方式

  • 移除對應方法

未使用的分類方法

定義的ObjCCategory 方法並未被使用到。

掃描規則

  • 不存在和此方法一樣的方法名使用

  • 不存在和方法名一致的字串使用

  • 不是來自父類或Category的方法

  • 不是來自實現介面的方法

可選的修復方式

  • 移除未使用的方法

  • 如果是介面協議的方法,需要新增類實現此介面

未使用的資原始檔

包含的檔案資源並沒有被使用到。這裡的資源不包含Imageset/DataSet

掃描規則

  • 未檢測到和檔名同樣名字的字串使用

可選的修復方式

  • 移除資源

注意事項

  • 某些Swift程式碼中使用的字串不能被發現所以會被當做未使用

  • 使用字串拼接的名字作為資源的名字

安全

動態反射呼叫ObjC類

存在類名和字串一致,可能使用NSClassFromString()方法動態呼叫類。當字串或類名變更時無法利用編譯時檢查發現問題,可能會導致功能異常。

掃描規則

  • 存在使用的字串NSObject子類類名相同

可選的修復方式

  • 使用NSStringFromClass()獲取類名字串

  • 使用Framework外部的類應該使用方法封裝,除了少部分功能不應該使用反射去呼叫

提示:包含繼承NSObject的 swift 類。

ObjC屬性記憶體申明錯誤

一些特殊的NSObject型別的屬性記憶體型別申明錯誤,可能會導致功能異常或觸發Crash

掃描規則

  • NSArray/NSSet/NSDictionary型別的屬性使用strong申明

  • NSMutableArray/NSMutableSet/NSMutableDictionary型別的屬性使用copy申明

可選的修復方式

  • 修改strong/copy申明

衝突的分類方法

ObjC同一個類的多個Category分類中存在多個相同的方法,由於執行時最終會載入方法可能是不確定的,可能會導致功能異常等未知的行為。

掃描規則

  • NSObject類的多個Category分類中存在多個相同的方法

修復方式

  • 移除多餘的分類方法

重複的分類方法

ObjC原始類和類的Category分類中有相同的方法,分類中的方法會覆蓋原始類的方法,可能會導致功能異常等未知的行為。

掃描規則

  • NSObject原始類和類的Category分類中有相同的方法

修復方式

  • 移除重複的分類方法

未實現的ObjC協議方法

類實現了某個ObjC協議,但是沒有實現協議的非可選方法。可能會導致功能異常或觸發Crash

掃描規則

  • 分類未實現NSObject協議的非可選方法

可選的修復方式

  • 對應的類實現缺失的非可選協議方法

  • 將對應的協議方法標識為optional可選方法

重複的ObjC類

多個動態庫靜態庫之間存在同樣的。不會導致編譯失敗,但是執行時只會使用其中一個類,可能會導致功能異常或觸發Crash。同時會增加包體積

掃描規則

  • 多個動態庫靜態庫之間存在同樣的NSObject類符號

可能的修復方式

  • 移除重複的類

效能

使用動態庫

使用動態庫會增加啟動耗時。

掃描規則

  • Macho為動態庫

可選的修復方式

  • 使用靜態庫

  • 使用Mergeable Library

實現+load方法的類

APP啟動後會執行所有+load方法,減少+load方法可以降低啟動耗時。

掃描規則

  • 實現+load方法的NSObject

可選的修復方式

  • 移除+load方法

  • 使用+initialize替代

自定義配置

重要配置

systemFrameworkPaths

可以基於自身專案進行系統庫目錄的配置,解析工程時也會對系統庫進行解析。配置系統庫目錄對於未使用方法的查詢可以提供更多的資訊避免誤報。但是配置更多會導致執行的更慢,建議至少配置Foundation/UIKit

unusedObjCProperty-enable

unusedObjCProperty規則預設不開啟。

  • 開啟未使用屬性檢查以後,會掃描macho__TEXT段,會增加分析的耗時。

unusedClass-swiftEnable

unusedClass-swiftEnable預設不開啟。

  • 開啟Swift類檢查以後,會掃描macho__TEXT段,會增加分析的耗時。

  • 未使用Swift類的專案建議不要開啟,如果考慮執行效能的話Swift使用相對比較多的再開啟。

提示:掃描macho__TEXT段需要使用XCodeRun編譯出的包,不能直接使用用於上架APP Store構建出的包。主要是Debug會包含更多的資訊用於掃描。

配置屬性

/Users/Test/APPAnalyzeCommand -ipa /Users/Desktop/ipas/APPMobile/APPMobile.app -config /Users/Desktop/ipas/config.json --output /Users/Desktop/ipas/APPMobile





可基於自身專案需要,新增下列規則可配置引數。在使用APPAnalyzeCommand指令時新增--config配置檔案地址。

{
    "systemFrameworkPaths": ["/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation",
        "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation"
    ], // 配置系統庫。會極大增加未使用方法的誤報
    "rules": {
        "dynamicCallObjCClass": { // 動態調`ObjC類
            "enable": false, // 是否啟用
            "excludeClasslist": [ // 過濾類名
                "NSObject",
                "param"
            ]
        },
        "incorrectObjCPropertyDefine": { // 錯誤的 ObjC 屬性定義
            "enable": false // 是否啟動
        },
        "largeResource": { // 大資源
            "maxSize": 20480 // 配置大資源判定大小。預設 20480Byte=20KB
        },
        "unusedObjCProperty": { // 未使用的 ObjC 屬性
          "enable": false, // 是否啟用。預設不開啟
          "excludeTypes": ["NSString", "NSArray", "NSDictionary", "NSNumber", "NSMutableArray", "NSMutableDictionary", "NSSet"] // 過濾掉部分型別的屬性
        },
        "unusedClass": { // 未使用的類
            "swiftEnable": false, // 是否支援 Swift 類。預設不支援
            "excludeSuperClasslist": ["JDProtocolHandler", "JDProtocolScheme"],// 如果類繼承了某些類就過濾
            "excludeProtocols": ["RCTBridgeModule"], // 如果類實現了某些協議就過濾
            "excludeClassRegex": ["^jd.*Module$", "^PodsDummy_", "^pg.*Module$", "^SF.*Module$"] // 過濾掉名字元合正規表示式的類
        },
        "unusedObjCMethod": { // 未使用的 ObjC 方法
            "excludeInstanceMethods": [""], // 過濾掉某些名字的物件方法
            "excludeClassMethods": [""], // 過濾掉某些名字的類方法
            "excludeInstanceMethodRegex": ["^jumpHandle_"], // 過濾掉名字元合正規表示式的物件方法
            "excludeClassMethodRegex": ["^routerHandle_"], // 過濾掉名字元合正規表示式的類方法
            "excludeProtocols": ["RCTBridgeModule"] // 如果類整合了某些協議就不再檢查,例如 RN 方法
        },
        "loadObjCClass": { //  呼叫 ObjC + load 方法
            "excludeSuperClasslist": ["ProtocolHandler"], // 如果類繼承了某些類就過濾
            "excludeProtocols": ["RCTBridgeModule"] // 如果類實現了某些協議就過濾,例如 RN 方法
        },
        "unusedImageset": { // 未使用 imageset
            "excludeNameRegex": [""] // 過濾掉名字元合正規表示式的imageset
        },
        "unusedResource": { // 未使用資源
            "excludeNameRegex": [""] // 過濾掉名字元合正規表示式的資源
        }
    }
}






元件化工程掃描

可以基於APPAnalyzeCore.framework定製實現自己的元件化工程掃描,或者新增基於自身元件化工程的檢查規則。詳情可以看Demo

基於元件化掃描方式有以下優勢:

  • 細化資料粒度- 可以細化每個模組的包體積和包體積問題,更容易進行包體積最佳化。

  • 更多的檢查- 例如檢查不同元件同一個Bundle包含同名的檔案,不同元件包含同一個category方法的的實現。

  • 檢查結果更準確- 例如ObjC未使用方法的檢查,只要存在一個和方法名同樣的呼叫就表示方法有被使用到。但是整個ipa中可能存在很多一樣的方法名但是隻有一個方法有真正被呼叫到,如果細分到元件的粒度就可以發現更多問題。

提示:只有APP主工程無程式碼,全部透過子元件以framework的形式匯入二進位制庫的方式的工程才適合這種模式。

其他

掃描質量如何

這套工具我們團隊內部開發加逐步完善有一年的時間了。基於此工具修改了幾十個元件的包體積問題,同時不斷的修復誤報問題。目前現有提供的這些規則檢查誤報率是很低的,只有極少數幾個規則可能存在誤報的可能性,總體掃描質量還是很高的。

和社群開源的工具有什麼差異

我們在早期調研了社群的幾個同型別的開源工具,主要存在以下幾個問題:

  • 擴充套件性不夠- 無法支援專案更好的擴充套件定製能力,例如新增掃描規則、支援不同型別掃描方式、生成更多的報告型別。

  • 功能不全- 只提供部分能力,例如只提供未使用資源或者未使用類

  • 無法生成包體積資料- 無法生成包體積完整的資料。

  • 檢查質量不高- 掃描發現的錯誤資料多,或者有一些問題不能被發現。

開源計劃

後續一定會開源。主要是希望調整一些內部結構再開源,開源後就不方便調整。順便修復一些常見的問題。

開源帶來的好處

開源帶來的好處是,部分工程可以基於自身的業務需要,擴充套件定製自己的掃描工具。同時也可以將一些更好的想法實現新增進來。

  • 擴充套件解析方式- 目前只支援ipa模式掃描,很快會開放支援project元件化工程的掃描方式。基於元件化工程的掃描可以更加準確,但是不同的公司元件化工程的構建方式可能是不一樣的,有需要可以在上層定製自身元件化工程的掃描解析。

  • 擴充套件掃描規則- 雖然現在已經新增了比較多的通用性的規則,同時提供了一定的靈活性配置能力。但是不同的專案可能需要定製一些其他的規則,這些規則沒辦法透過在現有規則上新增配置能力實現。

  • 擴充套件資料生成- 預設包裡只包含兩種資料生成,包體積資料還有包體積待修復問題資料。可以擴充套件更多的資料生成格式,例如我們自身的專案就有新增基於元件的依賴樹格式。

後續規劃

元件化工程支援

新增更多用於元件化工程的掃描

對於 Swift 更好的支援

對於Swift語言只要開啟XCode編譯最佳化以後就能在生成產物的時候支援無用程式碼的移除,包括未使用型別未使用方法的自動移除,但是依然有部分場景不會進行最佳化。所以這一塊也是後續完善的重點:

  • 未使用屬性- 編譯器不會對於未使用屬性進行移除,包括classstruct的屬性。

  • 未使用方法- 對於class的方法,編譯器並不會進行移除,即使沒有申明[@objc](https://my.oschina.net/TnhqVdFXL8vnu)進行訊息派發。

相關連結

作者:京東零售 何驍

來源:京東雲開發者社群 轉載請註明來源

相關文章