LLDB 是 Xcode 中自帶的一個除錯工具,在開發的過程中使用好了這個除錯工具,不僅是能力的一種提升,更是一種裝逼的 神器。
一、如何進入 LLDB
通常當程式 crash 或者有斷點的時候,會自動的變成 LLDB 模式。也可以手動 處理,直接點選這裡:
也會變成 LLDB 模式。最終的效果是這樣的:二、使用 LLDB
2.1 expr 指令
這個指令的意思,能實時的執行程式碼中的程式碼邏輯。就像下面這樣的:
當點選下一步執行的時候,NSLog 列印的值是 CoderHG 而不是 Coder。這個功能想想都感覺挺不錯的。
2.2 call
這個指令與 expr 類似,呼叫一行程式碼,形如這樣的:
call self.view.backgroundColor = [UIColor redColor];
2.3 列印
其實關於列印,應該所有的小夥伴都知道的。接著上面的步驟,做如下的操作:
在 LLDB 中有兩個常見的列印指令 p 與 po。
- 1、p 通常用於列印基本資料型別的值。這個指令會預設生出一個臨時變數,如**$1**,學習過 Shell 的小夥伴看到這個應該很激動。
- 2、po 列印變數的內容,如果是物件,其列印的內容由 -debugDescription 決定。
2.4 操作記憶體
對記憶體的操作,無非就是讀寫操作。 修改記憶體中的值:
memory write 記憶體地址 數值
如:memory write 0x7ffee685dba8 25
讀取記憶體操作:
memory read/數量 _ 格式 _ 位元組數 記憶體地址
或者
x/數量 _ 格式 _ 位元組數 記憶體地址
2.4.1 格式
- x :代表16進位制
- f :代表浮點數
- d :代表10進位制
2.4.2 位元組大小
- b :byte 代表1個位元組
- h :half word 代表2個位元組
- w :word 代表4個位元組
- g :giant word 代表8個位元組
如:
memory read/1wx 0x7ffee14a5ba8 memory read/1wd 0x7ffee14a5ba8
寓意是:讀取 0x7ffee14a5ba8 中 4 個位元組的內容。 示例如下:
2.5 bt
bt 返回所有的呼叫棧, 形如:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
* frame #0: 0x000000010758b6fd LLDBDev`-[ViewController viewDidLoad](self=0x00007fedad7057e0, _cmd="viewDidLoad") at ViewController.m:27
**中間被我幹掉了很多。**
frame #34: 0x000000010758b79f LLDBDev`main(argc=1, argv=0x00007ffee8674108) at main.m:14
frame #35: 0x000000010c2d1d81 libdyld.dylib`start + 1
frame #36: 0x000000010c2d1d81 libdyld.dylib`start + 1
複製程式碼
這個指令很強大,現在的 Xcode 在這裡都顯示不全了:
所以只能藉助 bt 指令。
三、實戰
沒有實戰的紙上談兵,都是耍流氓。 在開始之前,先定義一個 Class,程式碼如下所示:
#import <Foundation/Foundation.h>
@interface HGObject : NSObject
/** 年齡 */
@property (nonatomic, assign) NSInteger age;
/** 身高 */
@property (nonatomic, assign) NSInteger height;
@end
#import "HGObject.h"
@implementation HGObject
@end
複製程式碼
很簡單的一個Class。
3.1 檢視一個物件的 isa 指標
大家都說一個instance 物件中的 isa 的值就是當前 instance 物件的 class 物件的值,接下來求證一下。先寫一段簡單的程式碼:
Class cls = NSClassFromString(@"HGObject");
id obj = [[cls alloc] init];
NSLog(@"%@, %@", cls, obj);
複製程式碼
執行程式碼發現:
- 1、cls 沒有顯示具體的地址值。
- 2、在 obj 中也根本沒有看到 isa 這個成員變數。
看不到任何的地址顯示,所以只能是藉助 LLDB 除錯工具,這裡既是是使用簡單的 p 或者 po指令都是不可以的。需要藉助上面說的 操作記憶體 的指令。
輕鬆搞定,上圖中是不是就說明了一個 Class 物件的 instance 物件的 isa 的值就是其 class 物件本身的值呢?是的,本來就是這樣的。
3.2 物件中的地址檢視
簡單的實現如下程式碼:
HGObject* obj = [[HGObject alloc] init];
obj.age = 18;
obj.height = 2;
NSLog(@"%@", obj);
複製程式碼
在 NSLog 處打一個斷點,執行程式碼,開啟記憶體檢視檢視:
剛開啟是這樣的:
將 obj 的地址寫入,再看下面這張圖:
看到上面的記憶體圖,發現一個規律,請看下圖:
上圖中的記憶體分佈,如紅框框所示,分別是 isa,_age 與 _height。為了驗證其正確性,可以修改一下其中的值,看一下效果:
上圖中的邏輯大概為:查詢 _age 與 _height 對應的地址,然後修改其地址的值,然後重新整理看記憶體檢視。
四、記憶體斷點
顧名思義,給一個記憶體打一個斷點。其實在開發中,還是挺實用的,有點像 KVO 監聽。下圖為簡單的測試程式碼,圖中在 viewDidLoad 有一個手動打的一個斷點,目的是進入 lldb 環境:
進入 lldb 環境之後,我們可以執行如下指令:
watchpoint set variable self->_name
log 為:
Watchpoint created: Watchpoint 1: addr = 0x7fcfaf9061d0 size = 8 state = enabled type = w
watchpoint spec = 'self->_name'
new value: 0x0000000000000000
複製程式碼
即為斷點成功。修改 _name的值,即為修改 _name 所在記憶體的值,所以應該是這樣的:
值得注意的是,log 能將舊值、新值一起列印出來。當然,設定記憶體斷點的指令,還可以這樣:
watchpoint set expression &_name
記憶體斷點, 在分析資料流轉的特別有用,比如就想知道某個變數在什麼情況下為 nil 了。
五、UI 控制元件檢視
這個功能可厲害了,先看一個問題:
其實這個就是我們在做自動佈局是後的一個問題,在控制檯會給出這樣的提示。如果介面簡單,那麼很好排查,如果介面複雜,那麼就很難定位問題所在。那麼如何找到具體的檢視呢?可以這樣來:
這樣就能實時的定位到是介面上的哪個UI 了,具體的命令如下:
(lldb) e id $hgView = (id)0x7fdfc66127f0
(lldb) e (void)[$hgView setBackgroundColor:[UIColor redColor]]
(lldb) e (void)[CATransaction flush]
複製程式碼
注意:後面的那個命令一定要執行,否則在 lldb 的狀態下是看不到效果的。
六、動態注入程式碼邏輯
看到這個小標題就很詭異,意思就是在程式碼執行的過程中如何去通過修改程式碼的方式修改程式碼的邏輯。有點繞,先簡單的舉個例子。
6.1 場景一
有如下的程式碼:
主要看 yesOrNo
屬性,當在程式碼已經執行起來的時候,想重新執行,就要把 yesOrNo
的值給改成 true
。你會怎麼做?其實在上面已經介紹了,使用 expr
可以搞定,但是這裡還有可以有更高階的。
如下圖所示,弄一個斷點,雙擊讓斷點變成編輯狀態。
寫上 expression yesOrNo = true
會發現這樣的話不用每次執行到這個斷點的時候都去 lldb 一次。
選中了這個核取方塊,都感覺不到斷點的存在。
這樣的斷點, 是不是很高大上!??
6.2 場景二
就想知道某個屬性的值是在什麼時候改變的,應該怎麼辦呢?因為很多的時候,某個屬性的改變會發生在很多的地方,那如何做到統一的跟蹤呢?
在很多年前我是這樣做的:重寫 set
方法,然後在 set
方法中打一個斷點。這樣做是很優秀的,但是也是最不雅觀的。因為重寫了 set
方法之後,還需要再次刪除,太 TM 繁瑣了。何嘗不弄一個斷點呢?再者說,如果去跟蹤系統屬性的變動呢?接下來就介紹一個比較牛逼的方法。以監聽 -[UILabel setText:]
方法為例。
第一步就是盤她:
然後再這樣的盤她:
-[UILabel setText:]
這一句大家都能看懂。之後是這樣子的:
這樣之後,會驚奇的發現,什麼也沒有幹,這個斷點就觸發了:
是的,這是彙編程式碼。本來 UILabel 中的系統方法都是沒有暴露實現的,跳到這樣的介面也是很正常的。這到底是怎麼回事呢?我們可以簡單的來一波下面的操作: 從上面的這張圖中,就能看出端倪。在看到這樣強大的斷點方式的同時,也帶來了很多的思考。這樣打斷點就顯得有點流氓
了,只要是這個方法被觸發的地方都會被斷點到。那麼就看一下下面這張圖:
我只想監聽在 btn1
方法中被觸發的 -[UILabel setText:]
,應該怎麼辦?在實際的開發中, 可能 btn1
方法會很複雜。直接給出最終的處理方案:
這張圖似曾相識。其中關鍵的命令是這樣的 breakpoint set -one-shot true --name "-[UILabel setText:]"
這句命令的大意是如果在 btn1
方法中有 -[UILabel setText:]
操作的話,會被自動觸發跟蹤。
具體這樣的斷點有什麼用,可以隨便的腦補一下。
七、小節
一個常見的修復週期就是修改程式碼,編譯,重新執行,並且祈禱出現最好的結果。
熟悉一些常見的 LLDB 除錯技巧之後,在實際的開發中可以節省我們一大堆的除錯時間。