上週公眾號釋出的以下文章:
- Flutter中Hybrid的實現
- 不改程式碼,Link-Time Optimization提高iOS程式碼效率 + 彙編程式碼原理分析
- 關於 Block 的幾點思考
- YYAsyncLayer 原始碼剖析:非同步繪製
本期知識小集的主要內容包括:
- 為 Fabric MOD 一個卡頓檢測功能
- 關於定位的一個小知識點
- iOS 獲取裝置型號最新總結
- Storyboard 中的約束優先順序
- UIWindow 的顯示特性與常見操作方法小結
為 Fabric MOD 一個卡頓檢測功能
作者:hite和落雁
卡頓檢測系統,用於檢測 App 的主執行緒執行情況。在追求 N 個 9 奔潰之外,卡頓也是我們極其重要執行指標。
很遺憾,世界上最好的免費 APM 平臺 Fabric 卻沒有。而國內的 bugly、網易雲捕等,都提供了類似的功能。如下圖是雲捕的卡頓功能。
說起來卡頓檢測,技術原理很簡單,下面是來自 bugly 的 QA 裡的描述
iOS 卡頓檢查的依據是監控主執行緒 Runloop 的執行,觀察執行耗時是否超過預定閥值(預設閥值為3000ms) 在監控到卡頓時會立即記錄執行緒堆疊到本地,在App從後臺切換到前臺時,執行上報。
複製程式碼
卡頓檢測系統,這個大任務,可以分解為兩部分:卡頓的檢測 + 卡頓的展示和管理。
卡頓的收集
卡頓的收集,有現成的程式碼,核心程式碼可檢視,gist gist.github.com/hite/1a7ee4…
檢測到之後,需要獲取當前時刻的堆疊,所有執行緒的堆疊(其實只需要主執行緒就夠了)。
OK,拿來主義,一個很有名 PLCrashReporter
。我自己整合測試,在我的老古董機器,iPhone 6 跑,卡頓是能檢測到,但是整個軟體基本不可用,整個介面全卡住了。PLCrashReporter 生成日誌程式碼如下圖所示。
效能非常差,完全不可用。
拿不到堆疊資訊,無法展示,所以只能採用造輪子的方式。根據戴銘 blog 裡的例子,我改造了下,如 gist gist.github.com/hite/1a7ee4… 所示。我們收集到了所有堆疊的棧頂地址;接下來我們需要將這些棧資訊符號化。
很容易想到的方案是傳到自己的伺服器上,用 Mac 環境處理堆疊的符號化,轉換為可讀的堆疊資料——代價太大,而且還不經濟。
在瀏覽了 fabric 的各項 API 後,我發現有一個很討好的介面,recordCustomExceptionName
符號化堆疊,卡頓管理
fabric 提供了 recordCustomExceptionName 介面,介面簽名如下圖所示。
我們利用這個介面,將第一步收集的堆疊資料傳給 fabric,讓 fabric 給我們符號化,而且 fabric 卡頓日誌還能夠聚合、分類、分組、跟蹤。crash 日誌的那一套都可以用上, fabric 使用者對此是熟悉的。核心程式碼如下圖所示。
至此,我們用很少的代價就做好了一個卡頓檢測的系統,並且和奔潰功能一起使用,集中處理 APM 各項指標。
這個方案在月活幾十萬的 App 應用了快半年了。使用者一點都沒有感受到有卡頓系統,對現有系統影響很小。如下圖所示。
所有的資料都在 crashlytics 裡,選擇 non-fatal,即可,從統計下來的資料來看,卡頓主要體驗在;
- 讀寫檔案
- 讀寫資料庫
- 處理圖片
- 動畫渲染
- 在非主執行緒讀一些主執行緒才能用的屬性(奇怪吧?)
基本上和我們猜想是一致的,接下來就需要跟蹤和處理這些卡頓。
目前這個系統有兩個缺陷:
- 檢查卡頓本身的 runloop 也被認為是卡頓
- 因為
recordCustomExceptionName
介面的限制,所有執行緒的棧都被合併到一個棧,但不影響核心卡頓程式碼的閱讀。
關於定位的一個小知識點
作者: 高老師很忙
今天分享一個輕鬆的小知識點~~~
搜尋了網上關於 iOS 定位的文章,很多在 locationManager(_:didUpdateLocations:)
收到回撥就執行了 stopUpdatingLocation()
,如下圖:
然而在一些情況之下,這樣寫是有隱患的(如下圖),
在某次執行的時候(並不是每次出現),在 21 點 16 分返回了一個 21 點 09 分的點,這是因為 CoreLocation
可能會返回一個快取的值給我們,所以我們使用的時候應該判斷一下時間戳(如下圖),這樣可以減少定位偏差。
參考連結:O網頁連結
iOS 獲取裝置型號最新總結
作者: KANGZUBIN
在開發中,我們經常需要獲取裝置的型號(如 iPhone X,iPhone 8 Plus 等)以進行資料統計,或者做不同的適配。但蘋果並沒有提供相應的系統 API 讓我們直接取得當前裝置的型號。
其中,UIDevice 有一個屬性 model 只是用於獲取 iOS 裝置的型別,如 iPhone,iPod touch,iPad 等;而其另一個屬性 name 表示當前裝置的名稱,由使用者在設定》通用》關於》名稱中設定,如 My iPhone,xxx 的 iPhone 等。然而,我們無法根據這兩個值獲得具體的型號。
不過,每一種 iOS 裝置型號都有對應的一個或多個硬體編碼/識別符號,稱為 device model 或者叫 machine name,之前的小集介紹過,我們可以通過如下圖中的程式碼來獲取。
所以,通常的做法是,先獲取裝置的 device model 值,再手動對映為具體的裝置型號(或者直接把device model 值傳給後端,讓後端去做對映,這樣的好處是可以隨時相容新裝置)。
例如:去年釋出的第一代 iPhone X 對應的 device mode 為 iPhone10,3 和 iPhone10,6,而今年最新發布 iPhone XS 對應 iPhone11,2,iPhone XS Max 對應 iPhone11,4 和 iPhone11,6,iPhone XR 對應 iPhone11,8,完整的 device mode 資料參考 Wiki:www.theiphonewiki.com/wiki/Models
綜上,我們可以先獲取 device model 值,記為 platform,然後進行對比判斷,轉換成具體的裝置型號,實現程式碼如下圖所示。
備註:圖中程式碼只給了對 iPhone 裝置型號的判斷,而完整的包括 iPad 和 iPod touch 型號我已經放在 GitHub Gist 上,大家可以參考,詳見這裡:gist.github.com/kangzubin/5…
參考連結:
Storyboard 中的約束優先順序
**作者:**這個湯圓沒有餡
在 Masonary 中也可以設定約束的優先順序,如make.left.equalTo(weakSelf.view.mas_left).offset(20).priority(250)
中的 priority。
在 Storyboard 中也可以,舉個?:父檢視上有 imgView 和兩個 label,現要求兩個 label 的寬度隨內容且不超出,另必須保證紅色 label 中的內容顯示完整。如下圖。
storyboard 拖控制元件就不說了,直接從約束開始。
imgView: left、right、top、height、width 綠色label:left、center-y、right、height 紅色label:left、center-y、right、height
這個時候 storyboard 會報錯,因為兩個 label 的寬度無法定位。如下圖。
提示說,降低紅色 label 的水平方向壓縮阻力(即容易被壓縮)以確保在其他檢視之前可以被裁剪。點選 Change Priority,改變約束優先順序。
如上圖,我們可以看 Size Inspector 中,紅色 label 水平方向壓縮阻力由750降為了749,說明在水平方向上,綠色 label 展示的優先順序要高於紅色 label。當然這和我們一開始的需求反了,待會兒再改。我們先看看 Size Inspector 中的說明。
- Content Hugging Priority:拉伸阻力,即抗拉伸。值越大,越不容易被拉伸。
- Content Compression Resistance Priority:壓縮阻力,即抗壓縮。值越大,越不容易被壓縮。
- Intrinsic Size:控制元件未設定寬高約束時用的。
- Ambiguity:解決衝突時是否需要驗證。
Priority 的值預設分為三個等級 Required(1000)、High(750)、Low(250),其實可以輸入任意其他數字。
好,回到需求,只要把紅色 label 的水平方向壓縮阻力優先順序的值改成任意大於綠色 label壓縮阻力的值即可。如果紅色 label 的內容太多,那就會把綠色 label 給擠沒掉。如下圖。
UIWindow 的顯示特性與常見操作方法小結
**作者:**陳滿iOS
UIWindow 的顯示特性
1、相同 windowLevel
下,調整 UIWindow 顯示層的基本方法
- 顯示相關屬性:hidden
- 如果僅僅想顯示一個UIWindow
customWindow.hidden = NO;
複製程式碼
雖然設定自己的 hidden 即可顯示出來,但上述方法並不會"自動"影響之前顯示的 UIWindow 物件的 hidden 屬性。如果,之前 UIWindow 的
hidden = NO
,設定新 UIWindow 的 hidden 將舊 UIWindow 覆蓋後,舊 UIWindow 的 hidden 屬性依舊為 NO。
- 如果僅僅想隱藏一個 UIWindow
customWindow.hidden = YES;
複製程式碼
如果你沒有專門設定過 hidden 屬性,系統預設為 YES。上述程式碼會將 UIWindow 絕對隱藏,不管有沒其他 UIWindow 覆蓋。當也沒有其它非隱藏的 UIWindow 的時候,APP 螢幕完全黑屏。
- 如果想顯示一個UIWindow,同時設定為keyWindow,並將其顯示在同一windowLevel的其它任何UIWindow之上
- (void)makeKeyAndVisible
複製程式碼
上述方法真的會將其顯示在同一windowLevel的其它任何UIWindow之上!顯示最上層的UIWindow以最後執行過該程式碼的UIWindow為準。
- 顯示相關方法:makeKeyAndVisible 的作用
[self.window makeKeyAndVisible];
複製程式碼
其執行效果包括 但不限於 執行了如下程式碼(因為還會覆蓋同 level 的所有 window):
[self.window makeKeyWindow];
self.window.hidden = NO;
複製程式碼
講真,makeKeyAndVisible
真的會自動改變 hidden 屬性值為 NO。
- UIWindow 物件的 hidden 屬性預設值
- 預設值:true
如果你僅僅建立一個 UIWindow,而又不專門設定 hidden 屬性(或者makeKeyAndVisible),系統預設分配的預設值為true。
- 誤區:關於
keyWindow
的混淆易錯點
設定 keyWindow 與否並不影響檢視層級顯示,僅來接收鍵盤及其它非觸控事件。如果沒有專門設定過 keyWindow 的 hiden 為 NO,而且也沒有其它非隱藏的 UIWindow,那麼APP會黑屏。
- 如果僅僅設定為keyWindow
- (void)makeKeyWindow
複製程式碼
- 如果僅僅解除為keyWindow
- (void)resignKeyWindow
複製程式碼
app 的 keyWindow 與是否在最上層顯示沒有任何關係。比如,你如果想通過 [[UIApplication sharedApplication] keyWindow]
獲取正在顯示的 UIWindow 是極其不準確 的。有時候通過這個程式碼獲取的如果真的是正在顯示的 UIWindow,僅僅是因為碰巧而已。
- 警惕點:有多個
hidden
屬性=NO
的UIWindow
,該顯示誰?
如上所見,makeKeyAndVisible
與 hidden 的 setter 方法均可以改變 hidden 的值,但有個問題,經過多次調整,可能有多個 UIWindow 的 hidden 都為 NO,那麼應該顯示誰?
- 對於 hidden 的 setter 方法,最終顯示的以最後執行過
.hidden=NO
的 UIWindow 為準,且執行 .hidden=NO 之前 hidden 的值為 YES。(hidden如果是從NO改為NO的不 算 最後 改變UIWindow的顯示狀態) - 對於 makeKeyAndVisible 方法,最終顯示的以最後 執行過 makeKeyAndVisible 的 UIWindow 為準。
- 對於先後分別用 makeKeyAndVisible 方法和 hidden 的 setter 方法,還是先後分別用 hidden 的 setter 方法和 makeKeyAndVisible 方法,結局同樣以最後改變顯示狀態的 UIWindow 為準。
2、基於 windowLevel
,調整 UIWindow 顯示層的擴充方法
先去 UIWindow.h 裡面看看 UIWindowLevel 的定義:
例如,在手勢相關類中調整自定義的 UIWindow 層級
[self.window makeKeyAndVisible];
_window.windowLevel = UIWindowLevelAlert;
複製程式碼
- 列印代表 UIWindowLevelAlert 層級的資料值
(lldb) po self.window.windowLevel
2000
複製程式碼
- 同理,列印代表UIWindowLevelStatusBar層級的資料值
(lldb) po self.window.windowLevel
1000
複製程式碼
- 同理,列印代表UIWindowLevelNormal層級的資料值
(lldb) po self.window.windowLevel
0
複製程式碼
小結:
- windowLevel 數值越大的顯示在視窗棧的越上面
- 顯示層的優先順序 為: UIWindowLevelAlert > UIWindowLevelStatusBar > UIWindowLevelNormal
- 系統給 UIWindow 預設的 windowLevel 為UIWindowLevelNormal
UIWindow常見操作方法總結
1、獲取 App 所有 window 的 windows 陣列
[[UIApplication sharedApplication] windows]
複製程式碼
例如,第三方載入動畫框架 KVNProcess 中 KVNProgress.m 檔案會有一段這樣的程式碼,如下圖1所示
2、keyWindow
[[UIApplication sharedApplication] keyWindow]
複製程式碼
例如,第三方下拉選單框架 FFDropDownMenu 的 FFDropDownMenuView.m 檔案中有這樣一段程式碼:
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
[keyWindow addSubview:self];
複製程式碼
這段程式碼的目的是新增到最上層 UIWindow,但實際操作是把自己的檢視新增到 keyWindow 上。其實,如果我們在編寫程式碼時嚴謹地保證 keyWindow 是顯示在最上層的 UIWindow,這樣寫沒有問題。但如果:自己或者其它第三方框架曾經調高過其它 UIWindow 屬性 windowLevel,或者有同級 windowLevel 的其它 UIWindow 後來改變過顯示狀態(如 .hidden=NO,makeKeyAndVisible 等),可能會導致下拉選單的彈出檢視無法顯示(被覆蓋)。
3、獲取 AppDelegate 單例的 window 屬性 專門獲取 AppDelegate.m 檔案中的 window 屬性,不包含其它其定義的 window
[[[UIApplication sharedApplication] delegate] window]
複製程式碼
關注我們
歡迎關注我們的公眾號:iOS-Tips,也歡迎加入我們的群組討論問題。可以公眾號留言 ios
、flutter
等關鍵詞獲取入群方式。
推薦閱讀