相關(guān)概念
App 啟動分類
- 冷啟動
內(nèi)存中不包含App的相關(guān)數(shù)據(jù)本刽,必須從磁盤加載到內(nèi)存(即 App 被 kill 掉,重新打開啟動過程)是由系統(tǒng)所決定的
- 熱啟動
就是 App 已經(jīng)打開過屋群,但是按下 home 鍵進入后臺,這個時候 App 還存在一段時間鬼店。點擊 App踢涌,立馬就恢復(fù)到原狀態(tài)的過程即為熱啟動
啟動耗時統(tǒng)計
我們一般統(tǒng)計 App 的啟動耗時是從點擊 App 開始到首屏渲染完成的過程所消耗的時間。那么我們?nèi)ソy(tǒng)計耗時呢攘残?啟動檢測一般會以 main
函數(shù)作為一個分界點:main
函數(shù)之前稱為 pre_main
階段拙友,是由 DYLD
反饋應(yīng)用耗時;main
函數(shù)之后由開發(fā)者自己檢測(main
函數(shù)開始計時歼郭,到首屏渲染完成結(jié)束)
冷啟動優(yōu)化
pre_main(main 函數(shù)之前)
打開需要檢測耗時的項目遗契,Xcode -> Product -> Scheme -> Edit Scheme
彈出界面后,依次點擊 Run -> Arguments -> Environment Variables 點擊 +
添加一個變量名 DYLD_PRINT_STATISTICS
病曾,并將其值設(shè)置為 1
此時牍蜂,在項目的 main
函數(shù)處打個斷點,運行項目知态,就可以在控制臺上看到 pre_main
耗時時間捷兰,如下:
字段含義
- dylib loading time
動態(tài)庫加載所需要時間,這個過程會去加載 app 使用的動態(tài)庫负敏,而動態(tài)庫之間有它自己的依賴關(guān)系,所以會消耗時間去查找和讀取秘蛇。
- rebase/binding time
修正符號和綁定符號耗時其做。
Rebase:在鏡像(MachO文件)內(nèi)部調(diào)整指針的指向,針對mach-o在加載到內(nèi)存中不是固定的首地址(ASLR)這一現(xiàn)象做數(shù)據(jù)修正的過程赁还。
iOS4.3后引入了 ASLR 妖泄,MachO會被加載到隨機地址,這個隨機的地址跟代碼和數(shù)據(jù)指向的舊地址會有偏差艘策。dyld 需要修正這個偏差蹈胡,做法就是將 dylib 內(nèi)部的指針地址都加上這個偏移量。
binding:將指針指向鏡像(MachO文件)外部的內(nèi)容朋蔫,binding就是將這個二進制調(diào)用的外部符號進行綁定的過程罚渐。
- ObjC setup time
1、讀取二進制文件的 DATA 段內(nèi)容驯妄,找到與 objc 相關(guān)的信息
2荷并、注冊 Objc 類,ObjC Runtime 需要維護一張映射類名與類的全局表青扔。當加載一個 MachO 時源织,它定義的所有的類都需要被注冊到這個全局表中翩伪;
3、讀取 protocol 以及 category 的信息谈息,把category的定義插入方法列表 (category registration)
- initializer time:
1缘屹、Objc的+load()函數(shù)
2、C++的構(gòu)造函數(shù)屬性函數(shù) 形如attribute((constructor))
優(yōu)化方案
動態(tài)庫加載優(yōu)化
- 系統(tǒng)動態(tài)庫已做了高度優(yōu)化侠仇,所以從效率的角度來說轻姿,盡可能使用系統(tǒng)庫。
- 自定義的動態(tài)庫比較耗時傅瞻,官方建議盡量少的使用自定義的動態(tài)庫踢代,使用不要超過 6 個動態(tài)庫,多于 6 個的話建議合并嗅骄。
- 在性能上出發(fā)將動態(tài)庫編譯成靜態(tài)庫也會優(yōu)化這部分時間胳挎。
ObjC setup 優(yōu)化
- 不刻意的去減少幾個類,但是可以避免浪費溺森。
- 很多模塊和方法已經(jīng)被廢棄但是卻一直留存在項目中慕爬,可以刪除或保存在其他分支。
- 使用一些工具來查找項目中沒有被用到的文件屏积,從而達到優(yōu)化医窿。
initializer
- 將不必須在
+load
方法中做的事情延遲到+initialize
中。因為+load
方法是在 app 啟動的時候就被調(diào)用炊林,而+initialize
方法則是在類(Class)第一次使用的時候才調(diào)用姥卢,相當于是懶加載了≡郏可以把+load
中的代碼移到+initialize
中独榴,并結(jié)合dispatch_once
來防止重復(fù)調(diào)用。 - 如果可以奕枝,盡量減少 C++ 函數(shù)調(diào)用
main 函數(shù)之后
針對 main
函數(shù)之后的時間檢測就通過打點記錄棺榔。在 main()
、didFinishLaunchingWithOptions
以及第一個頁面的 viewDidAppear
中打點隘道,進行記錄症歇,從而來計算 main
函數(shù)之后的時間。
main 函數(shù)階段耗時
這里我們需要借助 Xcode
提供的 Instruments
工具了谭梗,打開項目忘晤,Xcode -> Open Developer Tool -> Instruments
- 進入 Instruments,選擇 Time Profiler 檢測工具默辨,雙擊打開
- 選擇需要測試的真機以及測試的 app德频,點擊下面的
Call Tree
,勾選Hide System Libraries
(隱藏系統(tǒng)庫)和Separate by Thread
(按線程分類)缩幸,這樣設(shè)置運行的時候就可以看到某個頁面某個函數(shù)運行的時間了
- 點擊左上角的運行按鈕壹置,就可以看到耗時操作了竞思。點擊展開可以查詢具體耗時時間,發(fā)現(xiàn)是16進制地址
- 針對上面16進制地址钞护,我們需要在
Xcode
中進行設(shè)置下盖喷,才能發(fā)現(xiàn)具體的函數(shù)運行耗時時間
- 查看裝在真機上的 app 是哪種模式
打開 Xcode -> Product -> Scheme -> Edit Scheme -> Run -> Info,當前的 Build Configuration
- 設(shè)置該模式下對應(yīng)的運行信息格式為
DWARF with dSYM File
- 設(shè)置完成后难咕,重新 Run 一次項目到手機上课梳,然后再進行1-4步驟操作,就可以發(fā)現(xiàn)發(fā)現(xiàn)具體的函數(shù)運行耗時時間了
- 在手機上點擊不同的頁面就可以查詢到每個頁面耗時時間余佃,可以根據(jù)自己的項目對耗時比較長的函數(shù)進行優(yōu)化了
main 函數(shù)之后耗時優(yōu)化可操作空間比較大暮刃,需要根據(jù)自己項目情況進行處理。
didFinishLaunchingWithOptions
可以將一些耗時又非必須立馬使用到功能代碼延后加載
熱啟動優(yōu)化
- 數(shù)據(jù)優(yōu)化,將耗時操作做異步處理.
- 檢查
NSUserDefaults
的存儲爆土,NSUserDefaults
實際上是在Library
文件夾下會生產(chǎn)一個plist
文件椭懊,加載的時候是整個plist
配置文件全部加載到內(nèi)存中。所以非常頻繁的存取大量數(shù)據(jù)也是有可能導(dǎo)致APP啟動卡頓的步势。
拓展-調(diào)試第三方應(yīng)用
首先氧猬,我們需要創(chuàng)建一個空工程,在真機上運行(利用這個工程的描述文件)
其次坏瘩,我們利用腳本文件來輔助盅抚,代碼如下
# ${SRCROOT} 是工程文件所在的目錄
TEMP_PATH="${SRCROOT}/Temp"
#資源文件夾,我們提前在工程目錄下新建一個 wechat_objc 文件夾倔矾,里面放需要調(diào)試的 ipa 包
ASSETS_PATH="${SRCROOT}/wechat_objc"
#目標 ipa 包路徑
TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"
#清空Temp文件夾
rm -rf "${SRCROOT}/Temp"
mkdir -p "${SRCROOT}/Temp"
#----------------------------------------
# 1. 解壓 IPA 到 Temp 下
unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
# 拿到解壓的臨時的 APP 的路徑
TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")
# echo "路徑是:$TEMP_APP_PATH"
#----------------------------------------
# 2. 將解壓出來的 .app 拷貝進入工程下
# BUILT_PRODUCTS_DIR 工程生成的 APP 包的路徑
# TARGET_NAME target名稱
TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
echo "app路徑:$TARGET_APP_PATH"
rm -rf "$TARGET_APP_PATH"
mkdir -p "$TARGET_APP_PATH"
cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"
#----------------------------------------
# 3. 刪除 extension 和 WatchAPP. 個人證書沒法簽名 Extention
rm -rf "$TARGET_APP_PATH/PlugIns"
rm -rf "$TARGET_APP_PATH/Watch"
#----------------------------------------
# 4. 更新 info.plist 文件 CFBundleIdentifier
# 設(shè)置:"Set : KEY Value" "目標文件路徑"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"
#----------------------------------------
# 5. 給 MachO 文件上執(zhí)行權(quán)限
# 拿到 MachO 文件的路徑
APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
#上可執(zhí)行權(quán)限
chmod +x "$TARGET_APP_PATH/$APP_BINARY"
#----------------------------------------
# 6. 重簽名第三方 FrameWorks
TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
then
for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
do
#簽名
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
done
fi
#注入
#yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/HankHook.framework/HankHook"
以上代碼可以寫成一個 .sh 的腳本文件或者直接將代碼粘貼到調(diào)試工程中(每一步都有相應(yīng)的解釋妄均,根據(jù)個人需求更改)
示例
新建一個空工程 ObjC_Test
,在工程根目錄下新建文件夾 wechat_objc
哪自,并將脫殼的 iPA 包放進去
修改上述代碼存放第三方 ipa
的路徑
將上述代碼生成 appSign.sh
腳本文件并放到工程目錄下
給工程添加腳本
配置腳本(./
表示工程根目錄丛晦,即加載根目錄的 appSign.sh
腳本)
運行應(yīng)用,此時就可以將三方的應(yīng)用運行到真機了提陶。下面是打印第三方(wechat)的啟動時間