<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          脈脈iOS如何啟動(dòng)秒開(kāi)

          共 12344字,需瀏覽 25分鐘

           ·

          2021-08-13 16:25



          李揚(yáng),脈脈客戶端高級(jí)開(kāi)發(fā)工程師。2018年加入脈脈,目前作為脈脈平臺(tái)組iOS開(kāi)發(fā),負(fù)責(zé)移動(dòng)端平臺(tái)開(kāi)發(fā),平臺(tái)類(lèi)基礎(chǔ)設(shè)施建設(shè)、維護(hù)、性能調(diào)優(yōu)和新技術(shù)探索。

          https://www.zhihu.com/column/p/396550853


          前言


          啟動(dòng)是 App 給用戶的第一印象,啟動(dòng)越慢,用戶流失的概率就越高,良好的啟動(dòng)速度是用戶體驗(yàn)不可缺少的一環(huán)。

          通過(guò)調(diào)研業(yè)內(nèi)現(xiàn)有的啟動(dòng)優(yōu)化方案,針對(duì)啟動(dòng)各個(gè)階段,結(jié)合脈脈自身app的情況,總結(jié)出了具體的可行性建議和可優(yōu)化的項(xiàng)目。

          加上后期不斷的調(diào)優(yōu)和實(shí)踐,最終在app啟動(dòng)過(guò)程涉及到現(xiàn)有復(fù)雜業(yè)務(wù)環(huán)境下,實(shí)現(xiàn)了900ms的秒開(kāi)成績(jī)。

          防劣化,建立健全app啟動(dòng)監(jiān)控體系。通過(guò)監(jiān)控大盤(pán),及時(shí)發(fā)現(xiàn)問(wèn)題解決問(wèn)題并總結(jié)經(jīng)驗(yàn) 。


          二、認(rèn)識(shí) App是如何啟動(dòng)的


          啟動(dòng)過(guò)程

          啟動(dòng)過(guò)程以main為界限,分為pre-main和main之后兩部分

          pre-main

          加載dyld

          動(dòng)態(tài)庫(kù)載入過(guò)程,會(huì)去裝載app使用的動(dòng)態(tài)庫(kù)。而每一個(gè)動(dòng)態(tài)庫(kù)有它自己的依賴(lài)關(guān)系,會(huì)消耗時(shí)間去查找和讀取。

          rebase&binding

          rebase:主要是調(diào)整鏡像內(nèi)部的指針,這里使用了ASLR(Address Space Layout Randomization 地址空間布局隨機(jī)化)。程序每次啟動(dòng)后地址都會(huì)隨機(jī)變化,這樣程序里的所有代碼地址都需要重新進(jìn)行計(jì)算修復(fù)

          binding:修復(fù)指向外部的指針。比如app中調(diào)用了NSLog函數(shù)打印信息,NSLog是系統(tǒng)函數(shù),在程序開(kāi)始運(yùn)行的時(shí)候app是不知道NSLog函數(shù)指針是多少,此時(shí)就需要通過(guò)dyld_stub_binder技術(shù)找到NSLog指針地址進(jìn)行調(diào)用。

          Objc setup

          runtime在此處初始化,對(duì)class和category進(jìn)行注冊(cè),selector唯一性判斷

          load&constructor&initialize

          調(diào)用所有類(lèi)的load的方法,初始化C&C++的靜態(tài)化變量,然后調(diào)用 constructor 函數(shù)

          main之后

          main函數(shù)

          創(chuàng)建整個(gè)app的autoreleasepool,初始化初始window,app界面開(kāi)始展示

          LifeCyle

          指定rootviewcontroller,調(diào)用業(yè)務(wù)代碼,完成各階段業(yè)務(wù)

          First Frame

          main頁(yè)面viewDidAppear 完成頁(yè)面第一幀渲染。至此啟動(dòng)完成。



          三、衡量 App啟動(dòng)時(shí)間


          打點(diǎn)系統(tǒng)監(jiān)控

          上圖具體指明了目前能夠做到的打點(diǎn)的地方,可以簡(jiǎn)化為下圖形式:

          進(jìn)程創(chuàng)建

          通過(guò) sysctl 系統(tǒng)調(diào)用拿到進(jìn)程創(chuàng)建的時(shí)間戳

          #import <sys/sysctl.h> 
          #import <mach/mach.h>
          + (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
          {
          int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
          size_t size = sizeof(*procInfo);
          return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
          }

          + (NSTimeInterval)processStartTime
          {
          struct kinfo_proc kProcInfo;
          if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo])
          {
          return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
          }
          else
          {
          return 0;
          }
          }

          最早的 +load

          和上面的分階段監(jiān)控一樣,通過(guò) AAA 為前綴命名 Pod,讓 +load 第一個(gè)被執(zhí)行

          didFinishLaunching

          此處監(jiān)控可以使用第三方SDK,也可以手動(dòng)加入打點(diǎn)來(lái)衡量

          另外,對(duì)于pre-main階段,Apple提供了一種測(cè)量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環(huán)境變量 DYLD_PRINT_STATISTICS 設(shè)為1 。之后控制臺(tái)會(huì)輸出類(lèi)似內(nèi)容,我們可以清晰的看到每個(gè)耗時(shí):

          如果將 Edit scheme -> Run > Auguments 將環(huán)境變量 DYLD_PRINT_STATISTICS_DETAILS 設(shè)為1,則可以更多詳細(xì)的pre-main階段的耗時(shí):

          工具

          TimeProfiler

          Time Profiler是Xcode自帶的時(shí)間性能分析工具,正常Time Profiler會(huì)1ms采樣一次,默認(rèn)只采集所有在運(yùn)行線程的調(diào)用棧,最后以統(tǒng)計(jì)學(xué)的方式匯總。通過(guò)統(tǒng)計(jì)比較時(shí)間間隔之間的堆棧狀態(tài),來(lái)推算某個(gè)方法執(zhí)行了多久,并獲得一個(gè)近似值。Time Profiler的使用方法網(wǎng)上有很多使用教程,這里我們也不過(guò)多介紹,附上一篇使用文檔:Instruments Tutorial with Swift: Getting Started。

          取從開(kāi)始啟動(dòng)后的2s內(nèi)的時(shí)間為啟動(dòng)樣本

          通過(guò)展開(kāi) Main Thread 經(jīng)過(guò)分析發(fā)現(xiàn),耗時(shí)方法在一個(gè)內(nèi)存泄露檢測(cè)模塊,修改之后如下圖:

          當(dāng)然其他線程也有在做并發(fā)的處理,也要注意線程個(gè)數(shù)的控制

          System Trace

          System Trace一直作為Instruments中一個(gè)默默無(wú)聞的功能出現(xiàn),模板提供了系統(tǒng)行為的全面信息。它顯示線程的調(diào)度、系統(tǒng)線程的轉(zhuǎn)化和內(nèi)存使用情況。這個(gè)模板可以使用在OS X或iOS中。簡(jiǎn)單點(diǎn)說(shuō)就是記錄一個(gè)App運(yùn)行過(guò)程中所有底層系統(tǒng)線程、內(nèi)存的調(diào)度使用過(guò)程的工具。

          脈脈iOS分析案例

          現(xiàn)象

          脈脈iOS在蜂窩網(wǎng)絡(luò)數(shù)據(jù)狀態(tài)下Debug,每次啟動(dòng),相比WiFi狀態(tài)下的啟動(dòng)時(shí)間都要長(zhǎng)了2s左右

          診斷過(guò)程

          首先選中 System Load 選取時(shí)間線,到第一次出現(xiàn) 脈脈 線程的點(diǎn),從這個(gè)時(shí)間點(diǎn)開(kāi)始脈脈app正式啟動(dòng),開(kāi)始處理app內(nèi)部邏輯。也就是main之后的階段。

          統(tǒng)計(jì)活躍的高優(yōu)線程數(shù)量和CPU核心數(shù)對(duì)比,如果高于核心數(shù)量會(huì)顯示成黃色,小于等于核心數(shù)量會(huì)是綠色。這個(gè)工具是用來(lái)幫助調(diào)試線程的優(yōu)先級(jí)的。

          系統(tǒng)維護(hù)了 5 個(gè)不同的線程優(yōu)先級(jí)/QoS: background,utility,default,user-initiated,user-interactive。

          切換到主線程 Main Thread 看到一大塊block的灰色狀態(tài),選取發(fā)現(xiàn)2.01s的卡頓,難道這是巧合?大膽猜測(cè),這就是我們要找的卡頓2s

          不要移動(dòng)時(shí)間線,切換到 Events:Thread States,可以看到線程切換的每一個(gè)事件,和狀態(tài)切換的發(fā)生的原因,觀察這個(gè)事件的下一個(gè)事件,因?yàn)橄聜€(gè)事件通常是鎖被釋放,線程重新進(jìn)入可執(zhí)行的狀態(tài)。發(fā)現(xiàn)卡頓的下一步執(zhí)行時(shí) CPU0 上的 0x90998 的線程,換句話說(shuō)是 0x90998 的線程使得主線程結(jié)束卡頓重新進(jìn)入了可執(zhí)行的狀態(tài)。

          切換到 0x90998 線程,選中主線程block狀態(tài)釋放開(kāi)始執(zhí)行的時(shí)間點(diǎn),可以看到 parked waiting for new woirk for dispatch,在往上追溯基本都是這句話的重復(fù)調(diào)用,可以大膽猜測(cè)基本上是在等待一個(gè)信號(hào)量。

          再次來(lái)到 Main Thread Block位置,雙擊右側(cè)的堆棧棧頂那句話,可以看到具體的代碼位置。

          的確是在等待一個(gè)信號(hào)量,并且是 RCT_DEV 環(huán)境下才會(huì)生效,線上不受影響。通過(guò)Debug發(fā)現(xiàn),這里請(qǐng)求的URL是 http://localhost:8081。

          如果我們把這段代碼注釋?zhuān)琖iFi和5G流量下,啟動(dòng)速度相差無(wú)幾。之前的猜測(cè)卡主2s是在等待信號(hào)的想法得到印證。

          這也說(shuō)明,有時(shí)就是要 敢猜敢想敢做!

          通過(guò)SystemTrace工具 Debug 蜂窩數(shù)據(jù)流量環(huán)境下應(yīng)用啟動(dòng)會(huì)卡主2s的問(wèn)題終于查明!

          手動(dòng)下點(diǎn)分析

          通過(guò)插樁代碼,我們發(fā)現(xiàn)使用 OpenUDID 獲取udid的時(shí)候耗時(shí)有時(shí)竟然達(dá)到400ms。分析后發(fā)現(xiàn),讀取 UIPasteboard 非常耗時(shí),根據(jù)調(diào)研 UIPasteboard 使用場(chǎng)景在脈脈中基本可以忽略,故考慮去掉 UIPasteboard 讀取邏輯。

          但在 Time Profiler 里檢測(cè),OpenUDID 獲取udid在所在的子線程,只消耗了 7ms。依靠 Time Profiler分析也有一定的局限性。



          四、制定 App啟動(dòng)優(yōu)化方案


          整體思路


          • 刪掉啟動(dòng)項(xiàng),把不需要的過(guò)時(shí)的直接刪除

          • 如果不能刪除,嘗試延遲,延遲包括第一次訪問(wèn)以及啟動(dòng)結(jié)束后找個(gè)合適的時(shí)間加載

          • 不能延遲的可以嘗試并發(fā),利用好多核多線程。但也要注意控制好線程的數(shù)量和優(yōu)先級(jí)

          • 如果并發(fā)也不行,可以嘗試讓代碼執(zhí)行更快。比如,頻繁訪問(wèn)的可以只獲取一次就存下來(lái)

          pre-main

          動(dòng)態(tài)庫(kù)

          防止劣化,需要嚴(yán)格管控動(dòng)態(tài)庫(kù)的引入

          減少動(dòng)態(tài)庫(kù)

          Apple官方建議盡量少的使用自定義的動(dòng)態(tài)庫(kù),或者考慮合并多個(gè)動(dòng)態(tài)庫(kù),其中一個(gè)建議是當(dāng)大于6個(gè)的時(shí)候,則需要考慮合并它們。

          自有動(dòng)態(tài)庫(kù)轉(zhuǎn)靜態(tài)庫(kù),或者合并動(dòng)態(tài)庫(kù)

          源碼形式的是可以通過(guò)CocoaPods命令轉(zhuǎn)靜態(tài)庫(kù)的,如下

          # CocoaPods 打包靜態(tài)庫(kù) 命令 
          # 其中 –library 指定打包成.a文件,如果不帶上將會(huì)打包成.framework文件。–force 是指強(qiáng)制覆蓋。
          pod package xxxx.podspec --force

          CocoaPods不使用 use_frameworks! 字段,全部引入靜態(tài)庫(kù)

          rebase&binding 和 Objc setup:

          減少代碼量

          1、基于Mach-O文件分析

          objcselrefs 和 objcclassrefs 存儲(chǔ)了所有引用到的 sel方法簽名 和 class

          __objc_classlist 存儲(chǔ)了所已有的 sel 和 class

          二者做個(gè)差集就知道哪些類(lèi)和哪些類(lèi)的 sel 用不到,但objc 支持運(yùn)行時(shí)調(diào)用,刪除之前還要在二次確認(rèn)

          2、通過(guò)打點(diǎn)SDK,收集代碼使用數(shù)據(jù)情況,再?zèng)Q定要不要?jiǎng)h除某些代碼

          脈脈會(huì)定期通過(guò)數(shù)據(jù)庫(kù)腳本統(tǒng)計(jì)出日活小于10uv的頁(yè)面vc和相關(guān)的view,和相關(guān)的業(yè)務(wù)線確認(rèn)后會(huì)進(jìn)行刪除,以確保代碼的有效性和簡(jiǎn)潔性。

          3、通過(guò)脈脈自研的解耦合分析工具(后面會(huì)考慮開(kāi)源,可以關(guān)注作者github: Andy.Li)

          大致原理:數(shù)學(xué)集合運(yùn)算

          假如只有兩個(gè)類(lèi)a和類(lèi)b,分析出類(lèi)a提供的方法列表記為集合A,再分析出類(lèi)b使用的類(lèi)a的方法列表記為集合B。

          集合A和集合B做差集C,集合C就類(lèi)a中不再被類(lèi)b使用的方法集合

          此時(shí)就可以根據(jù)集合C從類(lèi)a中刪除對(duì)應(yīng)的方法。

          如果類(lèi)a的所有方法都沒(méi)有被類(lèi)b使用,也就是類(lèi)a完全不被類(lèi)b使用,則可以直接刪除類(lèi)a。

          類(lèi)比到到項(xiàng)目中所有的類(lèi),做遍歷遞歸差集,就可以得到全部的未被使用的類(lèi)和方法,考慮刪除之。

          重新排列函數(shù)符號(hào)位置,降低MACH-O文件載入內(nèi)存時(shí)PageFault缺頁(yè)中斷頻率 - 二進(jìn)制重排

          原理

          二進(jìn)制重排實(shí)際上是在windows和linux上就存在的技術(shù),旨在將啟動(dòng)用到的函數(shù)方法盡可能的放置在二進(jìn)制文件加載的前面,并且是將函數(shù)符號(hào)地址連續(xù)的編譯在一起,以減少Page Fault的次數(shù)和頻率,加快啟動(dòng)速度?,F(xiàn)在這項(xiàng)技術(shù)已經(jīng)移植運(yùn)用到了移動(dòng)端app上。

          如何理解PageFault缺頁(yè)中斷

          操作系統(tǒng)為了解決安全問(wèn)題和效率問(wèn)題,抽象出了虛擬內(nèi)存頁(yè)的概念。內(nèi)存都是分頁(yè)訪問(wèn)的。這里的page指的就是內(nèi)存頁(yè)。(就像磁盤(pán)存儲(chǔ)的最小單位 磁盤(pán)簇,大小是4k一樣)

          MacOS 、linux (4K為一頁(yè))

          iOS(16K為一頁(yè))

          PageFault就是缺頁(yè)中斷:當(dāng)app調(diào)用一個(gè)方法,發(fā)現(xiàn)該方法沒(méi)有在內(nèi)存中,此時(shí)操作系統(tǒng)就會(huì)立刻阻塞整個(gè)app進(jìn)程,觸發(fā)一個(gè)缺頁(yè)中斷。操作系統(tǒng)會(huì)從磁盤(pán)中讀取這頁(yè)數(shù)據(jù)到物理內(nèi)存上 , 然后再將其映射到虛擬內(nèi)存上 ( 如果當(dāng)前內(nèi)存已滿 , 操作系統(tǒng)會(huì)通過(guò)置換頁(yè)算法 找一頁(yè)數(shù)據(jù)進(jìn)行覆蓋,這也是為什么開(kāi)再多的應(yīng)用也不會(huì)崩掉 , 但是之前開(kāi)的應(yīng)用再打開(kāi)時(shí) , 就重新啟動(dòng)了的根本原因 )。

          假如,app啟動(dòng)時(shí)期需要調(diào)用 method1、method5和method6,這三個(gè)方法分布在page1、page2和page3上。每裝載一個(gè)內(nèi)存頁(yè)page都會(huì)發(fā)生一次PageFault(缺頁(yè)終端)。通常一個(gè)PageFault的處理時(shí)間是0.1ms~1ms,取0.5ms計(jì)算。這三次處理PageFault時(shí)間是 3 * 0.5ms = 1.5ms。

          二進(jìn)制重排后

          method1、method5和method6全都集中在了page1,這樣只需裝載page1就可以了。相比之前少了page2和page3的裝載。少了兩次處理PageFault時(shí)間。這次消耗的時(shí)間是 1 * 0.5ms = 0.5ms。節(jié)省了1ms

          iOS App之所以能夠使用二進(jìn)制重排,是因?yàn)閄code 已經(jīng)提供好這個(gè)機(jī)制 , 并且 libobjc 實(shí)際上也是用了二進(jìn)制重排進(jìn)行優(yōu)化 .

          獲取啟動(dòng)加載所有的函數(shù)的符號(hào)

          只有準(zhǔn)確獲取了app啟動(dòng)所用到的函數(shù)方法,對(duì)其進(jìn)行重新排列,才能做到啟動(dòng)加速,那么如何獲取這些函數(shù)符號(hào)呢?

          Hook

          oc 或者 swift @objc dynamic 修飾的方法,調(diào)用都會(huì)通過(guò) objc_MsgSend 發(fā)送消息,hook objc_MsgSend 可以做到這個(gè)方法的檢測(cè)。但如果是可變參數(shù)個(gè)數(shù),則需要匯編來(lái)獲取參數(shù)

          二進(jìn)制靜態(tài)掃描

          Mach-O文件在特定段Segment和Section里存儲(chǔ)著符號(hào)及函數(shù)數(shù)據(jù),通過(guò)靜態(tài)掃描Mach-O文件,主要是分析獲取load方法和c++ constructor 構(gòu)造方法。

          clang 匯編插樁

          clang 本身已經(jīng)提供了一個(gè)代碼覆蓋率檢測(cè)機(jī)制(SanitizerCoverage),來(lái)實(shí)現(xiàn)我們獲取所有符號(hào)的需求

          前兩種都或多或少存在一些問(wèn)題,并不是完美的狀態(tài),網(wǎng)上的資料有很多,可以自行查閱。接下來(lái)主要是通過(guò)clang 插樁的方式來(lái)hook所有的函數(shù)符號(hào)

          clang插樁

          Xcode如何配置

          在目標(biāo)工程 Target -> Build Settings -> Other C Flags 添加 -fsanitize-coverage=func, trace-pc-guard。

          如果有swfit代碼,也要在 Other Swift Flags 添加 -sanitize-coverage=func 和 __-sanitize=undefined__

          (如果有源碼編譯的Framework也要添加這些配置。CocoaPods引入的第三方庫(kù)不建議添加上述配置)

          添加hook代碼

          LLVM內(nèi)置了一個(gè)簡(jiǎn)單的代碼覆蓋率檢測(cè)(SanitizerCoverage)。它在函數(shù)級(jí)、基本塊級(jí)和邊緣級(jí)插入對(duì)用戶定義函數(shù)的調(diào)用,并提供了這些回調(diào)的默認(rèn)實(shí)現(xiàn)。在認(rèn)為啟動(dòng)結(jié)束的位置添加代碼,就能夠拿到啟動(dòng)到指定位置調(diào)用到的所有函數(shù)符號(hào)。

          void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, 
          uint32_t *stop) {
          static uint64_t N; // Counter for the guards.
          if (start == stop || *start) return; // Initialize only once.
          printf("INIT: %p %p\n", start, stop);
          for (uint32_t *x = start; x < stop; x++)
          *x = ++N; // Guards should start from 1.
          }

          //原子隊(duì)列
          static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
          //定義符號(hào)結(jié)構(gòu)體
          typedef struct{
          void * pc;
          void * next;
          }SymbolNode;
          void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
          //if (!*guard) return; // Duplicate the guard check.
          void *PC = __builtin_return_address(0);

          SymbolNode * node = malloc(sizeof(SymbolNode));
          *node = (SymbolNode){PC,NULL};

          //入隊(duì)
          // offsetof 用在這里是為了入隊(duì)添加下一個(gè)節(jié)點(diǎn)找到 前一個(gè)節(jié)點(diǎn)next指針的位置
          OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
          }

          運(yùn)行工程,通過(guò)Hopper反編譯工具可以看到在函數(shù)內(nèi)部一開(kāi)始就添加了 額外方法的匯編代碼,這樣就做到了 靜態(tài)插樁

          脈脈自研二進(jìn)制重排預(yù)分析工具(已開(kāi)源)

          此工具詳細(xì)介紹了如何生成 linked_map.txt 和 lb.order文件,以及如何預(yù)驗(yàn)證重排效果。

          目的

          在沒(méi)有上線之前可以分析出對(duì)App啟動(dòng)優(yōu)化節(jié)省的大致時(shí)間,起到指導(dǎo)作用。(具體能優(yōu)化多少,以線上數(shù)據(jù)為準(zhǔn))

          建議

          因?yàn)楣こ痰拇a隨著開(kāi)發(fā)的進(jìn)行會(huì)不斷的改變位置或者刪減,之前已經(jīng)排好的順序有些會(huì)失效。

          每隔三個(gè)月執(zhí)行一次二進(jìn)制重排更新,確保 PageFault 次數(shù)維持在一個(gè)較低的穩(wěn)定的水平。

          輸出示例

          ---> 分析結(jié)果:
          linked map __Text(鏈接文件):
          起始地址:0x100006A60
          結(jié)束地址:0x1021E75E8
          分配的虛擬內(nèi)存頁(yè)個(gè)數(shù):2169
          order symbol(重排文件)
          需要重排的符號(hào)個(gè)數(shù):4630
          分布的虛擬內(nèi)存頁(yè)個(gè)數(shù):392
          二進(jìn)制重排后分布的虛擬內(nèi)存頁(yè)個(gè)數(shù):99
          內(nèi)存缺頁(yè)中斷減少的個(gè)數(shù):293
          預(yù)估節(jié)省的時(shí)間:146ms

          如何反向驗(yàn)證

          用二進(jìn)制重排之后的工程,再次分別編譯出 linked_map.txt 和 lb.order 文件,使用此工具再次運(yùn)行檢查??梢缘玫饺缦陆Y(jié)果

          ---> 分析結(jié)果:
          linked map __Text(鏈接文件):
          起始地址:0x100006A60
          結(jié)束地址:0x1021E75E8
          分配的虛擬內(nèi)存頁(yè)個(gè)數(shù):2169
          order symbol(重排文件)
          需要重排的符號(hào)個(gè)數(shù):4630
          分布的虛擬內(nèi)存頁(yè)個(gè)數(shù):99
          二進(jìn)制重排后分布的虛擬內(nèi)存頁(yè)個(gè)數(shù):99
          內(nèi)存缺頁(yè)中斷減少的個(gè)數(shù):0
          預(yù)估節(jié)省的時(shí)間:0ms

          可以看出重排后的二進(jìn)制文件已經(jīng)不需要再次進(jìn)行重排了。至此,二進(jìn)制重排線下預(yù)評(píng)估結(jié)束。

          工具開(kāi)源地址:https://github.com/lyandy/Linked_Order_Analyze

          load&constructor&initialize

          +load 盡量不要使用

          在 pre-main 時(shí)期,objc 會(huì)向 dyld 注冊(cè)一個(gè) init 回調(diào),當(dāng) dyld 將要執(zhí)行載入 image 的 initializers 流程時(shí) (依賴(lài)的所有 image 已走完 initializers 流程時(shí)),init 回調(diào)被觸發(fā),在這個(gè)回調(diào)中,objc 會(huì)按照父類(lèi)-子類(lèi)-分類(lèi)順序調(diào)用 +load 方法。因?yàn)?+load 方法執(zhí)行地足夠早,并且只執(zhí)行一次,所以我們通常會(huì)在這個(gè)方法中進(jìn)行 method swizzling 或者自注冊(cè)操作。也正是因?yàn)?+load 方法調(diào)用時(shí)間點(diǎn)的特殊性,導(dǎo)致此方法的耗時(shí)監(jiān)測(cè)較為困難,而如何使監(jiān)測(cè)代碼先于 +load 方法執(zhí)行成為解決此問(wèn)題的關(guān)鍵點(diǎn)。

          脈脈自研了一套 hook 監(jiān)測(cè) +load 執(zhí)行時(shí)間方案,并結(jié)合 CocoaPods 實(shí)現(xiàn)了一行代碼集成耗時(shí)監(jiān)測(cè)的功能。(后續(xù)會(huì)開(kāi)源)

          __attribute__((constructor)) 盡量不要使用

          如果函數(shù)被設(shè)定為 constructor 屬性,則該函數(shù)會(huì)在 main 函數(shù)執(zhí)行之前被自動(dòng)的執(zhí)行。執(zhí)行時(shí)間太長(zhǎng),會(huì)大大增加啟動(dòng)時(shí)間。

          C/C++靜態(tài)化變量遷移

          1、std:string 轉(zhuǎn)換成 const char *
          2、靜態(tài)變量移動(dòng)到方法內(nèi)部

          因?yàn)榉椒▋?nèi)部的靜態(tài)變量會(huì)在方法第一次調(diào)用的時(shí)候初始化

          main 之后

          啟動(dòng)器

          啟動(dòng)是需要一個(gè)框架來(lái)管控的,脈脈采用的是流控制方案。

          為什么需要啟動(dòng)器呢?

          全局并發(fā)調(diào)度

          比如 AB 任務(wù)并發(fā),C 任務(wù)等待 AB 執(zhí)行完畢,框架調(diào)度還能減少線程數(shù)量和控制優(yōu)先級(jí)

          延遲執(zhí)行

          提供一些時(shí)機(jī),業(yè)務(wù)可以做預(yù)熱性質(zhì)的初始化

          精細(xì)化監(jiān)控

          所有任務(wù)的耗時(shí)都能監(jiān)控到,線下自動(dòng)化監(jiān)控也能受益

          管控

          啟動(dòng)任務(wù)的順序調(diào)整,新增/刪除都能通過(guò) Code Review 管控

          脈脈自研的流控制器方案流程

          脈脈的啟動(dòng)流程大致分為三個(gè)階段

          流控制器

          從Task1到Task8分為兩個(gè)Flow Category,基礎(chǔ)支撐Category和業(yè)務(wù)流Category。這些業(yè)務(wù)流flow中有廣告、新手引導(dǎo)、資料補(bǔ)全引導(dǎo)、業(yè)務(wù)拉新等眾多邏輯。解耦合和精細(xì)化管控每個(gè)Task所執(zhí)行的代碼,啟動(dòng)流程得以規(guī)范化治理,啟動(dòng)速度也大大加快。

          主容器加載

          包含了底部5個(gè)tab容器的加載,其中首頁(yè)tab會(huì)被優(yōu)先加載,其他4個(gè)懶加載。

          RN首頁(yè)首幀渲染

          通常首頁(yè)容器加載完畢,就認(rèn)為是啟動(dòng)結(jié)束的點(diǎn),如上圖虛線的位置。但脈脈的啟動(dòng)結(jié)束點(diǎn)是首頁(yè)RN Feed流第一幀渲染結(jié)束的點(diǎn)。

          優(yōu)化方式

          三方SDK

          有些三方 SDK 的啟動(dòng)耗時(shí)很高,將第三方SDK延后或并發(fā)。有些SDK已經(jīng)分布在了不同的flow里并發(fā),但flow內(nèi)部還可以做并發(fā),但要注意線程數(shù)量的控制

          高頻次方法

          有些方法的單個(gè)耗時(shí)不高,但是在啟動(dòng)路徑上會(huì)調(diào)用很多次的,這種累計(jì)起來(lái)的耗時(shí)也不低,比如讀 Info.plist 里面的配置:

          + (NSString *)plistChannel 
          {
          return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CHANNEL_NAME"];
          }

          線程間一些信號(hào)量等待鎖,有可能會(huì)長(zhǎng)時(shí)間卡主啟動(dòng)流程的方法,需要移除或者換種方式去做。

          線程數(shù)量

          線程的數(shù)量和優(yōu)先級(jí)都會(huì)影響啟動(dòng)時(shí)間。在介紹 System Trace 工具的環(huán)節(jié),有講到高優(yōu)先級(jí)線程和CPU數(shù)量的關(guān)系。瞬時(shí)開(kāi)啟過(guò)多的線程,占用了太多的內(nèi)存和CPU,反而會(huì)拖慢啟動(dòng)速度

          圖片

          啟動(dòng)難免會(huì)用到很多圖,有沒(méi)有辦法優(yōu)化圖片加載的耗時(shí)呢?

          用 Asset 管理圖片而不是直接放在 bundle 里。Asset 會(huì)在編譯期做優(yōu)化,讓加載的時(shí)候更快。

          此外在 Asset 中加載圖片是要比 Bundle 快的,因?yàn)?UIImage imageNamed 要遍歷 Bundle 才能找到圖。

          加載 Asset 中圖的耗時(shí)主要在在第一次張圖,因?yàn)橐⑺饕梢酝ㄟ^(guò)把啟動(dòng)的圖放到一個(gè)小的 Asset 里來(lái)減少這部分耗時(shí)。每次創(chuàng)建 UIImage 都需要 IO,在首幀渲染的時(shí)候會(huì)解碼。所以可以通過(guò)提前子線程預(yù)加載(創(chuàng)建 UIImage)來(lái)優(yōu)化這部分耗時(shí)。

          Fishhook

          fishhook 是一個(gè)用來(lái) hook C 函數(shù)的庫(kù),但這個(gè)庫(kù)的第一次調(diào)用耗時(shí)很高,最好不要帶到線上。fishhook 是遍歷 Mach-O 的多個(gè)段來(lái)找函數(shù)指針和函數(shù)符號(hào)名的映射關(guān)系,帶來(lái)的副作用就是要大量的 Page In,對(duì)于大型 App 來(lái)說(shuō)在 iPhone X 冷啟耗時(shí) 200ms+。

          如果不得不用 fishhook,請(qǐng)?jiān)谧泳€程調(diào)用,且不要在在 dyldregister_func_for_add_image 直接調(diào)用 fishhook。因?yàn)檫@個(gè)方法會(huì)持有 dyld 的一個(gè)全局互斥鎖,主線程在啟動(dòng)的時(shí)候系統(tǒng)庫(kù)經(jīng)常會(huì)調(diào)用 dlsym 和 dlopen。其內(nèi)部也需要這個(gè)鎖,造成上文提到的子線程阻塞主線程。

          首幀渲染

          不同 App 的業(yè)務(wù)形態(tài)不同,優(yōu)化方式也相差的比較多,幾個(gè)常見(jiàn)的優(yōu)化點(diǎn):

          LottieView

          lottie 是 airbnb 用來(lái)做 AE 動(dòng)畫(huà)的庫(kù),但是加載動(dòng)畫(huà)的 json 和讀圖是比較慢的,可以先顯示一幀靜態(tài)圖,啟動(dòng)結(jié)束后再開(kāi)始動(dòng)畫(huà),或者子線程預(yù)先把圖和 json 設(shè)置到 lottie cache 里

          Lazy 初始化 View

          不要先創(chuàng)建設(shè)置成 hidden,這是很不好的習(xí)慣

          AutoLayout

          AutoLayout 的耗時(shí)也是比較高的,但這塊往往歷史包袱比較重,可以評(píng)估 ROI 看看要不要改成 frame

          Loading 動(dòng)畫(huà)

          App 一般都會(huì)有個(gè) loading 動(dòng)畫(huà)表示加載中,這個(gè)動(dòng)畫(huà)最好不要用 gif,線下測(cè)量一個(gè) 60 幀的 gif 加載耗時(shí)接近 70ms



          五、驗(yàn)證 脈脈App啟動(dòng)優(yōu)化效果


          pre-main

          二進(jìn)制重排效果

          應(yīng)用啟動(dòng)90分位耗時(shí)降低 600ms,且非常穩(wěn)定

          未重排的版本:5.3.64、5.3.66

          已重排的版本:5.3.70、5.3.74

          main 之后

          啟動(dòng)耗時(shí)再次降低 500ms,實(shí)現(xiàn)了秒開(kāi)

          優(yōu)化前的版本:6.0.62、6.0.64

          優(yōu)化后的版本:6.0.70、6.0.72

          總體啟動(dòng)耗時(shí)native部分的優(yōu)化,在2021年6月10號(hào) 6.0.70 版本上線后,由之前的600ms降到了270ms,降了接近300ms多。

          同時(shí)通過(guò)統(tǒng)計(jì)從feed vc didappear到RN首幀渲染也降低了200ms左右。

          從Xcode自帶的統(tǒng)計(jì)工具Organizer也可以看出,6.0.70版本啟動(dòng)時(shí)間90分位為900ms, 實(shí)現(xiàn)秒開(kāi)


          六、補(bǔ)充 非常規(guī)優(yōu)化手段


          +load 方法遷移

          +load 除了方法本身的耗時(shí),還會(huì)引起大量 PageFault,

          另外 +load 的存在對(duì) App 穩(wěn)定性也是沖擊,因?yàn)?Crash 了捕獲不到。

          舉個(gè)例子,很多 容器需要把協(xié)議綁定到類(lèi),所以需要在啟動(dòng)的早期(+load)里注冊(cè)

          + (void)load 
          {
          [ProtocolClass registerClass:IMPClass forProtocol:@protocol(myProcotol)]
          }

          本質(zhì)上只要知道協(xié)議和類(lèi)的對(duì)應(yīng)關(guān)系即可,利用 clang attribute,這個(gè)過(guò)程可以遷移到編譯期, 在Mach-O文件的末尾再添加一個(gè)Section段,Mach-O裝載進(jìn)內(nèi)存加載Section段的時(shí)候,再去做Class和Protocol的對(duì)應(yīng)關(guān)系。這種方式已經(jīng)運(yùn)用到脈脈自動(dòng)化解耦合工具里。

          typedef struct{ 
          const char * cls;
          const char * protocol;
          }_mm_pair;
          #if DEBUG
          #define MM_SERVICE(PROTOCOL_NAME,CLASS_NAME)\
          __used static Class<PROTOCOL_NAME> _MM_VALID_METHOD(void){\
          return [CLASS_NAME class];\
          }\
          __attribute((used, section(_MM_SEGMENT "," _MM_SECTION ))) static _mm_pair _MM_UNIQUE_VAR = \
          {\
          _TO_STRING(CLASS_NAME),\
          _TO_STRING(PROTOCOL_NAME),\
          };\
          #else
          __attribute((used, section(_MM_SEGMENT "," _MM_SECTION ))) static _mm_pair _MM_UNIQUE_VAR = \
          {\
          _TO_STRING(CLASS_NAME),\
          _TO_STRING(PROTOCOL_NAME),\
          };\
          #endif

          當(dāng)時(shí)脈脈iOS做解耦合的時(shí)候采用的,添加 ProtocolSect Section Data段

          __Text段 重命名遷移

          App Store 會(huì)對(duì)上傳的 App 的 TEXT 段加密,在發(fā)生 PageFault 的時(shí)候會(huì)解密,解密的過(guò)程是很耗時(shí)的。

          既然會(huì) TEXT 段加密,那么直接的思路就是把 TEXT 段中的內(nèi)容移動(dòng)到其它段,ld 也有個(gè)參數(shù) rename_section 支持重命名。

          不建議使用此種方式優(yōu)化,原因如下:

          1、__TEXT 段遷移最難解決的問(wèn)題是ld鏈接失敗問(wèn)題,是由 CPU 對(duì)尋址范圍的限制以及 ld64 鏈接器的缺陷導(dǎo)致。
          2、被遷移的__TEXT 段段,無(wú)法配合dSYM文件做符號(hào)化

          PGO優(yōu)化啟動(dòng)時(shí)間

          PGO是蘋(píng)果官方提供的工具,具體使用方法是點(diǎn)擊xcode工具欄

          Product -> Perform Action -> Generate Optimization Profile 按xcode提示操作即可

          不建議使用此種方式優(yōu)化,原因如下:

          1、如果項(xiàng)目中有 swift 代碼,那么這種方式就不能用了,因?yàn)?swift 不支持 PGO。
          2、代碼發(fā)生變更,Xcode 會(huì)提示 profdata file out of date,需要每個(gè)版本或者每隔一段時(shí)間重新生成

          瀏覽 95
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品第一页在线观看 | 免费一级a毛片,免费观看免 | 九九在线观看视频 | 欧美激情亚洲无码 | 中文字幕五码在线 |