WWDC14 Session 409 學習筆記

學習筆記好久沒寫了呢,主要是昨天看了What’s New In Cocoa Touch,感覺沒什麼可寫的。那個就只是一個目錄罷了,乾貨還是得去看目錄指向的Session。言歸正傳,調試是軟件工程師必備技能之一,本Session介紹的是Swift調試的初步知識。

REPL

對於iOS調試除了當初我們早已習慣的LLDB,Swift又添加了一個新的利器REPL(Read-Eval-Print-Loop),在項目中我們可以在斷點時打入

1
(lldb) repl

或者直接在命令行下:

1
xcrun swift

若你機器上裝了XCode 5 和 6 的時候,xcrun 一般走的是 XCode 5的,那麼有兩種做法:

1
/Applications/Xcode6-Beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin 添加到PATH中

或者

1
sudo xcode-select -switch /Applications/Xcode6-Beta.app/Contents/Developer

REPL可以在斷點處插入代碼,和我們的程序做交互。並且顯示的數據比playground上的結果多,如圖:

REPL的調試技巧我們稍後再說,先說LLDB 調試 Swift。

LLDB 調試

對於Bug,有兩種情況,一種是Crash,另一種是運行出來的結果和我們想像的不一樣。

本Session主要介紹了如何用LLDB調試崩潰。

看懂崩潰信息

當我們遇到崩潰的時候,可能會停在各種奇奇怪怪的地方。這個時候該幹什麼?

我本想用我自己寫的例子,但不如session的直觀,還是用Session吧..

單純的Swift錯誤

例1:

1
2
3
let a = ["String":1]
var foo : AnyObject? = a["char"] //我發現wwdc上讀chars ch發的k的音,而不是tʃ
var bar : AnyObject = foo!

運行的時候崩潰了,這個時候我們首先要看線程信息:

1
thread info

session中用的t i 但是我不知道是不是我寫這個的時候沒洗手還是什麼原因,這句話不管用…

如下圖:

嗯,線程1中,在Swift._getOptionalValue出了問題。

我們觀察到EXC_BAD_INSTRUCTION,記住一般出現這個的時候,是因為斷言崩潰

唔,知道了是線程1運行的什麼東西導致了斷言異常。這時候該看一下trace了

1
bt //short for "Backtrace Info"

這裡可以注意到:frame #1: top_level_code 是我們的代碼,frame 0 就是上面info中出現異常的那句話。

我們需要看一下 frame 1

1
f 1 // short for "frame select 1"

恩,我們的代碼出現了。

1
p foo

會發現foo是空的。Bug 找到!

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func FindElement<T>(var array:Array<T>, var match : T-> Bool) -> T?{
    for index in 0...array.count {
        if (match(array[index])){
            return array[index]
        }
    }
    return nil
}

let array = [0, 3, 2, 8]

func c (var x:Int)->Bool
{
    return x == 1314
}

let a:Int? = FindElement(array, c)

輕車熟路,thread info 又是 EXC_BAD_INSTRUCTION,果斷bt

這次稍微複雜點,但是明顯依然要去看 f 1

p index 得到

1
(RangeGenerator<Int>) $R1 = (startIndex = 5, endIndex = 5)

很強力,直接告訴你這個循環運行到的範圍。

p array.count –> (Int) $R2 = 4

我們發現,oops,數組只有4個,結果溢出了。 恩,哪個坑貨多打了一個.啊。 //官方吐槽… 和 .. 是個坑麼…

Swift X Objective-C 錯誤

例3:

代碼和上文差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func FindElement<T> (var array : NSArray, var match: T->Bool) -> T? {
    for index in 0...array.count {
        if let elementAsT = array[index] as? T{
            if match(elementAsT)
            {
                return elementAsT
            }
        }
    }
    return nil
}

let array:NSArray = [0, 3, 2, 8]

func c (var x:Int)->Bool
{
    return x == 1314
}

let a:Int? = FindElement(array, c)

這次bt的時候,嚇尿了:

那麼一大片,還提示cpp錯誤之類的。

我們需要注意:signal SIGABRT

相信有OC開發經驗的人都知道,這個一般是oc 的 throw exception導致的,仔細往下看。果然!

1
frame #8: 0x00007fff8908afa1 libobjc.A.dylib`objc_exception_throw + 343

再往下看,frame 10 又出現了 FindElement 了。 按照上面的方法處理就是了。

例4:

1
2
3
4
let empty = NSData(contentsOfURL:NSURL(string:"http//non.existent.web.site.com"))
let chars = UnsafePointer<Character>(empty.bytes)
let char = chars[0]
println(char)

bt

這回,我們注意到了一個熟悉的東西:EXC_BAD_ACCESS (code=1, address=0x0)

找bug過程同上,這個例子說明的是:

我們知道,Swift的指針是安全的,但是Swift調用C/OC以及一些framework時,還是有可能出現空指針的。

Break Point

寫這個的時候,咖啡廳背景音樂是《斷點》 = =

斷點是debug的時候,大夥兒最常用的東西,對於LLDB來說,斷點有四個要素:

  • Specification: 我想停下來的地方
  • Location: LLDB根據Specification 找到的地方
  • Condition: 停下來的條件 if condition stop, 默認condition 是 true。
  • Action: 當停下來時執行什麼,默認為空

之後他介紹了一大堆lldb命令行打斷點的方法

在A文件B行下斷點

1
b main.swift:66 //breakpoint set --file main.swift --line 6

注意在lldb裡面下的斷點,不會在XCode中顯示

查看斷點列表

1
br l //breakpoint list

這裡要說一下:

1
2
3
1: file = '/Users/txx/Desktop/workspace/ida/ida/main.swift', line = 66, locations = 1, resolved = 1, hit count = 1

  1.1: where = ida`top_level_code + 4 at main.swift:66, address = 0x0000000100001554, resolved, hit count = 1

1: 代表的是我剛才的 b main.swift:66 也就是 specification

1.1 代表的是 lldb根據我的 specification 找到的對應點,由於我是用的行號作為限制條件,所以只會找到一行。如果:

1
2
3
func plus(a: Int, b:Int) -> Int {return a + b; }
func plus(a: Double, b:Double) -> Double {return a + b; }
func plus(a: String, b:String) -> String {return a + b; }

我要 br plus

就會給所有plus 打上標籤提示 Breakpoint 2: 3 locations.

br l就會

1
2
3
4
5
6
(lldb) br l
Current breakpoints:
2: name = 'plus', locations = 3, resolved = 3, hit count = 0
  2.1: where = ida`ida.plus (Swift.Int, Swift.Int) -> Swift.Int + 4 at main.swift:66, address = 0x0000000100001604, resolved, hit count = 0
  2.2: where = ida`ida.plus (Swift.Double, Swift.Double) -> Swift.Double + 4 at main.swift:67, address = 0x0000000100001634, resolved, hit count = 0
  2.3: where = ida`ida.plus (Swift.String, Swift.String) -> Swift.String + 4 at main.swift:68, address = 0x0000000100001654, resolved, hit count = 0

註釋部份斷點

若我只想要 返回值為 String 的 plus func,我就要在上面的斷點結果裡面disable掉部份斷點:

1
2
3
4
5
6
7
8
(lldb) br dis 2.1 2.2
2 breakpoints disabled.
(lldb) br l
Current breakpoints:
2: name = 'plus', locations = 3, resolved = 1, hit count = 0
  2.1: where = ida`ida.plus (Swift.Int, Swift.Int) -> Swift.Int + 4 at main.swift:66, address = 0x0000000100001604, unresolved, hit count = 0  Options: disabled
  2.2: where = ida`ida.plus (Swift.Double, Swift.Double) -> Swift.Double + 4 at main.swift:67, address = 0x0000000100001634, unresolved, hit count = 0  Options: disabled
  2.3: where = ida`ida.plus (Swift.String, Swift.String) -> Swift.String + 4 at main.swift:68, address = 0x0000000100001654, resolved, hit count = 0

但這樣下斷點的效率非常低,於是有了大殺器

根據正則表達式加斷點

1
2
3
4
5
6
(lldb) br s -r plus.*String  //breakpoint set --func-regex plus.*String
Breakpoint 3: where = ida`ida.plus (Swift.String, Swift.String) -> Swift.String + 4 at main.swift:68, address = 0x0000000100001654
(lldb) br l
Current breakpoints:
3: regex = 'plus.*String', locations = 1, resolved = 1, hit count = 0
  3.1: where = ida`ida.plus (Swift.String, Swift.String) -> Swift.String + 4 at main.swift:68, address = 0x0000000100001654, resolved, hit count = 0

Breakpoint Condition

例如:

1
2
3
4
5
6
7
var bin:Int64 = 1

for i in 1...50{
    bin *= 2
}

println(bin)

我們想在 在 i 是 5 的倍數的時候 下個斷點就這樣:

1
2
(lldb) b main.swift: 78
(lldb) br m --c "i.startIndex % 5 == 0"

注意,如果是Swift的工程,但是斷點斷在了OC的地方,那麼condition還是要用 oc 來寫,而不是swfit

BreakPoint Action

假設我們想輸出i是5的倍數的時候,bin的值:

1
2
3
4
5
(lldb) br co a //breakpoint command add
Enter your debugger command(s).  Type 'DONE' to end.
> p bin
> continue
> DONE

run:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(Int64) $R5 = 8
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R11 = 256
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R17 = 8192
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R23 = 262144
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R29 = 8388608
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R35 = 268435456
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R41 = 8589934592
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R47 = 274877906944
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R53 = 8796093022208
Process 18754 resuming
Command #2 'continue' continued the target.
(Int64) $R59 = 281474976710656
Process 18754 resuming
Command #2 'continue' continued the target.
1125899906842624

BreakPoint Filter

根據我們可以用filter來讓提供的Specification更精確一些。(Ref: Advanced Debugging With LLDB WWDC 2013)

First Header Second Header
Source Location —file —line|
Function name —name |
Module or class —func-regex|
Variable Values breakpoint modify —condition|

REPL

REPL 和 LLDB 交互

  1. 啟動REPL:

    在lldb斷點上,直接輸入 repl

  2. 切回LLDB: 在REPL中輸入 :

  3. 在REPL中調用LLDB命令: 在REPL中輸入 :lldb命令:p index

REPL ad-hoc Unit test

REPL 除了最基礎的插入代碼運行以外,可以幹一些很風騷的事情。

例如我隨手寫了一個快排:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func qsort(var array:Array<Int>, left:Int, right:Int)
{
    var l = left
    var r = right
    var x = array[(l + r) >> 1]

    while (l <= r)
    {
        while (array[l] < x) {l++}
        while (array[r] > x) {r--}

        if l<=r
        {
            var y = array[l]
            array[l] = array[r]
            array[r] = y
            l++
            r--
        }
    }

    if (l < right)
    {
        qsort(array, l, right)
    }
    if (r > left)
    {
        qsort(array, left, r)
    }
}

我可以在運行時這麼校驗他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1> func isSorted(a:Array<Int>)->Bool{
3. for i in 1..a.count {
5. if a[i-1] > a[i]{
7.  return false;

9. 11. }
13. }
15. return true
17. }
18> var arr = [2131, 3435, 45435, 32423, 545]
arr: Int[] = size=5 {
  [0] = 2131
  [1] = 3435
  [2] = 45435
  [3] = 32423
  [4] = 545
}
19> qsort(arr, 0, 4)
20> if (isSorted(arr)){
22. println(true)
24. }
true

總結

這篇介紹了一大堆命令行的東西,感覺和我們平時用XCode調試差距很大。也許覺得很蛋疼,為什麼要這麼幹呢?

首先,我們都是debug 自己代碼。如果是你調用別人的代碼崩潰,或者你是測試工程師(Session的講師是Debug Team的),而且代碼量是百萬級別的。找那個文件絕對會讓你崩潰。就需要命令行的方式來直接下斷點看結果。

以及,LLDB 支持 Python Script 來做自動化調試。

這篇文章明顯 LLDB 基礎比重比較大,REPL並不是重點,不是因為他不夠強悍,而是 410_hd_advanced_swift_debugging_in_lldb 專門講這個的。

Comments