今天給大家介紹的內(nèi)容攀甚,無關乎任何功能性開發(fā)技術秋度,但又對開發(fā)的效率影響至深,這就是調(diào)試技術埠居。
何為調(diào)試呢事期,比如我們用 print
函數(shù)在指定位置進行輸出兽泣,來定位某些節(jié)點的變量內(nèi)的取值:
let result = parseJSON("[1,2,3]");
print(result);
result = parseJSON("error");
print(result);
相信我們大家看到類似這樣的代碼都不會陌生唠倦,估計為開發(fā)者朋友都會或多或少的用這樣的方式對程序進行調(diào)試。
這種方式有它的方便之處法希,就是我們不需要太多思考靶瘸,需要跟蹤某些地方的時候怨咪,直接輸出就可以得到調(diào)試信息了。但這樣做也有它的弊端唉匾,就是我們每次這樣調(diào)試巍膘,都要反復的編譯峡懈,運行与斤,然后寫進新的 print
語句撩穿,再繼續(xù)編譯食寡,運行。反復的編譯善榛,運行會比較消耗時間锭弊。并且我們再調(diào)試完之后擂错,很容易會忘記將調(diào)試語句刪除钮呀,導致很多輸出語句遺留再代碼中爽醋,隨著項目的長期進展后蚂四,這樣會對項目后期的調(diào)試造成很多干擾。
而且久妆,當我們想再次調(diào)試這段區(qū)域的時候筷弦,我們不得不再次寫上這些輸出語句烂琴。而有時對于稍微復雜一些的調(diào)試場景蜕乡,print
輸出這樣的方式异希,往往還不能太好的應對称簿。
那么有什么辦法能解決這些麻煩呢憨降,那就是調(diào)試技術,調(diào)試器幾乎在大多數(shù)現(xiàn)代的開發(fā)環(huán)境中都會有士嚎,所以莱衩,iOS 開發(fā)也不例外笨蚁,Xcode 環(huán)境為我們提供的對應調(diào)試工具就是 LLDB括细。
初識 LLDB
LLDB 就是 XCode 為我們提供的調(diào)試工具。那么說了這么多锉试,到底什么是調(diào)試工具呢呆盖? 也許說調(diào)試器你可能會感到比較陌生絮短,但說到斷點丁频,相信你就會聽著比較耳熟了邑贴。我們還以剛才我們提到的代碼為例:
我們在第 23 行左邊點擊了一下拢驾,就創(chuàng)建了一個斷點繁疤,這時我們再運行這個應用的時候稠腊,程序運行到這里就會被斷點攔截:
并且在 Xcode 的命令行區(qū)域架忌,顯示了 (lldb) 提示符。
基本調(diào)試操作
我們回到最初的問題饰恕,如果不使用 print 輸出埋嵌,我們怎么能得到 result 的值呢雹嗦,這就是我們要討論的斷點調(diào)試機制了俐银,我們先看一下 XCode 底部調(diào)試區(qū)域的幾個按鈕:
- 第一個按鈕是繼續(xù)的意思捶惜,會讓程序從斷點處恢復吱七,繼續(xù)往下運行踊餐,我們點了這個按鈕后臀稚,應用就會恢復正常運行狀態(tài)吧寺。
- 第二個按鈕是(Step Over),單步執(zhí)行的意思稚机,每點這個按鈕一次赖条,程序就會從我們斷點開始的地方纬乍,向下執(zhí)行一步。
- 第三個按鈕是 (Step In)早芭,進入執(zhí)行的意思退个,簡單來說就是如果我們當前的斷點在一個函數(shù)調(diào)用上语盈,把么斷點會繼續(xù)進入這個函數(shù)的內(nèi)部進行調(diào)試刀荒。
- 第四個按鈕是(Step Out),跳出的意思, 就是如果我們當前再一個函數(shù)中缠借,它會跳出當前的函數(shù)泼返,回到函數(shù)的調(diào)用處绅喉。
恩柴罐。革屠。你說了這么多,完全聽不懂啊
沒關系似芝,我們一一道來国觉,還是回到我們最初的需求麻诀,我們的斷點現(xiàn)在停在給 result
變量賦值的這條語句中蝇闭,斷點所在位置的語句是還沒有被執(zhí)行的呻引,所以我們需要點一下 Step Over 按鈕(也就是我們剛才列出的四個按鈕的第二個),讓程序執(zhí)行一行代碼元践。
執(zhí)行完這行代碼后单旁,我們的 result
變量就被賦值完成了象浑。那么問題來了绪穆,我們怎么得到 result
變量中得內(nèi)容呢?
還記得我們的 LLDB
命令行么藤乙,我們使用一個叫做 po
的命令,就可以取到這個變量:
現(xiàn)在饱苟,我們使用 LLDB
命令達到了和 print
語句同樣的效果,得到了 result
變量的取值狼渊。那么問題又來了箱熬,這樣做有什么好處呢,怎么感覺比直接使用 print
輸出更麻煩了呢狈邑?
下面我就來告訴大家原因城须。
LLDB 探索之旅
LLDB
為我們提供了很多方便使用的命令,我們再 LLDB
命令行中米苹,輸入 help
命令即可看到這些命令的幫助信息:
Debugger commands:
apropos -- Find a list of debugger commands related to a particular
word/subject.
breakpoint -- A set of commands for operating on breakpoints. Also see
_regexp-break.
command -- A set of commands for managing or customizing the
debugger commands.
disassemble -- Disassemble bytes in the current function, or elsewhere
in the executable program as specified by the user.
expression -- Evaluate an expression (ObjC++ or Swift) in the current
program context, using user defined variables and
variables currently in scope.
frame -- A set of commands for operating on the current thread's
frames.
...............
這里我們看到了 LLDB 命令的列表,要想獲得某個命令更詳細的幫助蘸嘶,我們開可以輸入 help 命令名
, 比如我們輸入 help expression
:
help expression
Evaluate an expression (ObjC++ or Swift) in the current program context,
using user defined variables and variables currently in scope. This
command takes 'raw' input (no need to quote stuff).
Syntax: expression <cmd-options> -- <expr>
Command Options Usage:
expression [-AFLORTg] [-f <format>] [-G <gdb-format>] [-l <language>] [-a <boolean>] [-i <boolean>] [-t <unsigned-integer>] [-u <boolean>] [-v[<description-verbosity>]] [-d <none>] [-S <boolean>] [-D <count>] [-P <count>] [-Y[<count>]] [-V <boolean>] -- <expr>
expression [-AFLORTg] [-l <language>] [-a <boolean>] [-i <boolean>] [-t <unsigned-integer>] [-u <boolean>] [-d <none>] [-S <boolean>] [-D <count>] [-P <count>] [-Y[<count>]] [-V <boolean>] -- <expr>
就得到了關于 expression
命令的介紹良瞧。
基本情況就說這么多,那么咱們就來實踐一下训唱,體驗一下 LLDB
的強大之處褥蚯。
我們來看一個更強大的命令 expression
, 我們來看一下它的描述:
Evaluate an expression (ObjC++ or Swift) in the current program context,
using user defined variables and variables currently in scope. This
command takes 'raw' input (no need to quote stuff).
翻譯一下哈,意思就是在當前程序環(huán)境中况增,執(zhí)行任何的表達式赞庶,并且可以定義和操作已存在的變量。
怎么樣澳骤,讓我說的更具體吧歧强,有了 LLDB
我們不但可以在斷點處輸出某個變量的值,我們還可以修改甚至重新定義某些變量的值为肮。
咱們開始吧誊锭,將我們剛才的程序做一下修改:
var result = parseJSON("[1,2,3]");
if result?.count == 0 {
print("No Data");
}else{
print(result);
}
我們這里對 result
變量進行了判斷,并進行了分別的輸出弥锄。下面我們以讓將斷點設置到第一個語句上丧靡,然后運行程序蟆沫。再斷點處我們用 po
命令來打印出 result
的值。
這時候温治,result
中的值饭庞,應該是解析后的 JSON
數(shù)組。所以我們恢復程序執(zhí)行后熬荆,接下來的 if
判斷會走第二個分支舟山,輸出 result
中的內(nèi)容。
那么如果我們在剛才斷點時候卤恳,運行這個命令呢:
e result = []
這里我們將 result
的值修改為一個空數(shù)組累盗,然后我們繼續(xù)程序,接著你會發(fā)現(xiàn)突琳,下面的 if
判斷走了第一個分支若债,也就是說我們在斷點處對變量進行的修改,是對全局程序生效的拆融。
怎么樣這個能力是我們之前的調(diào)試方法不能達到的吧~
我們上面的 e 命令是 expression 命令的縮寫蠢琳,詳情可以參考 LLDB help 命令的幫助。
控制流快捷命令
我們繼續(xù)探索镜豹,還記得前面我們提到的幾個控制流按鈕嗎傲须,也就是這張圖片:
在 LLDB 命令行中,對于每個流程控制按鈕都有相應的命令趟脂。
-
n
命令泰讽,代表 Step Over 操作。 -
s
命令昔期,代表 Step Into 操作菇绵。 -
finish
命令,代表 Step Out 操作镇眷。 -
c
命令咬最,代表恢復程序執(zhí)行操作。
我們還是以這個程序為例欠动,這次我們使用控制流命令來進行操作:
我們運行這個程序永乌,然后在斷點檢測到時,按照下面的順序輸入 LLDB 命令:
s
n
n
c
我們第一個輸入的 s
命令具伍,會步入 parseJSON
函數(shù)的調(diào)用翅雏,然后斷點就會進入 parseJSON
函數(shù)中。隨后人芽,我們又輸入了 n
命令望几,由于 parseJSON
中只有一個 return
語句,那么控制流就會跳出 parseJSON
函數(shù)體萤厅,重新回到開始處橄抹。緊接著我們再次輸入 n
命令靴迫,這時候程序就會將 parseJSON
的結(jié)果賦值給 result
。最后我們按下 c
命令楼誓,來恢復程序的執(zhí)行玉锌。隨后的 if 判斷中就會按照相應的條件輸出內(nèi)容了。
怎么樣疟羹,這樣操作起來就比較方便了主守,我們不必用鼠標點來點去了,完全用鍵盤敲命令就可以完成控制流的操作了榄融。
另外参淫,除了這些,還有一個更加實用的控制流語句 thread return
愧杯。這個命令很有意思涎才,它不但可以使當前的函數(shù)返回,而且還可以任意修改當前函數(shù)的返回值民效,而不管傳進來的參數(shù)如何憔维。比如我們有這樣一個函數(shù):
func add(a:Int, b:Int) -> Int {
return a + b;
}
如果斷點進入這個函數(shù)體的時候涛救,我們執(zhí)行了 thread return 3
命令畏邢,那么不管這時候傳進來的兩個參數(shù)是什么,這個函數(shù)都會退出執(zhí)行检吆,并返回我們指定的值 3
舒萎。
恩。蹭沛。 這點還是有些神奇的~
斷點創(chuàng)建命令
我們除了通過用鼠標在代碼行的左邊點擊的方式創(chuàng)建斷點以外臂寝,我們還可以使用 LLDB 來創(chuàng)建斷點,比如要創(chuàng)建一個我們之前這樣的斷點:
我們可以輸入這樣一條命令:
(lldb) breakpoint set -f ViewController.swift -l 28
Breakpoint 2: where = Example`Example.ViewController.viewDidLoad (Example.ViewController)() -> () + 478 at ViewController.swift:29, address= 0x000000010f74f61e
輸入命令后摊灭,緊接著會有一行輸出咆贬,告訴我們斷點創(chuàng)建成功,并且顯示了創(chuàng)建的新斷點的位置等基本信息帚呼。
這個命令也有簡寫形式:
b ViewController.swift:28
我們還可以將斷點直接設置到函數(shù)上掏缎,假設我們有這樣一個函數(shù):
func add(a:Int, b:Int) -> Int {
return a + b;
}
我們還可以這樣設置斷點:
b add
這樣就將斷點設置再了 add
函數(shù)調(diào)用的開始位置。
我們開可以設置符號斷點,比如這樣:
b -[NSArray objectAtIndex:]
這個斷點會將所有對于 NSArray 的 objectAtIndex
方法的調(diào)用設置為斷點煤杀。這里包括我們開發(fā)者對它的調(diào)用眷蜈,以及系統(tǒng)框架內(nèi)部對它的調(diào)用。符號斷點對于調(diào)試是一個很好用的工具沈自,它能夠跟蹤那些我們引用的系統(tǒng)庫中的代碼出現(xiàn)的問題酌儒。
我們還可以對已經(jīng)創(chuàng)建的斷點設置激發(fā)條件:
我們上面設置的條件表示,只有在 result
的 count
屬性等于 0 的時候枯途,斷點才會被激發(fā)忌怎。
是不是覺得 LLDB
有點意思了呢籍滴。
剛才這一長串,給大家介紹了很多 LLDB 的基礎內(nèi)容呆躲,相信大家對 LLDB 已經(jīng)有了一個整體的了解异逐。
開始探險
那么現(xiàn)在我們就來用 LLDB
完成一些更加有意思的事情吧。
我們首先創(chuàng)建一個示例項目:
項目類型選擇 Single View Application
然后點擊 Next, 項目信息中的 Language 選擇 Swift:
點擊 Next 然后出現(xiàn)項目存儲位置的選擇插掂,選擇一個你自己的存儲位置灰瞻。接下來我們在 Main.storyboard 中拖放一個 Button 放到右上角:
隨后,我們按住 Option
鍵辅甥,然后點擊 ViewController.swift 文件酝润,可以在設計界面旁邊打開輔助界面。打開后璃弄,我們按住 Control
鍵要销,然后將我們剛剛創(chuàng)建的按鈕拖動到代碼視圖中:
然后松開鼠標按鍵,我們就會看到一個彈出菜單:
我們將 Connection 的類型選擇為 Action夏块, Name 輸入為 buttonClicked
疏咐,其他不用更改,然后點擊 Connect 按鈕脐供。這樣就完成了按鈕事件的創(chuàng)建浑塞。
接下來,我們運行應用政己,就可以看到這樣的界面了:
一切就緒酌壕,現(xiàn)在就可以展開我們的 LLDB 大法啦~
其實我們還可以不通過斷點的方式來打開 LLDB 命令行,在我們先將程序運行起來歇由,然后我們看一下調(diào)試區(qū)域的按鈕:
注意下卵牍,藍色的斷點開關按鈕右邊還有一個暫停按鈕,我們只需要點這個暫停按鈕沦泌,就可以進入 LLDB 命令行調(diào)試狀態(tài)糊昙。因為 LLDB 在 Xcode 運行中是一直駐留在后臺的,所以我們其實是可以在任何時間都可以啟動 LLDB 命令行的谢谦。
打開 LLDB 命令行后释牺,我們可以輸入這個命令,打印出當前的視圖層級(又學一招~):
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7ffcd2f0f1e0; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x7ffcd2f10170>; layer = <UIWindowLayer: 0x7ffcd2f0ea80>>
| <UIView: 0x7ffcd2c6dc10; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7ffcd2c17f10>>
| | <UIButton: 0x7ffcd2c6dfc0; frame = (20 62; 78 30); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x7ffcd2c6bc10>>
| | | <UIButtonLabel: 0x7ffcd2f15af0; frame = (16 6; 46 18); text = 'Button'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x7ffcd2f16120>>
| | <_UILayoutGuide: 0x7ffcd2c6fae0; frame = (0 0; 0 20); hidden = YES; layer = <CALayer: 0x7ffcd2c6faa0>>
| | <_UILayoutGuide: 0x7ffcd2c70740; frame = (0 667; 0 0); hidden = YES; layer = <CALayer: 0x7ffcd2c6d3d0>>
大家仔細看一下他宛,每個視圖的標識中船侧,都有一個 16進制的字符串,代表這個視圖的 ID厅各,比如這個:
UIView: 0x7ffcd2c6dc10
這個 ID 的作用非常的強大镜撩,得到了這個 ID, 我們就可以通過這個命令來得到這個視圖的引用了:
(lldb) e id $view = (id) 0x7fbd71432590
簡單解釋下,通過 expression命令(這里用縮寫形式 e
)袁梗,我們用 View 的 ID 值取得了這個 View 引用宜鸯,并將它保存到 $view 變量中。
我們得到了引用之后遮怜,就可以對這個視圖進行很多的操作了淋袖,比如我們可以在運行時改變這個視圖的背景色:
(lldb) e (void) [$view setBackgroundColor:[UIColor redColor]]
當然,我們運行完這條命令锯梁,界面上不會馬上反應出來即碗,我們還需要調(diào)用這個命令刷新一下:
(lldb) e (void)[CATransaction flush]
這樣,我們在看一下我們運行的程序陌凳,主界面的背景色變成紅色了吧剥懒。
我們甚至還可以用它來找到某些控件上添加的事件,我們找到我們自己添加的 UIBUtton 的 ID:
UIButton: 0x7ffcd2c6dfc0
然后運行下面的命令:
(lldb) e id $button = (id) 0x7ffcd2c6dfc0
(lldb) po [$button allTargets]
{(
<lldb.ViewController: 0x7feff2d67330>
)}
我們得到 UIButton 的引用后合敦,然后又輸出了他的 allTargets
屬性初橘,得到了這個 UIButton 所對應的事件 target
對象的地址,接下來我們再用剛剛得到的這個 target
地址獲取它的 action
屬性:
(lldb) po [$button actionsForTarget:(id)0x7feff2d67330 forControlEvent:0]
<__NSArrayM 0x7feff2c22350>(
buttonClicked:
)
我們這樣就得到了充岛,這個按鈕所對應的方法名了保檐。那么接下來,我們可以在這個方法上設置斷點崔梗,或者用 LLDB 的運行時能力替換這個方法的實現(xiàn)等等夜只。總之炒俱,對于我們調(diào)試應用來說盐肃,LLDB 是一個非常強大而高效的工具爪膊。這里只介紹了它的冰山一角权悟,關于更多的內(nèi)容,大家可以使用 help
命令推盛,進行深入的研究峦阁。相信大家發(fā)揮聰明才智,能夠發(fā)現(xiàn)更多它的強大之處耘成。
一點點延展榔昔,關于 Chisel
最后,再給大家延展一下瘪菌。LLDB 本身的命令系統(tǒng)非常健壯撒会,并且它還支持 Python 的腳本擴展,這樣它又有了很不錯的擴展性师妙,我們可以根據(jù)自己的需要來擴展自己的腳本诵肛。
Chisel 正是 LLDB 擴展的一個典型例子,這是由 Facebook 團隊開發(fā)的一個開源的 LLDB 的 Python 擴展集合默穴,它再 LLDB 命令的基礎上怔檩,又為我們提供了更加方便的操作接口褪秀。
比如我們要打印當前的視圖層級,如果用 LLDB 原生的命令薛训,我們需要這樣:
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
而 Chisel 為我們提供了更簡潔的接口:
(lldb) pviews
同樣的媒吗,這條用于刷新顯示的命令:
(lldb) e (void)[CATransaction flush]
Chisel 也為我們提供了簡便的接口:
(lldb) caflush
這里只給大家做一個簡單的介紹,關于 Chisel 的更多內(nèi)容乙埃,大家可以參看它的主頁:https://github.com/facebook/chisel
LLDB 自身完善的命令行系統(tǒng)闸英,以及它的擴展能力,都成為提升我們開發(fā)效率的利器介袜。正確的使用好調(diào)試工具自阱,一定會幫助我們快速的解決更多的問題。
現(xiàn)在米酬,通過 help 命令沛豌,來開始對 LLDB 命令行的探索吧,相信你能在這里發(fā)現(xiàn)更多的寶藏赃额。