「 iOS知識小集 」2018 · 第 35 期

知識小集發表於2018-11-05

原文連結

上週公眾號釋出的以下文章:

本期知識小集的主要內容包括:

  • 為 Fabric MOD 一個卡頓檢測功能
  • 關於定位的一個小知識點
  • iOS 獲取裝置型號最新總結
  • Storyboard 中的約束優先順序
  • UIWindow 的顯示特性與常見操作方法小結

為 Fabric MOD 一個卡頓檢測功能

作者:hite和落雁

卡頓檢測系統,用於檢測 App 的主執行緒執行情況。在追求 N 個 9 奔潰之外,卡頓也是我們極其重要執行指標。

很遺憾,世界上最好的免費 APM 平臺 Fabric 卻沒有。而國內的 bugly、網易雲捕等,都提供了類似的功能。如下圖是雲捕的卡頓功能。

「 iOS知識小集 」2018 · 第 35 期

說起來卡頓檢測,技術原理很簡單,下面是來自 bugly 的 QA 裡的描述

iOS 卡頓檢查的依據是監控主執行緒 Runloop 的執行,觀察執行耗時是否超過預定閥值(預設閥值為3000ms) 在監控到卡頓時會立即記錄執行緒堆疊到本地,在App從後臺切換到前臺時,執行上報。
複製程式碼

卡頓檢測系統,這個大任務,可以分解為兩部分:卡頓的檢測 + 卡頓的展示和管理。

卡頓的收集

卡頓的收集,有現成的程式碼,核心程式碼可檢視,gist gist.github.com/hite/1a7ee4…

檢測到之後,需要獲取當前時刻的堆疊,所有執行緒的堆疊(其實只需要主執行緒就夠了)。

OK,拿來主義,一個很有名 PLCrashReporter。我自己整合測試,在我的老古董機器,iPhone 6 跑,卡頓是能檢測到,但是整個軟體基本不可用,整個介面全卡住了。PLCrashReporter 生成日誌程式碼如下圖所示。

「 iOS知識小集 」2018 · 第 35 期

效能非常差,完全不可用。

拿不到堆疊資訊,無法展示,所以只能採用造輪子的方式。根據戴銘 blog 裡的例子,我改造了下,如 gist gist.github.com/hite/1a7ee4… 所示。我們收集到了所有堆疊的棧頂地址;接下來我們需要將這些棧資訊符號化。

很容易想到的方案是傳到自己的伺服器上,用 Mac 環境處理堆疊的符號化,轉換為可讀的堆疊資料——代價太大,而且還不經濟。

在瀏覽了 fabric 的各項 API 後,我發現有一個很討好的介面,recordCustomExceptionName

符號化堆疊,卡頓管理

fabric 提供了 recordCustomExceptionName 介面,介面簽名如下圖所示。

「 iOS知識小集 」2018 · 第 35 期

我們利用這個介面,將第一步收集的堆疊資料傳給 fabric,讓 fabric 給我們符號化,而且 fabric 卡頓日誌還能夠聚合、分類、分組、跟蹤。crash 日誌的那一套都可以用上, fabric 使用者對此是熟悉的。核心程式碼如下圖所示。

「 iOS知識小集 」2018 · 第 35 期

至此,我們用很少的代價就做好了一個卡頓檢測的系統,並且和奔潰功能一起使用,集中處理 APM 各項指標。

這個方案在月活幾十萬的 App 應用了快半年了。使用者一點都沒有感受到有卡頓系統,對現有系統影響很小。如下圖所示。

「 iOS知識小集 」2018 · 第 35 期

所有的資料都在 crashlytics 裡,選擇 non-fatal,即可,從統計下來的資料來看,卡頓主要體驗在;

  1. 讀寫檔案
  2. 讀寫資料庫
  3. 處理圖片
  4. 動畫渲染
  5. 在非主執行緒讀一些主執行緒才能用的屬性(奇怪吧?)

基本上和我們猜想是一致的,接下來就需要跟蹤和處理這些卡頓。

目前這個系統有兩個缺陷:

  1. 檢查卡頓本身的 runloop 也被認為是卡頓
  2. 因為 recordCustomExceptionName 介面的限制,所有執行緒的棧都被合併到一個棧,但不影響核心卡頓程式碼的閱讀。

關於定位的一個小知識點

作者: 高老師很忙

今天分享一個輕鬆的小知識點~~~

搜尋了網上關於 iOS 定位的文章,很多在 locationManager(_:didUpdateLocations:) 收到回撥就執行了 stopUpdatingLocation(),如下圖:

「 iOS知識小集 」2018 · 第 35 期

然而在一些情況之下,這樣寫是有隱患的(如下圖),

「 iOS知識小集 」2018 · 第 35 期

在某次執行的時候(並不是每次出現),在 21 點 16 分返回了一個 21 點 09 分的點,這是因為 CoreLocation 可能會返回一個快取的值給我們,所以我們使用的時候應該判斷一下時間戳(如下圖),這樣可以減少定位偏差。

「 iOS知識小集 」2018 · 第 35 期

參考連結: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,之前的小集介紹過,我們可以通過如下圖中的程式碼來獲取。

「 iOS知識小集 」2018 · 第 35 期

所以,通常的做法是,先獲取裝置的 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,然後進行對比判斷,轉換成具體的裝置型號,實現程式碼如下圖所示。

「 iOS知識小集 」2018 · 第 35 期

「 iOS知識小集 」2018 · 第 35 期

備註:圖中程式碼只給了對 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 中的內容顯示完整。如下圖。

「 iOS知識小集 」2018 · 第 35 期

storyboard 拖控制元件就不說了,直接從約束開始。

imgView: left、right、top、height、width 綠色label:left、center-y、right、height 紅色label:left、center-y、right、height

這個時候 storyboard 會報錯,因為兩個 label 的寬度無法定位。如下圖。

「 iOS知識小集 」2018 · 第 35 期

提示說,降低紅色 label 的水平方向壓縮阻力(即容易被壓縮)以確保在其他檢視之前可以被裁剪。點選 Change Priority,改變約束優先順序。

「 iOS知識小集 」2018 · 第 35 期

如上圖,我們可以看 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 給擠沒掉。如下圖。

「 iOS知識小集 」2018 · 第 35 期

UIWindow 的顯示特性與常見操作方法小結

**作者:**陳滿iOS

UIWindow 的顯示特性

1、相同 windowLevel 下,調整 UIWindow 顯示層的基本方法

  1. 顯示相關屬性: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為準。

  1. 顯示相關方法:makeKeyAndVisible 的作用
[self.window makeKeyAndVisible];
複製程式碼

其執行效果包括 但不限於 執行了如下程式碼(因為還會覆蓋同 level 的所有 window):

[self.window makeKeyWindow];
self.window.hidden = NO;
複製程式碼

講真,makeKeyAndVisible 真的會自動改變 hidden 屬性值為 NO。

  1. UIWindow 物件的 hidden 屬性預設值
  • 預設值:true

如果你僅僅建立一個 UIWindow,而又不專門設定 hidden 屬性(或者makeKeyAndVisible),系統預設分配的預設值為true。

  1. 誤區:關於 keyWindow 的混淆易錯點

設定 keyWindow 與否並不影響檢視層級顯示,僅來接收鍵盤及其它非觸控事件。如果沒有專門設定過 keyWindow 的 hiden 為 NO,而且也沒有其它非隱藏的 UIWindow,那麼APP會黑屏。

  • 如果僅僅設定為keyWindow
- (void)makeKeyWindow
複製程式碼
  • 如果僅僅解除為keyWindow
- (void)resignKeyWindow
複製程式碼

app 的 keyWindow 與是否在最上層顯示沒有任何關係。比如,你如果想通過 [[UIApplication sharedApplication] keyWindow] 獲取正在顯示的 UIWindow 是極其不準確 的。有時候通過這個程式碼獲取的如果真的是正在顯示的 UIWindow,僅僅是因為碰巧而已。

  1. 警惕點:有多個 hidden 屬性 =NOUIWindow,該顯示誰?

如上所見,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 的定義:

「 iOS知識小集 」2018 · 第 35 期

例如,在手勢相關類中調整自定義的 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
複製程式碼

小結:

  1. windowLevel 數值越大的顯示在視窗棧的越上面
  2. 顯示層的優先順序 為: UIWindowLevelAlert > UIWindowLevelStatusBar > UIWindowLevelNormal
  3. 系統給 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,也歡迎加入我們的群組討論問題。可以公眾號留言 iosflutter 等關鍵詞獲取入群方式。

「 iOS知識小集 」2018 · 第 35 期

推薦閱讀

相關文章