相信每個iOS開發者都會斷點,斷點之后,就能使用lldb
的命令進行操作,基本上每個人都用過po
這個命令吧.它在斷點的時候可以非常方便的查看各種值.但是,如果是相對復雜的場景,自定義調試命令就顯得十分必要了.
幾種方式
LLDB提供了幾種讓你自定義命令的方式:
1.別名(alias),使用簡單,但是這個只能用來實現沒有輸入的命令
2.正則命令(command regex),可以通過正則表達式來捕獲輸入,并且將其應用到命令中.但是這個在執行多行命令的時候非常不方便,并且,這個只能有一個輸入參數.
3.橋接腳本(script bridging) 基于python,這個很好的權衡了方便和復雜性.并且可以做任何LLDB能做的事情.
script bridging
就是今天我想寫的,官方文檔寫的實在是無力吐槽.文筆能力有限,如果有問題,可以追問.算是拋磚引玉了.
需求
定義一個findclass
命令,它能夠實現打印出所有運行時的類.用法如下:
1.沒有參數,則打印所有類
findclass
2.帶參數,則只打印類名中包含參數(GUI)的類
findclass GUI
準備工作
前面說了,這個腳本實際是基于python的,就目前來說(2017年07月13日16:59:15)LLDB中使用的還是python2.x
,所以為了一致性,請做個檢查.
- 檢查LLDB中python版本:
打開一個終端,輸入:
lldb
然后你會發現終端行中會出現以(lldb)
開頭的行,然后依次輸入如下命令:
script import sys
script print (sys.version)
script
表明接下來的輸入是腳本(也就是python)語句.后面的部分是python,一個導入,一個調用,不多贅述.
可以看到我的是2.7.10.
- 查看系統python版本
再開個終端,或者使用quit
退出LLDB
,輸入:
python --version
驗證是統一的,如果不統一,可以先行嘗試后面的,有問題,就改下系統python的版本吧.具體怎么改版本這里不贅述,自行Google
實現
在終端中的簡單實現
為了方便理解,我們首先在終端中用個非常簡單的函數.
1.創建目錄(這個隨意)
mkdir ~/lldb
touch findclass.py
2.編輯findclass.py(注意縮進)
def findclass(debugger,command,result,internal_dict):
print("hello lldb")
3.加載
因為我們的文件是放在我們自建目錄下,所以LLDB
根本不可能知道它的存在.所以需要經過下面幾步加載:
注意:都是打開終端,輸入lldb
進入lldb
模式之后
command script import ~/lldb/findclass.py
command : 管理LLDB自定義命令
script : 用腳本實現的自定義命令
import : 導入文件路徑
上面做的僅僅是將路徑加入到了LLDB的認知(相當于環境變量),所以如果要執行命令.還需要繼續:
script import findclass
上面做的是導入模塊.好了,已經將模塊導入了.為了方便的使用,我們還要進行下一步:
command script add -f findclass.findclass findclass
command : 管理LLDB自定義命令
script : 用腳本實現的自定義命令
add : 添加一個LLDB命令
-f : function 用于綁定一個ptyhon函數
findclass.findclass : findclass模塊(默認一個文件是一個模塊)中的findclass函數
findclass: 映射成的LLDB
命令
現在在終端輸入findclass
,終端就會輸出:
hello lldb
恭喜,這就是一個簡單的LLDB
自定義命令了!
優化
如果你剛剛手抖關閉了LLDB
的終端,當你再次輸入的時候,你會發現findclass
命令無效了.這可蛋疼了,以后每次開終端都這么弄,要死人.
我們希望,LLDB
每次啟動,都會加載我們的命令.LLDB
有個內置函數__lldb_init_module
,這是一個鉤子,當你的模塊被引入LLDB
的時候就會調用.
1.將我們的findclass.py
修改成如下樣式:
def findclass(debugger,command,result,internal_dict):
print("hello lldb")
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand('command script add -f findclass.findclass findclass')
2.編輯~/.lldbinit
,就是用戶根目錄下的.lldbinit
注意:需要打開finder的顯示隱藏文件功能.如果沒有打開,新開個終端窗口,輸入:
defaults write com.apple.finder AppleShowAllFiles -bool true
找到文件之后,在文件末尾添加:
command script import ~/lldb/findclass.py
這個文件是LLDB
初始化的時候就會調用的.這樣我們就形成了一個鏈條:
初始化文件(.lldbinit)--->加載findclass.py--->執行__lldb_init_module-->完成自定義命令添加
好了,現在無論重新關閉打開多少次,進入lldb
,這個命令也是有效的了.
如果測試確實如此,那么就可以關了終端.
在Xcode中
隨便打開一個原來項目(或者你新建一個),弄個斷點,就是為了觸發lldb.
這時候,你輸入findclass
,可以看到,控制臺有輸出了! 這就是一個自定義命令了!
真正實現功能
如果是僅僅為了打印,就不用都這么多圈子了.
現在開始實現我們的需求:
1.刪除原來的打印語句
2.定義一個要執行的表達式,當然這里就是利用OC的runtime獲取類的程序了.runtime在這里不贅述.
codeString = r'''
@import Foundation;
int numClasses;
Class * classes = NULL;
classes = NULL;
numClasses = (int)objc_getClassList(NULL, 0);
NSMutableString *returnString = [NSMutableString string];
classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
numClasses = (int)objc_getClassList(classes, numClasses);
for (int i = 0; i < numClasses; i++) {
Class c = classes[i];
[returnString appendFormat:@"%s,", class_getName(c)];
}
free(classes);
returnString;
'''
可以看到,我們把整個代碼賦值給codeString
這個字符串. r
是python的語法,為了防止轉義字符串的.如果不了解可以參考python學習之 字符串前'r'的用法
3.獲取運行結果
res = lldb.SBCommandReturnObject() #1
debugger.GetCommandInterpreter().HandleCommand("expression -lobjc -O -- " + codeString, res) #2
returnVal = res.GetOutput() #3
#1 獲取命令執行的返回值對象
#2 執行一段命令(codeString),并將命令執行結果放到返回值對象(res)中
#3 將res的輸出(可以理解為,就是剛剛OC代碼中的returnString)賦值給returnVal
4.python優化顯示
剩下的事情就簡單了,就是處理好換行.還有過濾.
我們的函數有個command
參數,這個參數就是用戶的輸入:
findclass aaa
這個aaa
就是command
參數.
resultArray = returnVal.split(",")
if not command: # 沒有輸入,顯示所有類
print returnVal.replace(",", "\n").replace("\n\n\n", "")
else:
filteredArray = filter(lambda className: command in className, resultArray)
filteredResult = "\n".join(filteredArray)
result.AppendMessage(filteredResult) #1
#1 能夠讓命令執行完,輸出filteredResult,也就是我們需要類的列表.
好了,到Xcode中試試吧!!!
注意:如果要生效,要先過了斷點,然后重新觸發斷點.
完整代碼:
import lldb
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand('command script add -f findclass.findclass findclass')
def findclass(debugger, command, result, internal_dict):
codeString = r'''
@import Foundation;
int numClasses;
Class * classes = NULL;
classes = NULL;
numClasses = (int)objc_getClassList(NULL, 0);
NSMutableString *returnString = [NSMutableString string];
classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
numClasses = (int)objc_getClassList(classes, numClasses);
for (int i = 0; i < numClasses; i++) {
Class c = classes[i];
[returnString appendFormat:@"%s,", class_getName(c)];
}
free(classes);
returnString;
'''
res = lldb.SBCommandReturnObject()
debugger.GetCommandInterpreter().HandleCommand("expression -lobjc -O -- " + codeString, res)
returnVal = res.GetOutput()
resultArray = returnVal.split(",")
if not command: # No input supplied
print returnVal.replace(",", "\n").replace("\n\n\n", "")
else:
filteredArray = filter(lambda className: command in className, resultArray)
filteredResult = "\n".join(filteredArray)
result.AppendMessage(filteredResult)
擴展更多
現在你已經有個架子了,以后想實現什么自定義命令,就先用OC的runtime實現了,然后替換上面例子中的OC代碼,保證有個正確的返回值就好了.歡迎大家留言分享創意.