本文針對 WWDC 2016 Session 410 和 412 以及 WWDC 2015 Session 413 中的內容進行了整理.編程
文中主要傾向於如何使用,以及這些工具的使用場景, 若是想了解這些工具的工做原理或者細節方面的東西,請你們觀看原視頻便可.具體連接在文中最後的參考資料裏.swift
廢話少說,接着往下看吧.數組
Static Analyzer 是一個常見的 debug 的工具, 蘋果工程師在 WWDC 中是這樣介紹它的:xcode
使用 Static Analyzer 很簡單, 你能夠經過選擇 Product -> Analyze
或者 Cmd + Shit + B
的方式執行, 若是有錯誤,就會在 Issue Navigator 上顯示出來安全
在今年的 Session 412 中, Apple 的工程師告訴咱們在 Xcode 8 中, Static Analyzer 可以檢測出三種新的錯誤, 它們分別是:ruby
看英文有點很差理解,不用擔憂,接着往下看,我們一個個的說.session
Localizability 其實說的是 Static Analyzer 如今可以檢測出本地化信息缺失的問題,目前可以檢測出來兩種類型的錯誤, 一種是沒有使用 NSLocalizeString 這樣的 API, 而直接給控件設置 Sting 的狀況, 一種是使用了相應的 API, 但在 comment 信息裏面賦值爲 nil. 若是有錯, 就會像下圖同樣, 在代碼下方出現一個藍色提示條, 告訴開發者具體的錯緣由.多線程
在 Xcode 裏,檢測第二種類型的錯誤並非默認開啓的,若是想開啓,須要在BuildSting 中進行以下設置: app
Instance Cleanup 說的是什麼呢? async
這說的是在 MRC 的代碼中, 尤爲在 dealloc 中,咱們不該該對 assign 類型的屬性進行 release 操做,應該對 retain 或者 copy 類型的屬性進行 release 操做, 若是不這樣操做的話,會引起一些沒必要要的麻煩. 不過如今有了 Xcode 8, 這些問題就交給 Static Analyzer 吧,它可以很準確的檢測出這樣的錯誤.
Anyway, 仍是建議你把代碼轉成 ARC 吧! 不知道怎麼轉, 看下圖
關於這個話題是說的什麼呢?
首先咱們得先說說在 2015 年的 WWDC 大會上, Objective-C 引入的一個新特性就叫作 Nullability, 用於代表一個東西到底能夠爲 nil 仍是不能夠爲 nil , 這和 Swift 裏的 option 類型很類似. 既然知道了這個玩意後,咱們再說說 Static Analyzer 在這一塊到底可以乾點什麼?
通俗的說, Static Analyzer 能夠檢測出在不一樣場景下是否作到了 nullability 的一致性.
那麼咱們通常何時會出現 nullability 方面的錯誤呢?
我想看到這,不少朋友都會對這個功能嗤之以鼻, 而且想着"我只要不使用與 nullability 相關的關鍵字, Static Analyzer 就確定不會報錯啦.", 確實從某種角度來講, 你這麼幹之後, Static Analyzer 確實不會報錯了,但這樣真的好麼?
這就回歸到爲何咱們須要使用與 nullability 相關的關鍵字這個問題上, 我認爲主要的緣由有三個:
估計有的朋友會對個人第三個觀點不太理解, 不用在這裏糾結, 下面的這個例子會解釋個人想法.
首先看這段代碼, 咱們假設他的使用場景以下, 這是一個相似地理位置的抽象類, 對於這樣的類,它能夠有一個方法來描述它所在的城市或者國家, 這個方法看起來是沒有任務錯誤的, 但其實裏面是有缺陷的, 如今假設咱們在大西洋的某個不知名的海域中, 因爲這個地方既不屬於某個城市, 更不屬於某個國家, 那麼因爲 name
的初始值爲 nil
, 那麼他的返回值必定爲 nil
, 這就與 API 設計者規定的 nonnull
相互衝突了, 萬幸的是 Static Analyzer 幫咱們檢測到了這一切.
但假如咱們沒有使用 nonnull 關鍵字呢? 那麼這段話原本是要用於展現在某個 label 上的,但因爲返回值爲空, 屏幕上空空如也, 用戶好幾臉懵逼, PM 和 QA 的同事火速殺到你的工位前......
總之,不用我說,你應該能明白我意思了, 這就是我說的:
可以將一些沒必要要的問題提早到編碼階段, 而不是到用戶使用時才暴露
咱們再來看一個例子說明下不正確的宏註釋產生的問題,在NS_ASSUME_NONNULL_BEGIN
和 NS_ASSUME_NONNULL_END
之間的屬性都會被默認爲 nonnull
類型, 那麼看下面的代碼:
在平常的工做中,咱們常常是從某我的手裏接過來一段代碼進行開發, 假設在這個文件裏, 因爲整個代碼已經到了近 1000 行, 且有好幾個類在同一個 .m
文件中, 因此兩個宏寫的很是隱蔽, 你根本沒有察覺到它們的存在.
而後你很自信的聲明瞭一個再普通不過的的 pressure
屬性, 並重寫了它的 get
方法, 同時咱們的邏輯很清楚, 這是一我的擁有的方法, 若是他有壓力計的話就測量一下壓力,並返回這個壓力值,若是沒有壓力計的話,就返回 nil , 這個邏輯看起來是如此的正確, 但你一運行就 crash , 是否是很崩潰.
好在 Static Analyzer 告訴了咱們問題的關鍵, OK, 這個 bug 很快就能解決了.
說完了Static Analyzer, 咱們來講說 Runtime Issue 這個東西,就像下面這個圖展現的同樣, 你能夠認爲之後見到這個紫色的感嘆號標誌就是一個 runtime issue , 哦, 順道說一下左邊的兩個分別是 Error 和 Warnning 狀態, 右邊的兩個分別是 Static Analyzer Issue 和 UI Test Failed 的狀態, 不一樣於其他這些東西的出現時間, runtime issue 是出如今程序運行期間的
目前支持 Runtime Issue 的工具備三個, 分別是 Debug View Hierarchy , Thread Sanitizer 和 Debug Memory Graph , 咱們會在下面的話題一個個介紹給你們!
嗯吶, 總之, Runtime Issue 的話題就告一段落啦.
View Debugging 到底指的是什麼呢? 我想各位看英文時候可能有點懵逼, 但看完下面兩張圖是否是瞬間明白我在說神馬了!!!
嗯吶, 就是這個功能叫作 View Debugging, 也能夠叫作 Debug View Hierarchy, anyway, 你喜歡啥就叫啥吧,
這個功能在 Xcode 6 就有了, 那麼在 Xcode 8 上又有了哪些提高呢? 蘋果工程師給出答案是這樣的,
至於第一個改進點,你們可能須要本身去感覺,我也確實沒有興趣作一個數據對比, 畢竟 PM 還找我作需求呢...
那麼咱們說下後面五個改進點吧.
這究竟是說神馬呢? 難道是說之前的 layout 和 transform 不許確麼? No, No, No, 並非說之前不許確,而是說如今比之前更精確了, What, 我說的話是否是好繞,仍是直接上圖吧...
因此說你看出來神馬名堂了麼? 在 Xcode 8 裏可以更加精確的代表這些約束的意義, 例如是不是等比例縮放(第一張的比例值), 是不是權值較低的約束(第二張的虛線段), 是不是一個不絕對相等的約束(第三張的小於等於). 這些在 Xcode 7 裏都是沒有體現出來的, 總之經過這些標記, 可以讓咱們更加清晰的瞭解到這些約束的意義, 而不僅是一根實線而已
這是說在新的 debug 模式下,咱們可以看到 blur 層了. 是否是很美好
這個東西我以爲仍是蠻好使的, 由於原先在 Navigator 裏找某個控件時, 真的很難, 尤爲在那種結構複雜的界面裏, 就看着本身點着那個三角按鈕一遍又一遍的...
Xcode 8 在今年很好的解決了這些個問題, 咱們如今在 Navigator 中能夠經過控件的內存地址來定位, 也能夠經過它的類名來定位, 甚至可使用控件中展現的 String 內容來定位.
這樣一來, 找控件就變得很 easy 了, 是否是!!! 不信, 你看下面的這張圖.
這個新增的功能充分體現了蘋果工程師的人文關懷, 試想一下, 咱們每次在定位到對應的控件後, 若是想要修改其 layout 的相關屬性時, 有些人會到左邊的 Project Navigator 中的層級結構裏找對應的.m
或者.h
文件, 熟悉快捷鍵的人可能會用 `Cmd + Shit
但在Xcode 8 以後, 咱們只須要去 UI 控件的 Object Inspector 的界面裏點一下右邊深灰色的前進按鈕, 嗖的一下,咱們就跳轉到了對應類的文件中
這個功能就要結合以前的 runtime issue 話題了, 廢話少說, 先上個圖給大家瞅瞅.
咱們能夠看到,若是咱們在佈局控件中有錯誤的話, 咱們點擊 Debug View Hierarchy 後, Xcode 8 就會報出來一堆 Runtime Issue , 這個功能是否是很吊, 之後寫約束不再怕不怕啦, 畢竟有錯, 我們就按提示改唄.
這一塊的內容並非某個 Session 裏提到的, 而是我在看這些個 WWDC 後總結出來的, 你能夠發現蘋果的工程師在解決這些問題時, 都是有一個套路的, 套路的英文我也不知道是啥, 就用個 workflow 吧.
他們在解決帶有 runtime issue 的問題時, 都會遵循這樣一個解決思路
至於這個東西, 我以爲可能須要你們本身在實踐中慢慢體會, 纔會更深刻的理解爲何會有這些 debug 工具的產生和爲何他們要在這裏提示.
固然這也是個仁者見仁,智者見智的問題, anyway 你若安好, 即是晴天!
講完了 View Debugging Enhancement , 咱們來講說今年 Xcode 8 推出的 Memory Graph Debugging.
最近看到不少公衆號和微博都有朋友在說這個特性, 我在這裏就不花費太多的篇章去講它, 更多的說說我以爲在其餘文章裏沒提到的東西吧.
在說這個東西以前, 不知道你們是否知道如下三個命令, 若是沒有你們不妨在本身的機子上試一試
$ heap YourAppName $ leaks YourAppName $ malloc_history YourAppName Address
好吧,假設看到這時, 你已經按照我說的那樣,按使用了上面的幾個命令, 那麼下面我就得告訴你一個真相.
其實 Memory Graph Debugging 就是把這樣的一套東西變成了UI界面而已.
那麼咱們接着說說如何使用它吧, 它的使用方式很簡單, 在 app 運行的時候, 點擊 Debug View Hierarchy 按鈕旁邊的 Debug Memory Graph 按鈕便可, 對就是那個三個圓圈兩個線的按鈕.
哦, 對了若是你想看到對象的 malloc_history, 記得在 Diagnostic Scheme Tab 頁面裏面選擇 Malloc Stack , 不然你是看不到任何信息的, 命令行也是如此, 另外, 蘋果的工程師還說若是勾選了 Malloc Scribble, 整個結果會更加精確
那麼咱們來看看點擊 Debug Memory Graph 按鈕後的效果吧
經過這段時間的使用呢, 大體總結出來這樣的一些規律
固然還有不少一些其餘的類型,具體的你們去看右上角的 Memory Inspect 界面就好,上面都會有詳細的信息
另外這一塊還要跟你們交流的就是在 Session 410 中, 蘋果的工程師說了一些內容, 但願開發者們在使用 Memory Graph Debug Tool 時可以知道:
Live Allocations Only
便可, 這樣會避免過多的性能消耗另外 Xcode 8 專門提供了一個文件格式來保存某一時刻 app 的 Memory Graph, 固然這個文件你是無法 run 起來的, 它只是個graph, 你要明確這一點.
對於喜歡命令行的小夥伴來講, 蘋果還提供了一下的操做指令
$ leaks --outputGraph=<path> <process> //creates .memgraph file $ {leaks|vmmap|heap} <path/to/file.memgraph> [options] //operate on .memgraph file
Santize在英文裏面有美化, 優化的意思, 可想而知 Sanitizer 就是一個用於優化的工具.
那麼 Xcode 中的 Sanitizer 究竟是什麼呢? 在 WWDC 2015
Session 413 中, 蘋果的工程師給出如下條目來介紹 Sanitizer:
那麼到底 Sanitizer 在 Xcode 裏怎麼使用呢? 其實很簡單, 打開 Product -> Scheme -> Edit Scheme, 就會彈出以下的界面, 咱們在 Diagnostics 中可以看到這樣一個標題 Runtime Sanitization
, 在它下面有 Address Sanitize
和 Thread Sanitizer
兩個選項, 咱們只須要勾選相應的 Sanitizer 便可.
說到這裏還必須多說幾句, 此處若是你只是勾選了相應的選項並不表明你就能使用 Sanitizer 來 Check 代碼了, 你還必須從新 run 一下代碼, 爲何呢?
這就必須說說整個代碼 build flow 了. 以下圖所示, 經過勾選了對應的選項, Xcode 會向 clang 傳遞一個特定的參數, 而後生成一個獨特的 binary, 而後這個 binary 會和 Thread Sanitizer 或者 Address Sanitizer 的 dylib 連接在一塊兒. 這樣 Sanitizer 就實現了它想要達到的功能.
至於每一個 Sanitizer 的實現原理, 我這裏就不過多描述了, 建議你們直接觀看 WWDC 2015 Session 413 ( Address Sanitizer ) 和 WWDC 2016 Session 412 ( Thread Sanitizer ) , 咱們這裏仍是着重介紹它們的使用方法和使用場景.
總之, 你須要記住的就是, 在使用 Sanitizer 的時候, 要從新 Run 一下代碼哦.
ASan 實際上是 Xcode 在去年新增的一個功能, 它主要用於檢測一些內存方面的錯誤, 在 Xcode 8 裏, ASan 已經全面支持了 Swift, 這應該是它惟一新增的一個功能.
那麼 ASan 到底能檢查哪些類型的錯誤呢? 蘋果工程師列舉了如下六種:
哦對了, 蘋果的工程師還說後面四種是 ASan 獨有的功能, 固然說這話的時候是 2015 年, 不知道 2016 年的時候, 其餘的 debug 工具備啥進步沒.
說了這麼多,我們來看看下面這段代碼吧.
你們應該可以看出來若是用 buffer[80]
的話是會產生數組越界的問題, 雖然 malloc 了 80 個位置,但起始位置是從 0 開始的.
但現實呢, 這段代碼在不開啓 ASan 的情況下, 百分之九十九都不會產生 crash , 並且產生 crash 的時候也不會像圖中紅色文字那樣明確的告訴你這是一個 Heap buffer Overflow 問題.
這就是 ASan 的做用, 因此若是再遇到內存問題, 不用再坐臥不安的改完代碼後使用哎彌陀佛 Cmd + R 大法了, 是否是很 nice!
最近發現很多公衆號和微博在說 Xcode 8 的新特性時, 都在說 Debug View Hierarchy 和 Debug Memory Graph 的相關內容, 但說實話, 我以爲今年 Xcode 8 最使人興奮的就是添加了 Thread Sanitizer 這個功能, 說真的, 這個功能太有用了, 爲何呢?
讓咱們想一想本身在調試線程方面的 bug 時, 有哪些使人記憶深入的東西:
相信上面的三點總會有一個讓你刻骨銘心......
那麼咱們趕忙說說怎麼開啓 TSan 來幫咱們檢查線程問題吧. 喂喂, 這個我們就再也不說一遍了吧, 記得看 ASan 章節裏的那個圖片, 在 Address Sanitizer 下面就行 Thread Sanitizer 啦.
至於 Thread Sanitizer 下面的那個 Pause on Issues
的選項就是說, 若是你想一個一個看 runtime issue 就勾選它, 若是你不想這樣, 就不要勾選它, 具體是個神馬感受, 你本身試試嘍.
若是你喜歡使用 Comman-Line ,那麼請記住下面的代碼
//Compile and Link with TSan $ clang -fsanitize=thread source.c -o executable $ swift -sanitize=thread source.swift -o executable $ xcodebuild -enableThreadSanitizer YES //Stop after the first error $ TSAN_OPTIONS=halt_on_error=1 ./executable
哦, 還要加一句, TSan 如今只支持 64爲 macOS, 以及 64位的 iOS 和 tvOS 的模擬器, 並不支持真機調試和 watchOS.
那麼 TSan 做爲一個可以檢查線程錯誤的工具, 它如今能檢查哪些類型的錯誤呢? 蘋果給出的答案以下:
phread_johin
)malloc
)那麼咱們拿下面的這段代碼來舉例:
- (void)viewDidLoad { [super viewDidLoad]; [self resetStatue]; pthread_mutex_init(&(_mutex), NULL); } - (void)resetStatue{ [self acquireLock]; self.dataArray = nil; [self releaseLock]; } - (void)acquireLock{ pthread_mutex_lock(&_mutex); } - (void)releaseLock{ pthread_mutex_unlock(&_mutex); }
這段代碼的意思是,咱們在 viewDidLoad 方法裏面從新 reset 本身的狀態, 爲了防止多個線程去訪問同一個 dataArray 屬性, 形成 data race 的狀態, 咱們在 resetStatus 的時候須要加鎖, 但當前代碼中,咱們實際上調用的是一個沒有初始化的鎖 ( init 方法在 resetStatus 方法下面哦) , 但這段代碼在實際運行的過程當中,百分之九十九也不會出現 crash, 但有了 TSan 後, 咱們來看看發生了什麼變化
發現 runtime issue 的標誌了麼! 看不清啊,那咱們把左邊的 Issue Navigator 放大一下
發現沒有,在 Issue Navigator 中, TSan 明確的告訴了咱們錯誤的類型, 並且把線程中的歷史信息都記錄了下來以便咱們分析並解決這個問題, 有沒有很貼心!
經過這個問題, 你發現了什麼問題麼?
這樣安全可靠的 debug 工具你怎能不愛呢!
說道這, 不妨咱們多說一點, 你們都知道 data race 是線程中最常出現的問題, 形成 data race 的狀況無非就是兩種, 一種是邏輯錯誤, 一種是沒有加鎖. 在這裏我特別想分享一個我本身在使用 TSan 時編寫的小 Demo.
咱們先看一下代碼的使用場景, 咱們假設有三個售票員在賣票, 票的數量由 ticketsCount
決定, 同時咱們將售票員抽象成一個線程類:
self.ticketsCount = 100; NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil]; thread1.name = @"售票員1"; self.thread1 = thread1; NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil]; thread2.name = @"售票員2"; self.thread2 = thread2; NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil]; thread3.name = @"售票員3"; self.thread3 = thread3;
而後咱們執行下面的這段代碼
for (NSUInteger count = self.ticketsCount; count > 0; count--) { if (count > 0) { [NSThread sleepForTimeInterval:0.1]; NSString *name = [NSThread currentThread].name; dispatch_async(dispatch_get_main_queue(), ^{ self.ticketsCount = count - 1; NSString *string = [NSString stringWithFormat:@"%@賣了一張票, 還剩%zd張票", name, self.ticketsCount]; self.ticketsCountLabel.text = string; NSLog(@"%@", string); }); } else { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"票賣完了"); }); return; } }
會發生什麼呢? 顯而易見,因爲沒有加鎖, 售票員會賣出去不應賣出去的票
那咱們加個鎖試試?
@synchronized (self) { for (NSUInteger count = self.ticketsCount; count > 0; count--) { if (count > 0) { [NSThread sleepForTimeInterval:0.1]; NSString *name = [NSThread currentThread].name; dispatch_async(dispatch_get_main_queue(), ^{ self.ticketsCount = count - 1; NSString *string = [NSString stringWithFormat:@"%@賣了一張票, 還剩%zd張票", name, self.ticketsCount]; self.ticketsCountLabel.text = string; NSLog(@"%@", string); }); } else { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"票賣完了"); }); return; } } }
加鎖後,咱們發現售票員確實沒有再賣出去不應賣的票,可是好像只有一個售票員在賣票.
這顯然是一個邏輯錯誤, 咱們鎖住更像是線程, 而不是資源, 因此咱們再改進一下
while (1) { @synchronized (self) { if (self.ticketsCount > 0) { [NSThread sleepForTimeInterval:0.1]; NSString *name = [NSThread currentThread].name; self.ticketsCount = self.ticketsCount - 1; dispatch_async(dispatch_get_main_queue(), ^{ NSString *string = [NSString stringWithFormat:@"%@賣了一張票, 還剩%zd張票", name, self.ticketsCount]; self.ticketsCountLabel.text = string; NSLog(@"%@", string); }); } else { dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"票賣完了"); }); return; } } }
來看看打印臺的結果
看起來是否是特別完美! 不一樣的售票員在賣票, 並且也沒有出現賣出去不應賣的票, 可是其實這段代碼仍是有潛在風險的.
What? What? What? 怎麼會有錯...
這時候我但願你能打開 TSan 來分析下潛在的風險在哪裏吧,
PS: 若是你比較懶的話, 好好想一想 dispatch_async
用的對麼? 若是我在這裏麼加入了一些危險操做 ?
說到這, 你是否是又一次被 TSan 強大的功能震撼住了, 說真的, TSan 真的是 Xcode 8 裏一個很是強大的新功能, 它可以幫咱們察覺到不少不少咱們本身徹底意識不到的細小問題, 而在這些問題常常會弄得咱們在 debug 時候苦不堪言, 因此從今天開始, 在你編程的時候, 用一下 TSan 吧
總結一下今天咱們到底說了些什麼:
.memgraph
Debug workflow
和 Runtime issue
Sanitizer
, View Hierarchy Debug Tool
, Memory Graph Debug Tool
最後呢, 想說的很簡單, Use this tools on our project and Make our app better than ever
WWDC 2016 Session 410 - Visual Debugging with Xcode : https://developer.apple.com/videos/play/wwdc2016/410/
WWDC 2016 Session 412 - Thread Sanitizer and Static Analysis : https://developer.apple.com/videos/play/wwdc2016/412/
WWDC 2015 Session 413 - Advanced Debugging and the Address Sanitizer : https://developer.apple.com/videos/play/wwdc2015/413/