抖音品質建設 - iOS啓動優化《實戰篇》

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動是 App 給用戶的第一印象,啓動越慢,用戶流失的概率就越高,良好的啓動速度是用戶體驗不可缺少的一環。啓動優化涉及到的知識點非常多,面也很廣,一篇文章難以包含全部,所以拆分成兩部分:原理和實戰,本文是實戰篇。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原理篇:"},{"type":"link","attrs":{"href":"http:\/\/mp.weixin.qq.com\/s?__biz=MzI1MzYzMjE0MQ==&mid=2247486932&idx=1&sn=eb4d294e00375d506b93a00b535c6b05&chksm=e9d0c636dea74f20ec800af333d1ee94969b74a92f3f9a5a66a479380d1d9a4dbb8ffd4574ca&scene=21#wechat_redirect","title":null,"type":null},"content":[{"type":"text","marks":[{"type":"underline"}],"text":"抖音品質建設-iOS 啓動優化《原理篇》"}],"marks":[{"type":"underline"}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"如何做啓動優化?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章的正式內容開始之前,大家可以思考下,如果自己去做啓動優化的,會如何去開展?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這其實是一個比較大的問題,遇到類似情況,我們都可以去把大問題拆解成幾個小的問題:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線上用戶究竟啓動狀況如何?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何去找到可以優化的點?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"做完優化之後,如何保持?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有沒有一些成熟的經驗可以借鑑,業界都是怎麼做的?"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對應着本文的三大模塊:"},{"type":"text","marks":[{"type":"strong"}],"text":"監控"},{"type":"text","text":","},{"type":"text","marks":[{"type":"strong"}],"text":"工具"},{"type":"text","text":"和"},{"type":"text","marks":[{"type":"strong"}],"text":"最佳實踐。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"監控"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"啓動埋點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然要監控,那麼就要能夠在代碼裏獲取到啓動時長。啓動的起點大家採用的方案都一樣:進程創建的時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動的終點對應用戶感知到的 Launch Image 消失的第一幀,抖音採用的方案如下:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOS 12 及以下:root viewController 的 viewDidAppear"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOS 13+:applicationDidBecomeActive"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Apple 官方的統計方式是第一個 "},{"type":"codeinline","content":[{"type":"text","text":"CA::Transaction::commit"}]},{"type":"text","text":",但對應的實現在系統框架內部,抖音的方式已經非常接近這個點了。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"分階段"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只有一個啓動耗時的埋點在排查線上問題的時候顯然是不夠的,可以通過"},{"type":"text","marks":[{"type":"strong"}],"text":"分階段和單點埋點"},{"type":"text","text":"結合,下面是這是目前抖音的監控方案:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/03\/03b9fc13a2d74d0359b7fa8d602aaefc.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+load、initializer 的調用順序和鏈接順序有關,鏈接順序默認按照 CocoaPod 的 Pod 命名升序排列,所以取一個命名爲 AAA 開頭既可以讓某個 +load、initializer 第一個被執行。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"無侵入監控"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"公司的 APM 團隊提供了一種無侵入的啓動監控方案,方案將啓動流程拆分成幾個粒度比較粗的與業務無關的階段:進程創建,最早的 +load,didFinishLuanching 開始和首屏首次繪製完成。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/11\/1190e2c9f436b237a620440e09d84677.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前三個時間點無侵入獲取較爲簡單"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進程創建:通過 "},{"type":"codeinline","content":[{"type":"text","text":"sysctl"}]},{"type":"text","text":" 系統調用拿到進程創建的時間戳"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最早的 +load:和上面的分階段監控一樣,通過 AAA 爲前綴命名 Pod,讓 +load 第一個被執行"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"didFinishLaunching:監控 SDK 初始化一般在啓動的很早期,用監控 SDK 的初始化時間作爲 didFinishLaunching 的時間"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首屏渲染完成時間我們希望和 "},{"type":"codeinline","content":[{"type":"text","text":"MetricKit"}]},{"type":"text","text":" 對齊,即獲取到 "},{"type":"codeinline","content":[{"type":"text","text":"CA::Transaction::commit()"}]},{"type":"text","text":"方法被調用的時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 Runloop 源碼分析和線下調試,我們發現 "},{"type":"codeinline","content":[{"type":"text","text":"CA::Transaction::commit()"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"CFRunLoopPerformBlock"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"kCFRunLoopBeforeTimers"}]},{"type":"text","text":" 這三個時機的順序從早到晚依次是:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f1\/f19fa58a373a8f6810bd0d8c722f98c4.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以通過在 didFinishLaunch 中向 Runloop 註冊 block 或者 BeforeTimer 的 Observer 來獲取上圖中兩個時間點的回調,代碼如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/註冊block\nCFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];\nCFRunLoopPerformBlock(mainRunloop,NSDefaultRunLoopMode,^(){\n    NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];\n    NSLog(@\"runloop block launch end:%f\",stamp);\n});\n\/\/註冊kCFRunLoopBeforeTimers回調\nCFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];\nCFRunLoopActivity activities = kCFRunLoopAllActivities;\nCFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {\n    if (activity == kCFRunLoopBeforeTimers) {\n        NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];\n        NSLog(@\"runloop beforetimers launch end:%f\",stamp);\n        CFRunLoopRemoveObserver(mainRunloop, observer, kCFRunLoopCommonModes);\n    }\n});\nCFRunLoopAddObserver(mainRunloop, observer, kCFRunLoopCommonModes);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過實測,我們最後選擇的無侵入獲取首屏渲染方案是:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"iOS13(含)以上的系統採用 "},{"type":"codeinline","content":[{"type":"text","text":"runloop"}]},{"type":"text","text":" 中註冊一個 "},{"type":"codeinline","content":[{"type":"text","text":"kCFRunLoopBeforeTimers"}]},{"type":"text","text":" 的回調獲取到的 App 首屏渲染完成的時機更準確。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"iOS13 以下的系統採用 "},{"type":"codeinline","content":[{"type":"text","text":"CFRunLoopPerformBlock"}]},{"type":"text","text":" 方法注入 block 獲取到的 App 首屏渲染完成的時機更準確。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"監控週期"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/6c\/6cd6fc7436cee58e1448389b9fa520cf.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"App 的生命週期可以分爲三個階段:研發,灰度和線上,不同階段監控的目的和方式都不一樣。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"研發階段"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"研發階段的監控主要目的是防止劣化,對應着會有線下的自動化監控,通過實際的啓動性能測試來儘早地發現和解決問題,抖音的線下自動化監控流程圖如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ca\/ca53981858370fa22b076c13233fb331.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由定時任務觸發,先 release 模式下打包,接着跑一次自動化測試,測試完畢後會上報測試結果,方便通過看板來跟蹤整體的變化趨勢。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果發現有劣化,會先發一條報警信息,接着通過二分查找的方式找到對應的劣化 MR,然後自動跑火焰圖和 Instrument 來輔助定位問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼如何保證測試的結果是穩定可靠的呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答案就是控制變量:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關閉 iCloud & 不登錄 AppleID & 飛行模式"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"風扇降溫,且用 MFI 認證數據線"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重啓手機和開始下一次測試之前靜置一段時間"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多次測量取平均值 & 計算方差"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Mock 啓動過程中的 AB 變量"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實踐下來,我們發現 iPhone 8 的穩定性最好,其次是 iPhone X,iPhone 6 的穩定性很差。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了自動化測試,在研發流程上還可以加一些准入,來防止啓動劣化,這些准入包括"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"新增動態庫"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"新增 +load 和靜態初始化"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"新增啓動任務 Code Review"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不建議做細粒度的 Code Review,除非對相關業務很瞭解,否則一般肉眼看不出會不會有劣化。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"線上 & 灰度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"灰度和線上的策略是相似的,"},{"type":"text","marks":[{"type":"strong"}],"text":"主要看的是大盤數據和配置報警"},{"type":"text","text":",大盤監控和報警和公司的基建有很大關係,如果沒有對應基建 Xcode MetricKit 本身也可以看到啓動耗時:打開 Xcode -> Window -> Origanizer -> Launch Time"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大盤數據本身是統計學的,會有些統計學的規律:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"發版本的前幾天啓動速度比較慢"},{"type":"text","text":",這是因爲 iOS 13 後更新 App 的第一次啓動要創建啓動閉包,這個過程是比較慢的"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"新版本發佈會導致老版本 pct50 變慢"},{"type":"text","text":",因爲性能差的設備升級速度慢,導致老版本性能差設備比例變高"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"採樣率調整會影響 pct50"},{"type":"text","text":",比如某些地區的 iPhone 6 比例較高,如果這些地區採樣率提高會導致大盤性能差的設備比例提高。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於這些背景,我們一般會通過控制變量的方式:拆地區,機型,版本,有時候甚至要根據時間看啓動耗時的趨勢。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"工具"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成了監控之後,我們需要找到一些可以優化的點,就需要用到工具。主要包括兩大類:"},{"type":"text","marks":[{"type":"strong"}],"text":"Instrument 和自研"},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Time Profiler"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Time Profiler 是大家日常性能分析中用的比較多的工具,通常會選擇一個時間段,然後聚合分析調用棧的耗時。但"},{"type":"text","marks":[{"type":"strong"}],"text":"Time Profiler 其實只適合粗粒度的分析"},{"type":"text","text":",爲什麼這麼說呢?我們來看下它的實現原理:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"默認 Time Profiler 會 1ms 採樣一次,只採集在運行線程的調用棧,最後以統計學的方式彙總"},{"type":"text","text":"。比如下圖中的 5 次採樣中,method3 都沒有采樣到,所以最後聚合到的棧裏就看不到 method3。所以 Time Profiler 中的看到的時間,並不是代碼實際執行的時間,而是棧在採樣統計中出現的時間。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/73\/739e1bd94c59375e82c4f3314db3dcc7.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Time Profiler 支持一些額外的配置,"},{"type":"text","marks":[{"type":"strong"}],"text":"如果統計出來的時間和實際的時間相差比較多,可以嘗試開啓"},{"type":"text","text":":"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"High Frequency,降低採樣的時間間隔"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Record Kernel Callstacks,記錄內核的調用棧"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Record Waiting Thread,記錄被 block 的線程"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"System Trace"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然 Time Profiler 支持粗粒度的分析,那麼有沒有什麼精細化的分析工具呢?答案就是 System Trace。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/61\/61e910d07456afc09a5ab4c1d8b54821.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然要精細化分析,那麼我們就需要標記出一小段時間,可以用 Point of interest 來標記。除此之外,System Trace 分析虛擬內存和線程狀態都很管用:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Virtual Memory:"},{"type":"text","marks":[{"type":"strong"}],"text":"主要關注 Page In"},{"type":"text","text":"這個事件,因爲啓動路徑上有很多次 Page In,且相對耗時"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Thread State:"},{"type":"text","marks":[{"type":"strong"}],"text":"主要關注掛起和搶佔兩個狀態,記住主線程不是一直在運行的"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"System Load 線程有優先級,高優先級的線程不應該超過系統核心數量"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"os_signpost"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"os_signpost 是 iOS 12 推出的用於在 instruments 裏標記時間段的 API,性能非常高,可以認爲對啓動無影響。結合最開始講的分階段監控,我們可以在 Instrument 把啓動劃分成多個階段,和其他模板一起分析具體問題:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/56\/561f084fb626488fb30e632bf397f5aa.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結合 swizzle,os_signpost 可以發揮出意想不到的效果,比如 hook 所有的 load 方法,來分析對應耗時,又比如 hook UIImage 對應方法,來統計啓動路徑上用到的圖片加載耗時。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"其他 Instrument 模板"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了這些,還有幾個模板是比較常用的:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Static Initializer"},{"type":"text","text":":分析 C++ 靜態初始化"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"App Launch"},{"type":"text","text":":Xcode 11 後新出的模板,可以認爲是 Time Profiler + System Trace"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Custom Instrument"},{"type":"text","text":":自定義 Instrument,最簡單是用 os_signpost 作爲模板的數據源,自己做一些簡單的定製化展示,具體可參考 WWDC 的相關 Session。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"火焰圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"火焰圖用來分析時間相關的性能瓶頸非常有用"},{"type":"text","text":",可以直接把業務代碼的耗時繪製出來。此外,火焰圖可以自動化生成然後 diff,所以可以用於自動化歸因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"火焰圖有兩種常見實現方式"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hook objc_msgSend"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編譯期插樁"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本質上都是在方法的開始和末尾打兩個點,就知道這個方法的耗時,然後轉換成 Chrome 的標準的 json 格式就可以分析了。注意就算用 mmap 來寫文件,仍然會有一些誤差,所以找到的問題並不一定是問題,需要二次確認。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"最佳實踐"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"整體思路"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優化的整體思路其實就四步:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/93\/93421fba08590c93d8878225011e5c99.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"刪"},{"type":"text","text":"掉啓動項,最直接"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果不能刪除,嘗試"},{"type":"text","marks":[{"type":"strong"}],"text":"延遲"},{"type":"text","text":",延遲包括第一次訪問以及啓動結束後找個合適的時間預熱"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"不能延遲的可以嘗試"},{"type":"text","marks":[{"type":"strong"}],"text":"併發"},{"type":"text","text":",利用好多核多線程"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"如果併發也不行,可以嘗試讓代碼執行"},{"type":"text","marks":[{"type":"strong"}],"text":"更快"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"這塊會以 Main 函數做分界線,看下 Main 函數前後的優化方案;接着介紹如何優化 Page In;最後講解一些非常規的優化方案,這些方案對架構的要求比較高"},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Main 之前"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Main 函數之前的啓動流程如下:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"加載 dyld"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"創建啓動閉包(更新 App\/重啓手機需要)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"加載動態庫"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Bind & Rebase & Runtime 初始化"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+load 和靜態初始化"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a2\/a278c98fb2a7e24002687330213a95a5.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"動態庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"減少動態庫數量可以加減少啓動閉包創建和加載動態庫階段的耗時,官方建議動態庫數量小於 6 個。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"推薦的方式是動態庫轉靜態庫"},{"type":"text","text":",因爲還能額外減少包大小。另外一個方式是合併動態庫,但實踐下來可操作性不大。最後一點要提的是,"},{"type":"text","marks":[{"type":"strong"}],"text":"不要鏈接那些用不到的庫"},{"type":"text","text":"(包括系統),因爲會拖慢創建閉包的速度。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"下線代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下線代碼可以減少 Rebase & Bind & Runtime 初始化的耗時。那麼如何找到用不到的代碼,然後把這些代碼下線呢?"},{"type":"text","marks":[{"type":"strong"}],"text":"可以分爲靜態掃描和線上統計兩種方式"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最簡單的靜態掃描是基於 AppCode,但是項目大了之後 AppCode 的索引速度非常慢,另外的一種靜態掃描是基於 Mach-O 的:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"_objc_selrefs"}]},{"type":"text","text":" 和"},{"type":"codeinline","content":[{"type":"text","text":"_objc_classrefs"}]},{"type":"text","text":" 存儲了引用到的 sel 和 class"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"__objc_classlist"}]},{"type":"text","text":" 存儲了所有的 sel 和 class"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"二者做個差集就知道那些類\/sel 用不到,但"},{"type":"text","marks":[{"type":"strong"}],"text":"objc 支持運行時調用,刪除之前還要在二次確認"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有一種統計無用代碼的方式是用線上的數據統計,主流的方案有三種:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ViewConteroller 滲透率,hook 對應的聲明週期方法即可統計"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Class 滲透率,遍歷運行時的所有類,通過 Objective C Runtime 的標誌位判斷類是否被訪問"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"行級滲透率,需要用編譯期插樁,對包大小和執行速度均有損。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前兩種是 ROI 較高的方案,絕大多數時候 Class 級別的滲透率足夠了。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"+load 遷移"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"+load 除了方法本身的耗時,還會引起大量 Page In"},{"type":"text","text":",另外 +load 的存在對 App 穩定性也是衝擊,因爲 Crash 了捕獲不到。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舉個例子,很多 DI 的容器需要把協議綁定到類,所以需要在啓動的早期(+load)裏註冊:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"+ (void)load\n{\n    [DICenter bindClass:IMPClass toProtocol:@protocol(SomeProcotol)]\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本質上只要知道協議和類的對應關係即可,利用 clang attribute,這個過程可以遷移到編譯期:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"typedef struct{\n    const char * cls;\n    const char * protocol;\n}_di_pair;\n#if DEBUG\n#define DI_SERVICE(PROTOCOL_NAME,CLASS_NAME)\\\n__used static Class _DI_VALID_METHOD(void){\\\n    return [CLASS_NAME class];\\\n}\\\n__attribute((used, section(_DI_SEGMENT \",\" _DI_SECTION ))) static _di_pair _DI_UNIQUE_VAR = \\\n{\\\n_TO_STRING(CLASS_NAME),\\\n_TO_STRING(PROTOCOL_NAME),\\\n};\\\n#else\n__attribute((used, section(_DI_SEGMENT \",\" _DI_SECTION ))) static _di_pair _DI_UNIQUE_VAR = \\\n{\\\n_TO_STRING(CLASS_NAME),\\\n_TO_STRING(PROTOCOL_NAME),\\\n};\\\n#endif\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原理很簡單:"},{"type":"text","marks":[{"type":"strong"}],"text":"宏提供接口,編譯期把類名和協議名寫到二進制的指定段裏,運行時把這個關係讀出來就知道協議是綁定到哪個類了"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有同學會注意到有個無用的方法"},{"type":"codeinline","content":[{"type":"text","text":"_DI_VALID_METHOD"}]},{"type":"text","text":" ,這個方法只在 debug 模式下存在,爲了讓編譯器保證類型安全。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"靜態初始化遷移"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"靜態初始化和 +load 方法一樣也會引起大量 Page In,一般來自 C++代碼,比如網絡或者特效的庫。另外"},{"type":"text","marks":[{"type":"strong"}],"text":"有些靜態初始化是通過頭文件引入進來的"},{"type":"text","text":",可以通過預處理來確認。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幾個典型的遷移思路:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"std:string 轉換成 const char *"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"靜態變量移動到方法內部,因爲方法內部的靜態變量會在方法第一次調用的時候初始化"}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/Bad\nnamespace {\n    static const std::string bucket[] = {\"apples\", \"pears\", \"meerkats\"};\n}\nconst std::string GetBucketThing(int i) {\n     return bucket[i];\n}\n\/\/Good\nstd::string GetBucketThing(int i) {\n  static const std::string bucket[] = {\"apples\", \"pears\", \"meerkats\"};\n  return bucket[i];\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Main 之後"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"啓動器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動是需要一個框架來管控的,抖音採用了輕量級的中心式方案:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有個啓動任務的配置倉,裏面只包含啓動任務的順序和線程"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"業務倉實現協議 BootTask,表明這是個啓動任務"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動任務的執行流程如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/8b\/8b6cab47601257d5aa1707b88bdc47c7.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼需要啓動器呢?"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"全局併發調度"},{"type":"text","text":":比如 AB 任務併發,C 任務等待 AB 執行完畢,框架調度還能減少線程數量和控制優先級"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"延遲執行"},{"type":"text","text":":提供一些時機,業務可以做預熱性質的初始化"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"精細化監控"},{"type":"text","text":":所有任務的耗時都能監控到,線下自動化監控也能受益"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"管控"},{"type":"text","text":":啓動任務的順序調整,新增\/刪除都能通過 Code Review 管控"}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"三方 SDK"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有些三方 SDK 的啓動耗時很高,比如 Fabric,抖音下線了 Fabric 後啓動速度 pct50 快了 70ms 左右。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了下線,很多 SDK 是可以延遲的,比如分享和登錄的 SDK。此外,在接入 SDK 之前可以先評估下對啓動性能的影響,如果影響較大是可以反饋給 SDK 的提供方去修改的,尤其是付費的 SDK,他們其實很願意配合做一些修改。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"高頻次方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有些方法的單個耗時不高,但是在啓動路徑上會調用很多次的,這種累計起來的耗時也不低,比如讀 Info.plist 裏面的配置:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"+ (NSString *)plistChannel\n{\n    return [[[NSBundle mainBundle] infoDictionary] objectForKey:@\"CHANNEL_NAME\"];\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"修改的方式很簡單,加一層內存緩存即可,這種問題在 TimeProfiler 裏時間段選長一些往往就能看出來。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"鎖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鎖之所以會影響啓動時間,是因爲有時候子線程先持有了鎖,"},{"type":"text","marks":[{"type":"strong"}],"text":"主線程就需要等待子線程鎖釋放。還要警惕系統會有很多隱藏的全局鎖"},{"type":"text","text":",比如 dyld 和 Runtime。舉個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖是 "},{"type":"codeinline","content":[{"type":"text","text":"UIImage imageNamed"}]},{"type":"text","text":" 引起的主線程 block:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/40\/401cf0e99e4bc721071f361924313ec3.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過右側的堆棧能看到,imageNamed 觸發了 dlopen,dlopen 會等待 dyld 的全局鎖。"},{"type":"text","marks":[{"type":"strong"}],"text":"通過 System Trace 的 Thread State Event,可以找到線程被 blocked 的下一個事件"},{"type":"text","text":",這個事件表明了線程重新可以運行,原因就是其他線程釋放了鎖:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a6\/a650762b992f474082270947869751b8.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來通過分析後臺線程這個時間在做什麼,就知道爲什麼會持有鎖,如何優化了。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"線程數量"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線程的數量和優先級都會影響啓動時間。可以通過設置 QoS 來配置優先級,兩個高優的 QoS 是 User Interactive\/Initiated,啓動的時候,"},{"type":"text","marks":[{"type":"strong"}],"text":"需要主線程等待的子線程任務都應該設置成高優的"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"高優的線程數量不應該多於 CPU 核心數量"},{"type":"text","text":",可以通過 System Trace 的 System Load 來分析這種情況。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/GCD\ndispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, -1);\ndispatch_queue_t queue = dispatch_queue_create(\"com.custom.utility.queue\", attr);\n\/\/NSOperationQueue\noperationQueue.qualityOfService = NSQualityOfServiceUtility\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線程的數量也會影響啓動時間,但 iOS 中是不太好全局管控線程的,比如二\/三方庫要起後臺線程就不太好管控,不過業務上的線程可以通過啓動任務管控。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線程多沒關係,只要同時"},{"type":"text","marks":[{"type":"strong"}],"text":"併發執行的不多就好"},{"type":"text","text":",大家可以利用 System Trace 來看看上下文切換耗時,確認線程數量是否是啓動的瓶頸。"}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"圖片"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動難免會用到很多圖,有沒有辦法優化圖片加載的耗時呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"用 Asset 管理圖片而不是直接放在 bundle 裏"},{"type":"text","text":"。Asset 會在編譯期做優化,讓加載的時候更快,此外在 Asset 中加載圖片是要比 Bundle 快的,因爲 UIImage imageNamed 要遍歷 Bundle 才能找到圖。"},{"type":"text","marks":[{"type":"strong"}],"text":"加載 Asset 中圖的耗時主要在在第一次張圖,因爲要建立索引"},{"type":"text","text":",可以通過把啓動的圖放到一個小的 Asset 裏來減少這部分耗時。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每次創建 UIImage 都需要 IO,在首幀渲染的時候會解碼。所以可以通過提前子線程預加載(創建 UIImage)來優化這部分耗時。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下圖,啓動只有到了比較晚的階段“RootWindow 創建”和“首幀渲染”纔會用到圖片,"},{"type":"text","marks":[{"type":"strong"}],"text":"所以可以在啓動的早期開預加載的子線程啓動任務"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/de\/de59949e75227977f9c0bbeaf31e7d6a.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Fishhook"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"fishhook 是一個用來 hook C 函數的庫,但這個庫的第一次調用耗時很高,最好"},{"type":"text","marks":[{"type":"strong"}],"text":"不要帶到線上"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Fishhook 是按照下圖的方式遍歷 Mach-O 的多個段來找函數指針和函數符號名的映射關係,帶來的"},{"type":"text","marks":[{"type":"strong"}],"text":"副作用就是要大量的 Page In,對於大型 App 來說在 iPhone X 冷啓耗時 200ms+。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ff\/ff06d8e01166bf7d05d88f183969a535.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果不得不用 fishhook,"},{"type":"text","marks":[{"type":"strong"}],"text":"請在子線程調用,且不要在在"},{"type":"codeinline","content":[{"type":"text","text":"_dyld_register_func_for_add_image"}],"marks":[{"type":"strong"}]},{"type":"text","marks":[{"type":"strong"}],"text":"直接調用 fishhook"},{"type":"text","text":"。因爲這個方法會持有 dyld 的一個全局互斥鎖,主線程在啓動的時候系統庫經常會調用 "},{"type":"codeinline","content":[{"type":"text","text":"dlsym"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"dlopen"}]},{"type":"text","text":",其內部也需要這個鎖,造成上文提到的子線程阻塞主線程。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"首幀渲染"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不同 App 的業務形態不同,首幀渲染優化方式也相差的比較多,幾個常見的優化點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LottieView:lottie 是 airbnb 用來做 AE 動畫的庫,但是加載動畫的 json 和讀圖是比較慢的,可以"},{"type":"text","marks":[{"type":"strong"}],"text":"先顯示一幀靜態圖,啓動結束後再開始動畫,或者子線程預先把圖和 json 設置到 lottie cache 裏"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Lazy 初始化 View:"},{"type":"text","marks":[{"type":"strong"}],"text":"不要先創建設置成 hidden,這是很不好的習慣"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AutoLayout:AutoLayout 的耗時也是比較高的,但這塊往往歷史包袱比較重,可以"},{"type":"text","marks":[{"type":"strong"}],"text":"評估 ROI 看看要不要改成 frame"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Loading 動畫:App 一般都會有個 loading 動畫表示加載中,這個"},{"type":"text","marks":[{"type":"strong"}],"text":"動畫最好不要用 gif"},{"type":"text","text":",線下測量一個 60 幀的 gif 加載耗時接近 70ms"}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"其他 Tips"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動優化裏有一些需要注意的 Tips:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"不要刪除"},{"type":"codeinline","content":[{"type":"text","text":"tmp\/com.apple.dyld"}],"marks":[{"type":"strong"}]},{"type":"text","marks":[{"type":"strong"}],"text":"目錄"},{"type":"text","text":",因爲這個目錄下存儲着 iOS 13+的啓動閉包,如果刪除了下次啓動會重新創建,創建閉包的過程是很慢的。接下來是 IO 優化,常見的方式是用 "},{"type":"codeinline","content":[{"type":"text","text":"mmap"}]},{"type":"text","text":" 讓 IO 更快一些,也可以在啓動的早期預加載數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有一些 iPhone 6 上耗時會明顯增加的點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebView User Agent:第一次在啓動時獲取,之後緩存,每次啓動結束後刷新"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"KeyChain:可以延遲獲取或者預加載"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"VolumeView:建議直接刪掉"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"iPhone 6 是個分水嶺,性能會斷崖式下跌,可以在 iPhone 6 上下掉部分用戶交互來換取核心體驗(記得 AB 驗證)"},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Page In 耗時"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動路徑上會觸發大量 Page In,有沒有辦法優化這部分耗時呢?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"段重命名"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"App Store 會對上傳的 App 的 TEXT 段加密,在發生 Page In 的時候會解密,解密的過程是很耗時的。既然會 TEXT 段加密,那麼直接的思路就是把 TEXT 段中的內容移動到其它段,ld 也有個參數 "},{"type":"codeinline","content":[{"type":"text","text":"rename_section"}]},{"type":"text","text":" 支持重命名:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b4\/b4d65ad298147c9b8700ba31b0c4f354.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"抖音重命名方案:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\"-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring\",\n\"-Wl,-rename_section,__TEXT,__const,__RODATA,__const\",\n\"-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab\",\n\"-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname\",\n\"-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname\",\n\"-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype\"\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個優化方式在 iOS 13 下有效,因爲 "},{"type":"text","marks":[{"type":"strong"}],"text":"iOS 13 優化了解密流程,Page In 的時候不需要解密了"},{"type":"text","text":",這是 iOS 13 啓動速度變快的原因之一。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"二進制重排"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然啓動的路徑上會觸發大量的 Page In,那麼有沒有什麼辦法優化呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動具有局部性特徵,即只有少部分函數在啓動的時候用到,這些函數在中的分佈是零散的,所以 Page In 讀入的數據利用率並不高。如果我們可以把啓動用到的函數排列到二進制的連續區間,那麼就可以減少 Page In 的次數,從而優化啓動時間:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下圖爲例,方法 1 和方法 3 是啓動的時候用到的,爲了執行對應的代碼,就需要兩次 Page In。假如我們把方法 1 和 3 排列到一起,那麼只需要一次 Page In,從而提升啓動速度。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/6c\/6cbb077d6599ffd6a3f355baa6067c4d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈接器 ld 有個參數-order_file 支持按照符號的方式排列二進制。獲取啓動時候用到的符號主流有兩種方式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"抖音方案:靜態掃描獲取 +load 和 C++靜態初始化,hook objc_msgSend 獲取 Objective C 符號。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Facebook 方案:LLVM 函數插樁,灰度統計啓動路徑符號,用大多數用戶的符號生成 order_file。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Facebook 的 LLVM 函數插樁是針對 order_file 定製,並且代碼也是他們自己給 LLVM 開發的,目前已經合併到 LLVM 主分支了。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/46\/464885b7e4254e7ea0ccafb842f3c4a8.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Facebook 的方案更精細化,生成的 order_file 是最優解,但是工程量很大。抖音的方案"},{"type":"text","marks":[{"type":"strong"}],"text":"不需要源碼編譯"},{"type":"text","text":","},{"type":"text","marks":[{"type":"strong"}],"text":"不需要對現有編譯環境和流程改造"},{"type":"text","text":",侵入性最小,缺點就是隻能覆蓋 90%左右的符號。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"- 灰度是任何優化都要利用好的一個階段,因爲很多新的優化方案存在不確定性,需要先在灰度上驗證。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"非常規方案"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"動態庫懶加載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最開始我們提到可以通過刪代碼的方式來減少代碼量,那麼有沒有什麼不減少代碼總量,就可以減少啓動時候要加載代碼數量的方式呢?"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答案就是動態庫懶加載。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"什麼是懶加載的動態庫呢?正常動態庫都是會被主二進制直接或者間接鏈接的,那麼這些動態庫會在啓動的時候加載。"},{"type":"text","marks":[{"type":"strong"}],"text":"如果只打包進 App,不參與鏈接,那麼啓動的時候就不會自動加載,在運行時需要用到動態庫裏面的內容的時候,再手動懶加載"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"懶加載動態庫需要在編譯期和運行時都進行改造,編譯期的架構:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a5\/a531fe11744a319ba5c1dec1c32c7d4d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"像 A.framework 等動態庫是懶加載的,因爲並沒有參與主二進制的直接 or 間接鏈接。動態庫之間一定會有一些共同的依賴,把這些依賴打包成 Shared.framework 解決公共依賴的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行時通過"},{"type":"codeinline","content":[{"type":"text","text":"-[NSBundle load]"}]},{"type":"text","text":"來加載,本質上調用的是底層的 "},{"type":"codeinline","content":[{"type":"text","text":"dlopen"}]},{"type":"text","text":"。那麼什麼時候觸發動態庫手動加載呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"動態庫可以分成兩種:業務和功能。"},{"type":"text","marks":[{"type":"strong"}],"text":"業務就是 UI 的入口,可以把動態庫加載的邏輯收斂到路由內部,這樣外部其實並不知道動態庫是懶加載的,也能更好地容錯"},{"type":"text","text":"。功能庫(比如上圖的 QR.framework)會有些不一樣,因爲沒有 UI 等入口,需要功能庫自己維護 Wrapper:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b1\/b10bc32136ddceb8bf1451fdfa67c7ee.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"App 對 Wrapper 直接依賴,這樣外部並不知道這個動態庫是懶加載的"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Wrapper 內部封裝了動態調用邏輯,動態調用指的是通過 dlsym 等方式調用"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"動態庫懶加載除了啓動加載的代碼減少,還能長期防止業務增加代碼引起啓動劣化,因爲業務的初始化在第一次訪問的時候完成的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個方案還有其他優點,比如動態庫化後本地編譯時間會大幅度降低,對其他性能指標也有好處,缺點是會犧牲一定程度的包大小,但可以用段壓縮等方式優化懶加載的動態庫來打平這部分損耗。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Background Fetch"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Background Fetch 可以隔一段時間把 App 在後臺啓動,對於時間敏感的 App(比如新聞)可以在後臺刷新數據,這樣能夠提高 Feed 加載的速度,進而提升用戶體驗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,這種類似“後臺保活”的機制,爲什麼能提高啓動速度呢?我們來看一個典型的 case:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/56\/562300f16c923df6515a3678e4af225c.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"系統在後臺啓動 App"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"時間長因爲內存等原因,後臺的 App 被 kill 了"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"這時候用戶立刻啓動 App,那麼這次啓動就是一次"},{"type":"text","marks":[{"type":"strong"}],"text":"熱啓動"},{"type":"text","text":",因爲緩存還在"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"又一次系統在後臺啓動 App"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"這次用戶在 App 在後臺的時候點了 App,那麼這次啓動就是一次"},{"type":"text","marks":[{"type":"strong"}],"text":"後臺回前臺"},{"type":"text","text":",因爲 App 仍然活着"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過這兩個典型的場景,可以看出來爲什麼 Background Fetch 能提高啓動速度了:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"提高熱啓動在冷啓動的佔比"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"後臺啓動回前臺被定義爲啓動,因爲用戶的角度來說這就是一次啓動"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後臺啓動有一些要注意的點,"},{"type":"text","marks":[{"type":"strong"}],"text":"比如日活,廣告,甚至是 AB 進組邏輯都會受影響"},{"type":"text","text":",需要做不少適配。往往需要啓動器來支撐,因爲正常啓動在 didFinishLaunch 執行的任務,在後臺啓動的時候需要延遲到第一次回前臺的時候再執行。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後提煉出幾點我們認爲在任何優化中都重要的:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"白盒優化"},{"type":"text","text":",知道爲什麼慢,優化的是哪部分。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"線上數據都是優化的指南針"},{"type":"text","text":",也是衡量優化效果的唯一方式,建議開 AB 實驗,驗證對業務上的影響。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"不要忽略防劣化的建設"},{"type":"text","text":",尤其是業務迭代迅速的團隊,否則很有可能優化的速度趕不上劣化。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"做長期的架構建設"},{"type":"text","text":",良好的架構會長期爲啓動這些基礎性能保駕護航。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:toutiaotechblog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/ekXfFu4-rmZpHwzFuKiLXw","title":"xxx","type":null},"content":[{"type":"text","text":"抖音品質建設 - iOS啓動優化《實戰篇》"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章