iOS 進階知識總結(三)
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
顯示在螢幕上的時候需要賦值iamge
。UIImage
持有的資料是未解碼的壓縮資料,賦值的時候影象資料會被解碼變成RGB
顏色資料,最終渲染到螢幕上。
說說渲染流程
- 1、確定頂點位置:
CPU
確定繪製圖形的位置,從iOS
座標系換算成螢幕座標系。 - 2、圖元裝配:確定頂點間的連線關係。
- 3、光柵化:就是把展示用到的畫素點摘出來。
- 4、著色器處理:
GPU
計算畫素點展示的顏色,並存入緩衝區。 - 5、螢幕展示
什麼是離屏渲染
- 普通渲染流程:APP - 幀緩衝區 - 展示
- 離屏渲染流程:APP - 離屏渲染緩衝區 - 幀緩衝區 - 展示
- 離屏渲染,是無法一次性處理渲染,需要分部處理並存儲中間結果引起的。
- 所以判斷是否出現離屏渲染的根本條件就是判斷渲染是否需要分部處理~
- 需要分步處理,會產生離屏渲染
- 一次性渲染,不產生離屏渲染
離屏渲染的影響
- 需要分幾步就需要開闢出幾個離屏渲染緩衝區儲存中間結果,造成空間浪費。
- 最後合併多個離屏渲染緩衝區才能展示結果,會影響效能。
什麼操作會觸發離屏渲染?
- 光柵化
layer.shouldRasterize = YES
- 遮罩
layer.mask
- 圓角
layer.maskToBounds = Yes
,Layer.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
時候會呼叫- 改變一個
view
的frame
的時候呼叫 - 滾動
UIScrollView
導致UIView
重新佈局的時候會呼叫 - 手動呼叫
setNeedsLayout
或者layoutIfNeeded
setNeedsLayout
和layoutIfNeeded
的區別
setNeedsLayout
標記為需要重新佈局- 非同步呼叫
layoutIfNeeded
重新整理佈局,不立即重新整理,在下一輪runloop
結束前重新整理。 - 對於這一輪
runloop
之內的所有佈局和UI更新
只會重新整理一次,layoutSubviews
一定會被呼叫。
- 非同步呼叫
layoutIfNeeded
- 如果有需要重新整理的標記,立即呼叫
layoutSubviews
進行佈局 - 如果沒有標記,不會呼叫
layoutSubviews
- 如果想在當前
runloop
中立即重新整理,呼叫順序應該是[self setNeedsLayout]; [self layoutIfNeeded];
- 如果有需要重新整理的標記,立即呼叫
drawRect
呼叫時機
drawRect
在loadView
和ViewDidLoad
之後呼叫
UIView
和CALayer
是什麼關係?
View
可以響應並處理使用者事件,CALayer
不可以。- 每個
UIView
內部都有一個CALayer
提供尺寸樣式(模型樹),進行繪製和顯示。 - 兩者都有樹狀層級結構,
layer
內部有subLayers
,view
內部有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
- 文字
CATextLayer
和UILabel
- 截圖
-renderInContext:
- 可伸縮圖片
- 混合圖層
- 物件回收
- 離屏渲染
- 光柵化
shouldRasterize
- 陰影效果
shadowPath
- 圓角裁切
cornerRadius
- 遮罩
- 光柵化
UITableVIew
優化
- 重用機制(快取池)
- 少用有透明度的
View
- 儘量避免使用
xib
- 儘量避免過多的層級結構
- iOS8以後出的預估高度
- 減少離屏渲染操作(圓角、陰影)
- 快取
cell
的高度(提前計算好cell
的高度,快取進當前的模型裡面) - 非同步繪製
- 滑動的時候,按需載入
- 儘量少
add、remove
子控制元件,最好通過hidden
控制顯示
imageName
與imageWithContentsOfFile
區別?
imageWithContentsOfFile
:載入本地目錄圖片,不能載入image.xcassets
裡面的圖片資源。不快取佔用記憶體小,相同的圖片會被重複載入到記憶體中。不需要重複讀取的時候使用。imageNamed
:可以讀取image.xcassets
的圖片資源,載入到記憶體快取起來,佔用記憶體較大,相同的圖片不會被重複載入。直到收到記憶體警告的時候才會釋放不使用的UIImage
。需要重複讀取同一個圖片的時候用。
IBOutlet
連出來的檢視屬性為什麼可以被設定成weak
?
- 因為
Xcode
內部把連結的控制元件放進了一個_topLevelObjectsToKeepAliveFromStoryboard
的私有陣列中,這個陣列強引用這所有topLevel
的物件,所以用weak
也無傷大雅。
UIScrollerView
實現原理
- 滾動其實是在修改原點座標。當手指觸控後,
scrollview
攔截觸控事件建立一個計時器。 - 如果計時器到點後沒有發生手指移動事件,
scrollview
傳送tracking events
到被點選的subview
。 - 如果計時器到點前發生了移動事件,
scrollview
取消tracking
自己滾動。
如何實現檢視的變形?
- 修改
view
的transform
。
事件響應鏈和事件傳遞
什麼是響應鏈
- 由連結在一起的響應者(
UIResponse
及其子類)組成的鏈式關係。 - 最先的響應者稱為第一響應者
- 最底層的響應者是
UIApplication
寫出一個響應鏈
subView -> view -> superView -> viewController -> window -> application
什麼是事件傳遞
- 觸發事件後,事件從第一響應者到
application
的傳遞過程
事件傳遞的過程
- 當程式中發生觸控事件之後,系統會將事件新增到
UIApplication
管理的一個隊列當中 application
將任務佇列的首個任務向下分發application -> window -> viewController -> view
view
需要滿足條件才可以處理任務,透明度>0.01、觸控在view
的區域內、userInteractionEnabled=YES
、hidden=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 IR
。OC
會在這裡進行runtime
的橋接:property
的合成、ARC
處理等
- 2-1、預處理後會進行詞法分析,詞法分析會把程式碼切片成一個個
- 3、編譯器後端:通過一個個
Pass
去優化,每個Pass
完成一部分功能,最終生成彙編程式碼- 3-1、蘋果對程式碼做了進一步優化,並且通過
.ll
檔案生成.bc
檔案。 - 3-2、可以通過
.bc
或.ll
檔案生成彙編程式碼
- 3-1、蘋果對程式碼做了進一步優化,並且通過
- 4、生成,
.o
格式的目標檔案 - 5、連結動態庫和靜態庫
- 6、通過不同架構,生成對應的可執行檔案
MachO
APP啟動過程
- 1、載入可執行檔案(讀取
Mach-O
) - 2、載入動態庫(
Dylib
) - 3、
Rebase & Bind
- 3-1、
Rebase
的作用是修正ASLR
的偏移,把當前MachO
的指標指向正確的記憶體 - 3-2、
Bind
的作用是重新修復外部方法指標的指向,fishhook
原理
- 3-1、
- 4、
objc_init
,載入類和分類 - 5、
Initializers
,呼叫load
方法,初始化C & C++
的物件等 - 6、
main()
函式 - 7、執行
AppDelegate
的代理方法(如:didFinishLaunchingWithOptions
)。 - 8、初始化
Windows
,初始化ViewController
。
dyld
做了什麼
- 1、
dyld
讀取Mach-O
的Header
和Load Commands
- 2、找可執行檔案的依賴動態庫,將依賴的動態庫載入到記憶體中。這是一個遞迴的過程,依賴的動態庫可能還會依賴別的動態庫,所以
dyld
會遞迴每個動態庫,直至所有的依賴庫都被載入完畢。