iOS 進階知識總結(三)

語言: CN / TW / HK

3~5年開發經驗的 iOS工程師 應該知道的內容~本文總結以下內容 - 檢視渲染和離屏渲染 - 事件傳遞和響應鏈 - crash處理和效能優化 - 編譯流程和啟動流程

導航

iOS 進階知識總結(一) - 物件 - 類物件 - 分類 - runtime - 訊息與訊息轉發

iOS 進階知識總結(二) - KVO - KVC - 多執行緒 - 鎖 - runloop - 計時器

iOS 進階知識總結(三) - 檢視渲染和離屏渲染 - 事件傳遞和響應鏈 - crash處理和效能優化 - 編譯流程和啟動流程

iOS 進階知識總結(四) - 記憶體管理 - 野指標處理 - autoreleasePool - weak - 單例、通知、block、繼承和集合

iOS 進階知識總結(五) - 網路基礎 - AFNetWorking - SDWebImage

渲染

螢幕撕裂的原因?

  • 單一快取模式下,幀緩衝區只有一個快取空間
  • 圖片需要經過CPU -> 記憶體 -> GPU -> 展示 的渲染過程
  • CPU和GPU的協作過程中出現了偏差,GPU應該完整的繪製圖片,但是工作慢了只繪製出圖片上半部分。
  • 此時CPU又把新資料儲存到緩衝區,GPU繼續繪製的時候下半部分就變成了新資料。
  • 造成了兩幀同時出現了一部分在螢幕上,看起來就撕裂了。

怎麼解決螢幕撕裂?

  • 解決上一幀和下一幀的覆蓋問題,需要使用不同的緩衝區,通過兩個圖形緩衝區的交替來解決。
  • 出現速度差的時候,就把下一幀儲存在後備緩衝區,繪製完成後再切換幀。
  • 當繪製完最後一個畫素點就會發出這個垂直同步訊號通知展示完成。
  • 所以螢幕撕裂問題需要通過 雙緩衝區 + 垂直同步訊號 解決。

掉幀是怎麼產生的?

  • 螢幕正在展示A幀的時候,CPU和GPU會處理B幀。
  • A幀展示完成該切換展示B幀的時候B幀的資料未準備好。
  • 沒辦法切換就只能重複展示A幀,感官上就是卡了,這就是掉幀的問題

怎麼解決掉幀?

  • 掉幀根本原因是CPU和GPU渲染計算耗時過長
  • 1、降低檢視層級
  • 2、提前或減少在渲染期的計算

CPU渲染職能

  • 佈局計算:如果檢視層級過於複雜,呈現或修改的時候需要消耗大量時間計算
  • 檢視懶載入:當顯示的時候才會載入檢視,這對記憶體使用和程式啟動時間很有好處。但展示前的操作都不會被及時響應,當檢視從xib載入或者涉及圖片顯示,懶載入都會比正常載入慢。
  • 解壓圖片PNG或者JPEG壓縮之後的圖片會比點陣圖小得多。在繪製到螢幕之前,必須把它擴充套件成完整的尺寸(通常等同於圖片寬 x 長 x 4個位元組)。為了節省記憶體,真正繪製的時候(第一次賦值UIImageView或者把它繪製到Core Graphics)才會解碼圖片,大圖會佔用一定的時間。
  • Core Graphics繪製:實現了drawRect:drawLayer:inContext:CALayerDelegate 的方法,在繪製前都會產生一個巨大的效能開銷以開闢繪畫上下文。
  • 圖層打包:由於GPU不知道圖層結構,CPU需要通過OpenGL把渲染樹中的每個可見圖層轉換成紋理三角板。CPU的工作量和圖層量成正比,如果層級關係太複雜就會渲染速度。

GPU渲染職能

  • GPU會根據生成的前後幀快取資料,根據實際情況進行合成
  • 造成GPU渲染負擔:離屏渲染,圖層混合,延遲載入

一個UIImageView新增到檢視上以後,內部如何渲染到手機上的?

  • 圖片顯示分為三個步驟: 載入、解碼、渲染
  • 通常,我們程式設計師的操作只是載入,至於解碼和渲染是由UIKit內部進行的。
  • 例如:UIImageView顯示在螢幕上的時候需要賦值iamgeUIImage持有的資料是未解碼的壓縮資料,賦值的時候影象資料會被解碼變成RGB顏色資料,最終渲染到螢幕上。

說說渲染流程

  • 1、確定頂點位置:CPU確定繪製圖形的位置,從iOS座標系換算成螢幕座標系。
  • 2、圖元裝配:確定頂點間的連線關係。
  • 3、光柵化:就是把展示用到的畫素點摘出來。
  • 4、著色器處理:GPU計算畫素點展示的顏色,並存入緩衝區。
  • 5、螢幕展示

什麼是離屏渲染

  • 普通渲染流程:APP - 幀緩衝區 - 展示
  • 離屏渲染流程:APP - 離屏渲染緩衝區 - 幀緩衝區 - 展示
  • 離屏渲染,是無法一次性處理渲染,需要分部處理並存儲中間結果引起的。
  • 所以判斷是否出現離屏渲染的根本條件就是判斷渲染是否需要分部處理~
    • 需要分步處理,會產生離屏渲染
    • 一次性渲染,不產生離屏渲染

離屏渲染的影響

  • 需要分幾步就需要開闢出幾個離屏渲染緩衝區儲存中間結果,造成空間浪費。
  • 最後合併多個離屏渲染緩衝區才能展示結果,會影響效能。

什麼操作會觸發離屏渲染?

  • 光柵化 layer.shouldRasterize = YES
  • 遮罩layer.mask
  • 圓角layer.maskToBounds = YesLayer.cornerRadis
  • 陰影layer.shadowXXX

檢視

AutoLayout的原理,效能如何

  • Auto Layout 只關注檢視之間的關係,通過佈局引擎和已有的約束計算出各個檢視的frame
  • 每當約束改變時會重新計算各個檢視的frame
  • 獲得frame的過程,就是根據各個檢視已有的約束條件解方程式的過程。
  • 效能會隨著檢視數量的增加呈指數級增加
  • 達到一定數量的檢視時,佈局所需要的時間就會大於16.67ms,超過螢幕的重新整理頻率時會出現卡頓。

你認為自動佈局怎麼實現的

  • 原理是線性公式,使用了系統提供的NSLayoutConstraint
  • Masonry基於它封裝

ViewController生命週期

  • initWithCoder:通過nib檔案初始化時觸發。
  • awakeFromNib:nib檔案被載入的時候,會發生一個awakeFromNib的訊息到nib檔案中的每個物件。
  • loadView:開始載入檢視控制器自帶的view
  • viewDidLoad:檢視控制器的view被載入完成。
  • viewWillAppear:檢視控制器的view將要顯示在window上。
  • updateViewConstrains:檢視控制器的view開始更新AutoLayout約束。
  • viewWillLayoutSubviews:檢視控制器的view將要更新內容檢視的位置。
  • viewDidLayoutSubviews:檢視控制器的view已經更新檢視的位置。
  • viewDidAppear:檢視控制器的view已經展示到window上。
  • viewWillDisappear:檢視控制器的view將要從window上消失。
  • viewDidDisappear:檢視控制器的view已經從window上消失。

LayoutSubviews呼叫時機

  • init 初始化不會呼叫 LayoutSubviews 方法
  • addsubView 時候會呼叫
  • 改變一個 viewframe 的時候呼叫
  • 滾動 UIScrollView 導致 UIView 重新佈局的時候會呼叫
  • 手動呼叫 setNeedsLayout 或者 layoutIfNeeded

setNeedsLayoutlayoutIfNeeded的區別

  • setNeedsLayout 標記為需要重新佈局
    • 非同步呼叫layoutIfNeeded重新整理佈局,不立即重新整理,在下一輪runloop結束前重新整理。
    • 對於這一輪runloop之內的所有佈局和UI更新只會重新整理一次,layoutSubviews一定會被呼叫。
  • layoutIfNeeded
    • 如果有需要重新整理的標記,立即呼叫layoutSubviews進行佈局
    • 如果沒有標記,不會呼叫layoutSubviews
    • 如果想在當前runloop中立即重新整理,呼叫順序應該是 [self setNeedsLayout]; [self layoutIfNeeded];

drawRect呼叫時機

  • drawRectloadViewViewDidLoad之後呼叫

UIViewCALayer是什麼關係?

  • View可以響應並處理使用者事件,CALayer 不可以。
  • 每個 UIView 內部都有一個 CALayer 提供尺寸樣式(模型樹),進行繪製和顯示。
  • 兩者都有樹狀層級結構,layer 內部有 subLayersview 內部有 subViews
  • CALayer是支援隱式動畫的,View 作為Layer的代理,通過 actionForLayer:forKey:Layer提交相應的動畫
  • layer 內部維護著三分 layer tree
    • 動畫樹presentLayer Tree,修改動畫的屬性改的是動畫樹的屬性值
    • 模型樹modeLayer Tree,最終展示在介面上的其實是提供檢視的模型樹
    • 渲染樹render Tree

UIView顯示原理

  • UIView 可以顯示是因為內部有一個layer作為根圖層,根圖層上可以放其他子圖層。
  • UIView 中所有能夠看到的內容都包含在layer
  • UIView需要顯示到螢幕上會呼叫drawRect:方法進行繪圖,並且會將所有內容繪製在自己的layer
  • 繪圖完畢後,系統將圖層展示到螢幕上,完成了UIView的顯示。

UIView顯示過程

  • view.layer建立一個圖層型別的上下文(Layer Graphics Contex
  • 觸發代理方法drawLayer:inContext:,傳入剛才準備好的上下文
  • drawLayer:inContext:內部會讓view呼叫drawRect:方法繪圖
  • 開發者在drawRect:方法中實現繪圖程式碼,內容最終繪製到view.layer
  • 系統將view.layer展示到螢幕,完成了view的顯示

UITableView的重用機制?

  • UITableView 通過重用單元格來節省記憶體
  • 為每個單元格指定一個重用識別符號,即指定了單元格的種類
  • 當螢幕上的單元格滑出螢幕時,系統會把這個單元格新增到重用佇列中,等待被重用。
  • 當有新單元格從螢幕外滑入螢幕內時,從重用佇列查詢可重用的單元格,如果有就拿來用,如果沒有就建立一個使用。

UITableView卡頓的的原因有哪些?

  • 隱式繪製 CGContext
  • 繪製 Core Graphics
  • 文字 CATextLayerUILabel
  • 截圖 -renderInContext:
  • 可伸縮圖片
  • 混合圖層
  • 物件回收
  • 離屏渲染
    • 光柵化 shouldRasterize
    • 陰影效果 shadowPath
    • 圓角裁切 cornerRadius
    • 遮罩

UITableVIew優化

  • 重用機制(快取池)
  • 少用有透明度的View
  • 儘量避免使用xib
  • 儘量避免過多的層級結構
  • iOS8以後出的預估高度
  • 減少離屏渲染操作(圓角、陰影)
  • 快取cell的高度(提前計算好cell的高度,快取進當前的模型裡面)
  • 非同步繪製
  • 滑動的時候,按需載入
  • 儘量少add、remove 子控制元件,最好通過hidden控制顯示

imageNameimageWithContentsOfFile區別?

  • imageWithContentsOfFile:載入本地目錄圖片,不能載入image.xcassets裡面的圖片資源。不快取佔用記憶體小,相同的圖片會被重複載入到記憶體中。不需要重複讀取的時候使用。
  • imageNamed:可以讀取image.xcassets的圖片資源,載入到記憶體快取起來,佔用記憶體較大,相同的圖片不會被重複載入。直到收到記憶體警告的時候才會釋放不使用的UIImage。需要重複讀取同一個圖片的時候用。

IBOutlet連出來的檢視屬性為什麼可以被設定成weak?

  • 因為Xcode內部把連結的控制元件放進了一個_topLevelObjectsToKeepAliveFromStoryboard的私有陣列中,這個陣列強引用這所有topLevel的物件,所以用weak也無傷大雅。

UIScrollerView實現原理

  • 滾動其實是在修改原點座標。當手指觸控後,scrollview攔截觸控事件建立一個計時器。
  • 如果計時器到點後沒有發生手指移動事件,scrollview 傳送 tracking events 到被點選的 subview
  • 如果計時器到點前發生了移動事件, scrollview 取消 tracking 自己滾動。

如何實現檢視的變形?

  • 修改viewtransform

事件響應鏈和事件傳遞

什麼是響應鏈

  • 由連結在一起的響應者(UIResponse及其子類)組成的鏈式關係。
  • 最先的響應者稱為第一響應者
  • 最底層的響應者是UIApplication

寫出一個響應鏈

subView -> view -> superView -> viewController -> window -> application

什麼是事件傳遞

  • 觸發事件後,事件從第一響應者到application的傳遞過程

事件傳遞的過程

  • 當程式中發生觸控事件之後,系統會將事件新增到UIApplication管理的一個隊列當中
  • application將任務佇列的首個任務向下分發
  • application -> window -> viewController -> view
  • view需要滿足條件才可以處理任務,透明度>0.01、觸控在view的區域內、userInteractionEnabled=YEShidden=NO
  • 滿足條件的view遍歷自身的subViews,判斷是否滿足上述條件
  • 如果所有subView都無法滿足條件,那麼最佳響應者就是自己。
  • 如果沒有任何一個view能處理事件,事件會被廢棄。

找出觸控的View

// 返回的View是本次點選的最佳響應者 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event ​ // 判斷點是否落在某區域 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

重寫hitTest:withEvent

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {   // 1.判斷當前控制元件能否接收事件   if (self.userInteractionEnabled == NO ||       self.hidden == YES ||       self.alpha <= 0.01) {       return nil;   }   // 2. 判斷點在不在當前控制元件   if ([self pointInside:point withEvent:event] == NO) {       return nil;   }   // 3.從後往前遍歷自己的子控制元件   NSInteger count = self.subviews.count;   for (NSInteger i = count - 1; i >= 0; i--) {     UIView *childView = self.subviews[I];     // 把當前控制元件上的座標系轉換成子控制元件上的座標系     CGPoint childP = [self convertPoint:point toView:childView];     UIView *fitView = [childView hitTest:childP withEvent:event];       if (fitView) { // 尋找到最合適的view           return fitView;       }   }   // 迴圈結束 沒有比自己更合適的view   return self; }

Crash

常見Crash的原因有哪些?

  • 1、找不到方法的實現 unrecognized selector sent to instance
  • 2、KVC造成的crash
  • 3、KVO造成的crash
  • 4、EXC_BAD_ACCESS
  • 5、集合類相關崩潰,越界等
  • 6、多執行緒中的崩潰,使用了被釋放的物件
  • 7、後臺返回錯誤的資料結構

BAD_ACCESS在什麼情況下出現?

  • 訪問已經釋放物件的成員變數
  • 給已經釋放物件發訊息
  • 死迴圈

不使用第三方,排查閃退問題?

  • 1、使用NSSetUncaughtExceptionHandler統計閃退的資訊
  • 2、將統計到的資訊發給後臺
  • 3、在後臺收集資訊,進行排查 ``` static void my_uncaught_exception_handler (NSException exception) {   //獲取NSException 資訊   NSLog(@"*******");   NSLog(@"%@",exception);   NSLog(@"%@",exception.callStackReturnAddresses);   NSLog(@"%@",exception.callStackSymbols);   NSLog(@"********"); }    
    • (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions {   NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);   return YES; } ```

效能優化

優化啟動時間

  • 啟動時間是使用者點選App圖示,到第一個介面展示的時間。
  • 啟動時間在小於400ms是最佳的,因為從點選圖示到顯示Launch Screen,到Launch Screen消失這段時間是400ms。
  • 啟動時間不可以大於20s,否則會被系統殺掉。

main函式作為分水嶺,啟動時間其實包括了兩部分:

mian函式之前的啟動優化 - 減少或合併動態庫(這是目前為止最耗時的了, 基本上佔了95%以上的時間) - 確認動態庫是optional or required。如果該Framework在支援的所有iOS系統版本都存在,那麼就設為required,否則就設為optional,因為optional會有些額外的檢查

mian函式之後的啟動優化 - 1、合併和刪減不必要的類或者分類 - 2、將不必需在+load方法中做的事情,延時放到+initialize。 - 3、非必要的 SDK 和配置事件可以放在第一個介面處理 - 4、減少建立執行緒,執行緒建立和執行需要多次的上下文切換所帶來的開銷,平均消耗大約在 29 毫秒。這是很大的時間開銷,應該避免濫用。 - 5、編譯器插樁獲取方法符號,生成order file設定到xcode。減少頁中斷帶來的耗時。

網路優化

  • IP直連,將我們原本的請求連結www.baidu.com 修改為 180.149.132.47
  • 運營商在拿到請求後發現是IP地址會直接放行,而不會去走DNS解析
  • 不走他的DNS解析也就不會存在DNS被劫持的問題
  • 實現方法1:接使用HTTPDNS等sdk
  • 實現方法2:服務端下發發域名-IP對應列表,客戶端快取,通過快取IP來進行業務訪問。

包體積優化

  • 1、刪除陳舊程式碼、刪除陳舊xib/sb ,刪除無用的資原始檔(檢測未使用圖片的工具LSUnusedResources
  • 2、圖片、音影片資源壓縮後使用。
  • 3、動圖可以使用webP格式,載入速度比較慢,但體積小
  • 4、能用動態庫的儘量用動態庫,一般情況靜態庫的體積會比動態庫大
  • 5、主題類的資原始檔提供按需下載功能,不直接打包在應用包裡面
  • 6、App Slicing,應用程式切片,只打包下載機型所需的資原始檔,不需要開發者處理
  • 7、Bitcode,中間程式碼, 蘋果會對可執行檔案進行優化,不需要開發者處理
  • 8、ODR,On Demand Resources,按需載入資源,需要開發者處理

電量優化

  • 1.定位,儘量不要實時更新,可以適當降低精度
  • 2.網路請求,能用快取的時候儘量使用快取,降低請求的頻率,減少請求的次數,優化傳輸結構
  • 3.CPU處理,需要複用的資料能快取的資料儘量快取,優化演算法和資料結構
  • 4.GPU處理,減少離屏渲染

短影片/直播優化

  • 多播放器,像tableview那樣維護快取池。多播放器,才能做預載入。
  • 邊下邊播,要實現下一個影片或者幾個影片能快速的播放起來,首先應該保證正在播放的短影片能順暢播放,邊下邊播任務優先順序應該高於預載入任務。沒有邊下邊播時才能執行預載入,邊下邊播任務進行時應當停止預載入。
  • 預載入,預載入的下載應該和邊下邊播區分開,手勢滑動到差不多出現的時候,開始播放預載入的那1m資料。滑動下一個的時候,就取消preload,播放那1m下載的資料,再繼續load。同時調起下一個preload準備下載。

短影片快取管理

  • 快取主要建立了三個目錄管理,分別為temp、media、trash目錄
  • 快取分為臨時快取和最終快取,當短影片資源未下載完時是放在temp、當影片資源快取完時移動到media,這樣分別存放便能方便讀取和管理兩種狀態的快取,
  • 所有需要刪除的快取檔案都移入trash,隨後再刪除以此來保證較高的刪除效率。所有檔案命名使用影片url的md5值保證唯一性。
  • 快取應該具有自動管理功能,預設配置下ShortMediaCache允許臨時快取最多儲存1天,最大100Mb,而最終快取則允許最多儲存2天最大200Mb,如果業務需要可以自定義ShortMediaCacheConfig配置實現。

一般是怎麼用Instruments的?

  • Instruments裡面工具很多,常用:
  • Time Profiler: 效能分析
  • Zombies:檢查是否訪問了殭屍物件,但是這個工具只能從上往下檢查,不智慧。
  • Allocations:用來檢查記憶體。
  • Leaks:檢查記憶體,看是否有記憶體洩露。

編譯 & 啟動

編譯連結流程

  • 0、輸入檔案:找到原始檔
  • 1、預處理:包括替換巨集和匯入標頭檔案
  • 2、編譯階段:詞法分析、語法分析、語義分析,最終生成IR
    • 2-1、預處理後會進行詞法分析,詞法分析會把程式碼切片成一個個token
    • 2-2、語法分析會驗證語法是否正確,在詞法分析的基礎上把單詞組合成各種語法短語,然後把節點組成抽象的語法樹
    • 2-3、程式碼生成器根據語法樹自頂向下生成LLVM IROC會在這裡進行runtime的橋接:property的合成、ARC處理等
  • 3、編譯器後端:通過一個個Pass去優化,每個Pass完成一部分功能,最終生成彙編程式碼
    • 3-1、蘋果對程式碼做了進一步優化,並且通過.ll檔案生成.bc檔案。
    • 3-2、可以通過.bc.ll檔案生成彙編程式碼
  • 4、生成,.o格式的目標檔案
  • 5、連結動態庫和靜態庫
  • 6、通過不同架構,生成對應的可執行檔案MachO

APP啟動過程

  • 1、載入可執行檔案(讀取Mach-O
  • 2、載入動態庫(Dylib
  • 3、Rebase & Bind
    • 3-1、Rebase的作用是修正ASLR的偏移,把當前MachO的指標指向正確的記憶體
    • 3-2、Bind的作用是重新修復外部方法指標的指向,fishhook原理
  • 4、objc_init,載入類和分類
  • 5、Initializers,呼叫load方法,初始化C & C++的物件等
  • 6、main()函式
  • 7、執行AppDelegate的代理方法(如:didFinishLaunchingWithOptions)。
  • 8、初始化Windows,初始化ViewController

dyld做了什麼

  • 1、dyld讀取Mach-OHeaderLoad Commands
  • 2、找可執行檔案的依賴動態庫,將依賴的動態庫載入到記憶體中。這是一個遞迴的過程,依賴的動態庫可能還會依賴別的動態庫,所以dyld會遞迴每個動態庫,直至所有的依賴庫都被載入完畢。