iOS 底層 - 性能優化之啟動和電池能耗

本文源自本人的學習記錄整理與理解,其中參考閱讀了部分優秀的博客和書籍,盡量以通俗簡單的語句轉述。引用到的地方如有遺漏或未能一一列舉原文出處還望見諒與指出,另文章內容如有不妥之處還望指教,萬分感謝 !

寫在前面:

Mach-O文件簡介

  • Mach object的縮寫,是Mac、iOS上用于存儲程序、庫的標準格式 ; Mach-O文件是一種叫法,就像以 .text 結尾的文件,被叫做為text文件

常見的Mach-O文件有:

  • MH_OBJECT:目標文件(.o)、靜態庫文件(.a) 靜態庫其實就是N個.o合并在一起

  • MH_EXECUTE:可執行文件 .app/xx

  • MH_DYLIB:動態庫文件 .dylib.framework/xx

  • MH_DYLINKER:動態鏈接編輯器 /usr/lib/dyld

  • MH_DSYM:存儲著二進制文件符號信息的文件
    .dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩潰信息)

dyld簡介

在iOS系統中,幾乎所有的程序都會用到動態庫,而動態庫在加載的時候都需要用dyld(位于/usr/lib/dyld)程序進行鏈接。很多系統庫幾乎都是每個程序都要用到的,與其在每個程序運行的時候一個一個將這些動態庫都加載進來,還不如先把它們打包好,一次加載進來來的快。

  • dynamic link editor的縮寫, Apple的 動態鏈接器,macOS和iOS通用; 動態庫不能直接運行 ,需要通過系統的動態鏈接加載器進行加載到內存后執行;主要用來裝載Mach-O文件; 比如:動態庫 和 可執行文件。

  • 動態鏈接器在系統中以一個用戶態的可執行文件形式存在, 一般應用程序會在Mach-O文件部分指定一個LC_LOAD_DYLINKER的加載命令,此加載命令指定了dyld的路徑,通常它的默認值是“/usr/lib/dyld”

  • 共享緩存:從iOS 3.1開始,為了提高系統的性能,所有的系統庫文件都被打包保存到了一個很大的緩存文件當中;所有默認的動態鏈接庫被合并成一個大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構分開存放

  • dyld加載時,為了優化程序啟動,啟用了共享緩存(shared cache)技術。共享緩存會在進程啟動時被dyld映射到內存中

  • 每當任何Mach-O鏡像加載時,dyld首先會檢查該Mach-O鏡像與所需的動態庫是否在共享緩存中,如果存在,則直接將它在共享內存中的內存地址映射到進程的內存地址空間。在程序依賴的系統動態庫很多的情況下,這種做法對程序啟動性能是有明顯提升的。

可執行文件:

  • 平時編寫的代碼最終會被編譯成為一個Mach-O格式的文件
  • 開發過程中所用到的動態庫(比如:UIKit、Foundation) 依賴信息也會存儲在可執行文件中

動態庫:

  • 程序運行時由系統動態加載到內存,而不是復制,供程序調用。
  • 系統只加載一次,多個程序共用,節省內存。因此,編譯內容更小,而且因為動態庫是需要時才被引用,所以更快。
    簡單認識:系統的UIKit框架最終被dyld以動態庫的形式加載到內存 !

系統使用動態鏈接有幾點好處:

代碼共用:很多程序都動態鏈接了這些 lib,但它們在內存和磁盤中中只有一份。 易于維護:由于被依賴的 lib 是程序執行時才鏈接的,所以這些 lib 很容易做更新,比如libSystem.dyliblibSystem.B.dylib 的替身,哪天想升級直接換成libSystem.C.dylib 然后再替換替身就行了。 減少可執行文件體積:相比靜態鏈接,動態鏈接在編譯時不需要打進去,所以可執行文件的體積要小很多。

動態鏈接優點.jpg

如上圖所示,不同進程之間共用系統dylib的_TEXT區,但是各自維護對應的_DATA區。

所有動態鏈接庫和我們App中的靜態庫.a和所有類文件編譯后的.o文件最終都是由dyld,Apple的動態鏈接器來加載到內存中。每個image都是由一個叫做ImageLoader的類來負責加載(一一對應)

1. 應用啟動

APP啟動時間長短,直接會影響用戶對APP的第一體驗;如果啟動時間過長,不但會影響體驗導致用戶直接奧利給,同時可能會觸發蘋果的 watch dog機制 kill 掉APP;這就很尷尬了,掉粉啊這 ! APP啟動卡死接著直接崩潰了。。。。。。

  • Xcode在debug模式下默認不開啟 watch dog,所以有條件還是走一走真機測試

APP的啟動可以分為2種

  • 冷啟動:Cold Launch , 從零開始啟動APP
  • 熱啟動:Warm Launch , APP已經在內存中,在后臺存活著,再次點擊圖標啟動APP

在衡量APP的啟動時間之前先了解下,APP的啟動流程:

APP的啟動流程.png

Mach-O文件加載

Mach-O文件結構.png

mach-o文件有如下幾個部分組成:

  • Header:保存了一些基本信息,包括了該文件運行的平臺、文件類型、LoadCommands的個數等等。

  • LoadCommands:可以理解為加載命令,在加載Mach-O文件時會使用這里的數據來確定內存的分布以及相關的加載命令。比如我們的main函數的加載地址,程序所需的dyld的文件路徑,以及相關依賴庫的文件路徑。

  • Data: 這里包含了具體的代碼、數據等等。

APP的啟動從用戶態來說可以分為三個階段,即 dyld加載依賴庫、runtime初始化main函數 總結如下

Launch time = dyld + runtime + main()

當然也有部分同學將其分為兩個階段:main()之前、main()之后;這都沒有問題,只是概括粒度的差別而已;也可以直接將其分為:內核態、用戶態

APP加載過程:

  1. 系統會開啟一個進程,然后讀取可執行文件(Mach-O文件),從里面獲得dyld的路徑
  2. 加載dyld,dyld先初始化運行環境,開啟緩存策略,加載(遞歸)程序相關依賴庫(其中也包含我們的可執行文件)到內存中,并生成相應的鏡像

C++靜態對象初始化構造器initializer

  1. 對依賴庫進行鏈接 -->link(),調用每個依賴庫的初始化方法Initalizer,在這一步,runtime被初始化---->
    (libSystem.dylib庫libdispatch_init里調用了runtime的初始化方法_objc_init);
    runtime初始化后不會閑著,在_objc_init注冊了幾個通知,從dyld這里接手了幾個活,其中包括:
    初始化相應依賴庫里的類結構
    調用依賴庫里所有的load方法。

  2. 當所有依賴庫的初始化后,輪到最后一位(程序可執行文件)進行初始化,在這時runtime會對項目中 所有類進行類結構初始化然后調用所有的load方法。

  • 調用map_images(鏡像)進行可執行文件內容的解析和處理
  • 在load_images中調用call_load_methods,調用所有Class(包括分類)的+load方法
  • 進行各種objc結構的初始化(注冊Objc類、初始化類對象)
  1. dyld返回main函數地址,這時進程進入就緒狀態;main函數被調用后進程進入執行狀態,至此便來到了熟悉的程序入口。
    這些事情大多數在 dyld:_main 方法中被發生;

main():調用UIKit庫中的UIApplicationMain()找到應用的委托方法執行開發者自定義的任務,比如:獲取主控制器顯示到UIWindow

  1. Xcode提供的主函數調用UIKit的UIApplicationMain函數

  2. UIApplicationMain函數創建UIApplication對象和你的 AppDelegate。

  3. UIKit從主故事板nib文件 加載應用程序的默認入口。

  4. UIKit調用AppDelegate的: willFinishLaunchingWithOptions:方法。

  5. UIKit執行狀態恢復,它調用你的AppDelegateUIWindow的附加方法。

  6. UIKit調用AppDelegate的: didFinishLaunchingWithOptions:方法。

  7. 初始化完成后,系統使用場景委托或應用程序委托來顯示UI并管理應用程序的生命周期。

總結:

APP的啟動由dyld主導,將可執行文件加載到內存,順便加載所有依賴的動態庫
并由runtime負責加載完成OC類結構初始化
所有初始化工作結束后,dyld就會調用main函數
接下來就是UIApplicationMain函數,AppDelegateapplication:didFinishLaunchingWithOptions:方法

補充:

dyld是蘋果操作系統一個重要組成部分,而且令人興奮的是,它是開源的,任何人可以通過蘋果官網下載它的源碼來閱讀理解它的運作方式(下載地址:Source Browser),了解系統加載動態庫的細節。

dyld詳細流程:

XNU加載程序可執行文件后,通過分析文件來獲得dyld所在路徑來加載dyld,同時也把當前主程序的Mach-O頭部信息給了dyld;有了頭部信息,加載器就可以從頭開始,遍歷整個Mach-O文件的信息,獲取LoadCommands(加載命令)、Data(數據、代碼);有了這些就正式開始搭建初始化程序環境 ! ! ! !

  • 加載dyld-->__dyld_start()-->dyldbootstrap::start()-->_main()函數; dyld的加載動態庫的代碼就是從_main()開始

共九個步驟如下:

第一步: 設置運行環境,處理環境變量

第二步:初始化主程序

  • 核心API -- instantiateFromLoadedImage()
  • 將實例化好的主程序添加到全局主列表sAllImages中,最后調用addMappedRange()申請內存,更新主程序映像映射的內存區。做完這些工作,第二步初始化主程序就算完成了。

第三步:加載共享緩存

  • 主要執行mapSharedCache()來完成映射共享緩存
  • 進行動態庫的版本化重載,這主要通過函數checkVersionedPaths()完成

第四步:加載插入的動態庫

  • 循環遍歷DYLD_INSERT_LIBRARIES環境變量中指定的動態庫列表,并調用loadInsertedDylib()將其加載

第五步:鏈接主程序

  • 執行link()完成主程序的鏈接操作,該函數調用了ImageLoader自身的link()函數;主要目的是將實例化的主程序的動態數據進行修正,達到讓進程可用的目的,其中典型的就是主程序中的符號表修正操作 rebase和bind
    rebase修復的是指向當前鏡像內部的資源指針。
    bind修復的是指向鏡像外部的資源指針。

由于ASLR(address space layout randomization)的存在,可執行文件和動態鏈接庫在虛擬內存中的加載地址每次啟動都不固定,所以需要這兩步來修復鏡像中的資源指針,來指向正確的地址。

rebase步驟先進行,需要把鏡像讀入內存,并以page為單位進行加密驗證,保證不會被篡改,所以這一步的瓶頸在IO。bind在其后進行,由于要查詢符號表,來指向跨鏡像的資源,加上在rebase階段,鏡像已被讀入和加密驗證,所以這一步的瓶頸在于CPU計算

備注:recursiveBind()完成遞歸綁定符號表的操作。此處的符號表針對的是非延遲加載的符號表,它的核心是調用了doBind(),在ImageLoaderMachOCompressed中,該函數讀取映像動態鏈接信息的bind_offbind_size來確定需要綁定的數據偏移與大小,然后挨個對它們進行綁定,綁定操作具體使用bindAt()函數,它主要通過調用resolve()解析完符號表后,調用bindLocation()完成最終的綁定操作,需要綁定的符號信息有三種:

BIND_TYPE_POINTER:需要綁定的是一個指針。直接將計算好的新值嶼值即可。
BIND_TYPE_TEXT_ABSOLUTE32:一個32位的值。取計算的值的低32位賦值過去。
BIND_TYPE_TEXT_PCREL32:重定位符號。需要使用新值減掉需要修正的地址值來計算出重定位值。

第六步:鏈接插入的動態庫

  • 鏈接插入的動態庫與鏈接主程序一樣,都是使用的link(),插入的動態庫列表是前面調用addImage()保存到sAllImages中的,之后,循環獲取每一個動態庫的ImageLoader,調用link()對其進行鏈接,注意:sAllImages中保存的第一項是主程序的映像。

第七步:執行弱符號綁定

  • 調用weakBind()函數執行弱符號綁定。
    首先通過調用contextgetCoalescedImages()sAllImages中所有含有弱符號的映像合并成一個列表,合并完后調用initializeCoalIterator()對映像進行排序,排序完成后調用incrementCoalIterator()收集需要進行綁定的弱符號
    incrementCoalIterator ()是一個虛函數,在ImageLoaderMachOCompressed中,該函數讀取映像動態鏈接信息的weak_bind_offweak_bind_size來確定弱符號的數據偏移與大小,然后挨個計算它們的地址信息。之后調用getAddressCoalIterator(),按照映像的加載順序在導出表中查找符號的地址,找到后調用updateUsesCoalIterator()執行最終的綁定操作,執行綁定的是bindLocation()

第八步:執行初始化方法

  • 執行初始化的方法入口是initializeMainExecutable(),主要實現了doImageInit()doModInitFunctions()執行映像模塊中設置為init的函數靜態初始化方法

第九步:查找程序入口函數并返回

  • 這一步調用主程序映像的getThreadPC()函數來查找主程序的LC_MAIN加載命令獲取程序的入口點,沒找到就調用getMain()LC_UNIXTHREAD加載命令中去找,找到后就跳轉到入口點指定的地址返回 !
到這里,dyld整個加載動態庫的過程就算完成了。

2. 啟動時間優化主要針對冷啟動

查看main()函數執行之前的耗時:

Xcode提供通過添加環境變量來打印APP啟動時間分析

  • Edit scheme --> Run -->Arguments-->Environment Variables 添加 DYLD_PRINT_STATISTICS 設置為 1
  • 如果需要更加詳細的時間信息,添加 DYLD_PRINT_STATISTICS_DETAILS 設置為 1
  • 以上環境變量本質上插入到 dyld全局的環境變量sEnv中,
添加環境變量.png

輸出結果示例

Total pre-main time:  36.22 milliseconds (100.0%)
         dylib loading time:  14.43 milliseconds (42.1%)
        rebase/binding time:   1.82 milliseconds (5.3%)
            ObjC setup time:   3.89 milliseconds (11.3%)
           initializer time:  13.99 milliseconds (40.9%)
           slowest intializers :
             libSystem.B.dylib :   3.20 milliseconds (6.4%)
   libBacktraceRecording.dylib :   3.90 milliseconds (8.4%)
    libMainThreadChecker.dylib :   6.55 milliseconds (19.1%)
-------------------------   分析    ------------------------
pre: previous  在…以前
在執行main函數之前所用的時間:36.22毫秒
                                    動態庫加載:14.43 毫秒
                                    rebase綁定:1.82毫秒
                                ObjC結構準備:3.89毫秒
                                             初始化:13.99毫秒
比較慢的加載:
 libSystem.B.dylib :  3.20 毫秒
 libBacktraceRecording.dylib :   3.90 毫秒
 libMainThreadChecker.dylib :  6.55 毫秒

優化方案:

dyld優化:

  • 減少動態庫、合并一些動態庫(定期清理不必要的動態庫)
  • 減少Objc類分類的數量、減少Selector數量(定期清理不必要的類、分類)
  • 減少C++虛函數數量,一旦虛函數會需要多維護一張虛表
    -Swift盡量使用struct
  • 使用AppCode工具檢查未被使用的文件

虛函數

C++中的虛函數的作用主要是實現了多態的機制。關于多態,簡而言之就是用父類指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。這種技術可以讓父類的指針有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的代碼來實現可變的算法。比如:模板技術,RTTI技術,虛函數技術,要么是試圖做到在編譯時決議,要么試圖做到運行時決議。

  • 和oc的多態不同,雖然也可以認為父類擁有多種形態,但OC只支持單繼承;父類指針可以指向子類示例,但是不能調用沒有在父類中聲明的方法;只能調用子類重寫的方法

runtime

  • +initialize方法和dispatch_once取代所有的__attribute__((constructor))C++靜態構造器、Objc的load

main

  • 在不影響用戶體驗的前提下,盡可能將一些操作延遲,不要全部都放在finishLaunching方法中
  • 按需加載,用到的時候再加載

3. 電池能耗

手機電池電量是極其有限的,沒有電的手機就像一塊沒有實際作用的模型;一個APP如果對電池消耗影響很大就可能會被用戶奧利給 !

一般開發中對電池消耗比較大的幾個方面:

  1. CPU處理,Processing; 高頻的處理會加快電量的消耗

  2. 網絡,Networking, 長連接發送接收數據,手機需要持續的保持信號接收和發送對手機電量的消耗會比較大;比如:微信、QQ等APP

  3. 定位,Location; 持續定位不斷的獲取GPS信息,和刷新對電量消耗也會很快 比如:高德導航、百度地圖等軟件使用起來電量就消耗很快

  4. 圖像,Graphics ; GPU的圖像渲染是會占用大量的資源,同時也很耗電

可以通過對一下方面做出相應的優化措施:

  • 盡可能降低CPU、GPU的功耗,即CPU、GPU的優化
  • 少用定時器, 定時器會持續循環的做事情,這樣的操作會對電量消耗較快
  • 優化 I/O (文件讀寫)操作
  1. 盡量不要頻繁寫入小數據,可以把小數據整理在空閑時一次性寫入;在不影響結果的情況下

  2. 讀寫大量重要數據時,考慮用dispatch_io,其提供了基于GCD的異步操作文件I/O的API, 用dispatch_io系統會優化對磁盤訪問;

  3. 數據量比較多的情況,不建議直接存儲在文件里;可以考慮是用數據庫,比如SQLite、CoreData; 因為數據庫對數據讀寫都是有優化過的,有相應的算法,會比直接讀寫更有優勢的多

  4. 網絡優化

    • 減少、壓縮網絡數據,比如網絡傳輸早期用XML:體積比較大 ,后來使用JSON: 體積就比較小;現在也有人在用protocl buffer這種格式傳輸,但前提是服務器也使用相同的格式接收
    • 上傳圖片,文件等數據先進行壓縮,或者分片上傳
    • 如果多次請求的結果相同,盡量使用緩存
    • 斷點續傳,100MB的文件,下載了一半,突然關機了;下次下載的時候可以從50MB開始繼續下載,不需要從頭開始
    • 如果網絡狀態變為不可用或者是未知網絡時,不要嘗試頻繁執行網絡請求;
    • 讓用戶可以取消長時間運行或者網速很慢的網絡操作;并設置合適的超時時間
    • 批量傳輸,比如 下載視頻流時,不要傳輸很小的數據包,直接下載整個文件或者一大塊一大塊地下載。 如果下載廣告,一次性多下載一些,然后再慢慢展示。如果下載電子郵件,一次下載多封,不要一封一封下載
  5. 定位優化

    • 如果只需要快速確定用戶位置,最好用CoreLocation框架中CLLocationManagerrequestLocation方法獲取位置信息;
      此方法優勢:定位完成后,會自動讓定位硬件斷電
    • 如果不是導航應用,盡量不要實時獲取定位;定位完畢就關掉定位服務
    • 盡量降低定位精度,比如盡量不要使用精度較高的kCLLocationAccuracyBest
    • 需要用到后臺定位時,盡量設置pausesLocationUpdatesAutomaticallyYES :用來停止當用戶靜止時位置的自動更新;
  6. 硬件檢測優化

    • 用戶移動、搖晃、傾斜設備是,會產生動作(motion)事件; 這些事件由加速度計、陀螺儀、磁力計等硬件檢測;在不需要檢測的場景,應該及時關閉這些的使用

今日頭條iOS客戶端啟動速度優化
iOS 底層 - 性能優化之CPU、GPU
iOS 底層 - 性能優化之安裝包瘦身(App Thinning)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容