來一點 SB 技巧,lldb 的 Scripting Bridge 框架,新手友好的 Debug 進階

經常羨慕前端開發、RN 、Flutter 和 SwiftUI 開發的 live rendering, 即時渲染html

別家項目的框架,UI 開發,修改了,很容易渲染出來。無需每次手動漫長的編譯前端

其實,lldb 自帶了一個 python 解析器。要調試代碼邏輯,不須要從新編譯, kill 進程。python

當前進程下,能夠直接調試。git

經常使用的是,蘋果封裝的一些命令。也能夠根據本身的工程,本身寫定製化的 python 腳本github

基礎:先搞清楚經常使用命令的含義

經常使用三連,ppov

lldb 很強大,開發者經常使用 ppovexpress

  • 使用 ppov, 查看當前函數幀的變量

po , expression --object-description , 使用對象描述的表達式,簡單理解爲 print object ,打印對象bash

p, expression, 就是不帶對象描述的 po,app

ppo 只是 lldb 中,簡單的別名。框架


v, frame variable, 函數幀變量編輯器

Swift 中,編譯時聲明的類型,能夠與運行時賦值過去的類型,不一致。

v 多了一個類型解析功能,Type Resolution

ppo,是根據編譯時的數據類型操做。固然能夠用as 作一個類型強轉,cast 到運行時的類型。

v ,能夠直接取出實際數據,也就是運行時的數據。( 若是編譯與運行類型不一致,lldb 中須要手動輸入字符串,沒有代碼自動補全)

po 與 p 和 v 的不一樣.png


  • 經過 ppo,執行代碼語句

看數據,v 優先。執行代碼語句, ppo 優先。

v 直接訪問調試信息,直接讀內存。步驟相對少,

ppo 須要去解析和執行。多了一些 JITing,跑代碼的工做。


這些在 Xcode 的控制檯,點一下暫定按鈕, 開啓 lldb, 均可以知道的

0.png

help 大法好

  • 看看 p
(lldb) help p
     Evaluate an expression on the current thread. 
     // ...
     'p' is an abbreviation for 'expression --'
     // p 是 expression -- 的縮寫
複製代碼
  • 看看 po
(lldb) help po
     Evaluate an expression on the current thread.  Displays any returned value
     // ...
     'po' is an abbreviation for 'expression -O --'
    // po 是 expression -O  -- 的縮寫
複製代碼
  • 看看 v
(lldb) help v
    // v 怎麼用的,說的很清楚
    // ...
    'v' is an abbreviation for 'frame variable'
   // v 是 frame variable 的縮寫
複製代碼

簡單的例子:

代碼

struct Trip{

    var name: String

    var destinations: [String]



}

extension Trip: CustomDebugStringConvertible{

    var debugDescription: String{

        "Trip 的描述"

    }

}

func one(){

    let cruise = Trip(name: "Alone", destinations: ["Pain", "Great", "V", "me"])

    // 斷點於此

    print("Place Holder")

}

one()

print("Place Holder")

複製代碼

結果:

(lldb) po cruise

▿ Trip 的描述

  - name : "Alone"

  ▿ destinations : 4 elements

    - 0 : "Pain"

    - 1 : "Great"

    - 2 : "V"

    - 3 : "me"

(lldb) expression --object-description -- cruise

▿ Trip 的描述

  - name : "Alone"

  ▿ destinations : 4 elements

    - 0 : "Pain"

    - 1 : "Great"

    - 2 : "V"

    - 3 : "me"

(lldb) p cruise

(One.Trip) $R18 = {

  name = "Alone"

  destinations = 4 values {

    [0] = "Pain"

    [1] = "Great"

    [2] = "V"

    [3] = "me"

  }

}

(lldb) expression cruise

(One.Trip) $R20 = {

  name = "Alone"

  destinations = 4 values {

    [0] = "Pain"

    [1] = "Great"

    [2] = "V"

    [3] = "me"

  }

}

複製代碼
程序報錯,能夠 bt 一下

bt,看下當前函數調用狀況

bt, thread backtrace, 線程函數調用棧回溯

lldb 的擴展性很好。能夠經過 Python 腳本,執行不少定製化的、動態的工做。

Xcode 的側邊欄,打斷點。是一個 debug 程序邏輯,很是好的入口。

關心的源代碼旁邊打斷點,對於代碼的執行時機的控制,很是強力。

側邊欄斷點的不利之處:

添加大段代碼,就不是很方便了。不是像文本編輯器那樣好寫。寫錯了,修改不便

並且斷點裏面,寫好的執行代碼,對複製粘貼大法,不友好。

側邊欄斷點,易添加,易刪除。複用不是很好。保存與編輯,仍是文件可靠

lldb 裏面內嵌了 python 的解釋器,使用 python 腳本操縱 lldb, 經過各類 SB 打頭的工具。

0


1.0 ,刷新頁面,先封裝一個經典的 Python 調用 lldb 小方法

經常使用的斷點大法,是在源代碼的側邊欄打斷點,從函數幀裏面拿信息。直接訪問變量名,自帶上下文。

操做簡易

這裏介紹 UI 全局斷點大法。直接取內存。修改當前界面,挺方便的

  • 進入 lldb

0.png

  • 進入調試界面

1.png

  • 開始調試,先拿一下地址,

( 對哪一個控件,感興趣。就去取他的地址 )

再轉換地址爲你須要的對象

(目前,UIKit 框架,仍是 Objective-C 寫的 )

2.png

換一下顏色:

先改顏色,再刷新界面

須要手動調用,由於當前進程凍結了,RunLoop 卡住了

本來 RunLoop 會自動處理 CATransaction, 作 FPS

(lldb) po ((UILabel *)0x100f2c100).backgroundColor = [UIColor redColor]
UIExtendedSRGBColorSpace 1 0 0 1

(lldb) po [CATransaction flush]
 nil
複製代碼

( 用的真機調試,效果很清楚 )

刷新界面,是常規操做。能夠用一個別名。這裏介紹 python 腳本命令

刷新頁面的 Python 代碼:

下面的這個是,很經典的代碼

def flush(debugger, command, result, internal_dict):
    debugger.HandleCommand("e (void)[CATransaction flush]")
複製代碼

應該很老的緣故,如今跑不通了。 會報錯:

error: property 'flush' not found on object of type 'CATransaction'

能夠當僞代碼看看。

我從 Chisel 找了些代碼

  •  使用統一的調用慣例,函數的參數約定
  • 先獲取當前的 frame, 執行上下文
  • 配置執行選項,執行代碼語句
import lldb

def flushUI(debugger, command, result, internal_dict):
    frame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame()
    options = lldb.SBExpressionOptions()
    options.SetLanguage(lldb.eLanguageTypeObjC)
    options.SetTrapExceptions(False)
    options.SetTryAllThreads(False)
    frame.EvaluateExpression('@import UIKit', options)
    frame.EvaluateExpression("(void)[CATransaction flush]", options)
複製代碼
先把代碼保存在 python 文件裏面,try_one.py
  • 引入文件路徑,
  • 引入文件名,就是 module
  • 將 python 方法,映射到命令行的命令
  • 以後調用,就能夠了
(lldb) command script import /Users/jzd/lldb/try_one.py 
 // 這個是我電腦的路徑,拖文件到 Xcode 編輯區,生成文件路徑

OK for the first
 //  這個細節,不用在乎 

(lldb) script import try_one
(lldb) command script add -f try_one.flushUI flush
(lldb) flush
(lldb) po ((UILabel *)0x104a25900).backgroundColor = [UIColor redColor]
UIExtendedSRGBColorSpace 1 0 0 1

(lldb) flush
複製代碼

以下圖,上面代碼作的事情,就是改 UILable 的背景色,刷新 UI

11.png

1.1, 簡化流程

lldb 有一個內置的方法 __lldb_init_module,

lldb 載入該模塊的時候,會作一些裏面的配置工做。

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f try_one.flushUI flush')
    print("OK for the first")
複製代碼
接着配置, lldb 每次啓動,能夠經過 ~/.lldbinit 文件,自動加載模塊

~/.lldbinit 文件,若是沒有,本身建立)

裏面輸入

command script import /Users/jzd/lldb/try_one.py
// 請填入你的路徑
複製代碼

這樣就取代了手動的三步

(lldb) command script import /Users/jzd/lldb/try_one.py 
(lldb) script import try_one
(lldb) command script add -f try_one.flushUI flush
複製代碼

2.0 閱讀蘋果的移動控件腳本源代碼,nudge

nudge 是一個改控件中心點座標的 python 腳本

調 UI layout 細節的時候,頗有用。要改控件幾個 pt 的位置,反覆編譯, run 程序,使人崩潰

nudge 解決了這個場景的問題

第一步,跑起來

先引入

~/.lldbinit 文件中,輸入

command script import /Users/jzd/lldb/nudge.py 
// 請替換爲,你的文件路徑
複製代碼

跑程序,會報錯

error: module importing failed: Missing parentheses in call to 'print'. Did you mean print('The "nudge" command has been installed, type "help nudge" for detailed help.')? (nudge.py, line 204)
  File "temp.py", line 1, in <module>
複製代碼

這裏有一個 Python 的編譯錯誤,由於 Python 的語法錯誤

// 把最後一行 
 print 'The "nudge" ... ` // 改成 print('The "nudge"  ... `)
複製代碼

再對模塊作一下刷新, 就行了

(lldb) command source ~/.lldbinit
Executing commands in '/Users/jzd/.lldbinit'.
nege OK, ready to nudge
(lldb) command script import /Users/jzd/lldb/nudge.py 
The "nudge" command has been installed, type "help nudge" for detailed help.

// nudge 命令,安裝好了

複製代碼

就開始調用 nudge,修改控件位置

(lldb) nudge 100 0 0x127d23710
Total offset: (100.0, 0.0)
(CGRect) $17 = (origin = (x = 20, y = 12), size = (width = 120.5, height = 20.5))

(lldb) nudge 100 0 0x127d23710
Total offset: (100.0, 0.0)
(CGRect) $20 = (origin = (x = 120, y = 12), size = (width = 120.5, height = 20.5))
(lldb) 
複製代碼
  • 前兩步,跟上面的同樣,先進入 UI 調試界面

  • 進入 UI 調試界面操做

222.png

第二步,改 bug

上面兩行調用 nudge 的代碼,可看出一個 bug.

第一處,調用

(lldb) nudge 100 0 0x127d23710
複製代碼

沒有生效。

反覆測試發現,加載 nudge 後,

第一次調用,改位置,不成功

傳進去的參數 Total offset 正常,

獲取控件的座標 (CGRect) 正常,

以後的操做,都正常。

調試源代碼

實際作事情的是這個方法,

def nudge(self, x_offset, y_offset, target, command_result):
複製代碼

該方法裏面,註釋很清楚

# 先經過設置中心點,修改控件的位置
        # Set the new view.center.
        setExpression = "(void)[(UIView *)%s setCenter:(CGPoint){%f, %f}]" %(self.target_view.GetValue(), center_x, center_y)
        target.EvaluateExpression(setExpression, exprOptions)
        
        # 改完 UI, 叫 CoreAnimation 去刷新界面
        # Tell CoreAnimation to flush view updates to the screen.
        target.EvaluateExpression("(void)[CATransaction flush]", exprOptions)
複製代碼

設置中心點以後,先刷新下 UI 就行了

先執行這個,再走上面的邏輯

target.EvaluateExpression("(void)[CATransaction flush]", exprOptions)
複製代碼

這樣解決了 RunLoop 的問題。CPU 繪製,GPU 渲染。RunLoop 維護 FPS 刷新界面

看源代碼的設計

調用界面圖,相似上

(lldb) nudge 10 0 0x102e23510
Total offset: (10.0, 0.0)
(CGRect) $17 = (origin = (x = 30, y = 12), size = (width = 120.5, height = 20.5))
(lldb) nudge 10 0 0x102e23510
Total offset: (20.0, 0.0)
(CGRect) $20 = (origin = (x = 40, y = 12), size = (width = 120.5, height = 20.5))
(lldb) nudge 10 0 0x102e23510
Total offset: (30.0, 0.0)
(CGRect) $23 = (origin = (x = 50, y = 12), size = (width = 120.5, height = 20.5))
(lldb) nudge 10 0 
Total offset: (40.0, 0.0)
(CGRect) $25 = (origin = (x = 60, y = 12), size = (width = 120.5, height = 20.5))
(lldb) nudge 10 0 
Total offset: (50.0, 0.0)
(CGRect) $27 = (origin = (x = 70, y = 12), size = (width = 120.5, height = 20.5))
(lldb) nudge 10 0 
Total offset: (60.0, 0.0)
(CGRect) $29 = (origin = (x = 80, y = 12), size = (width = 120.5, height = 20.5))
複製代碼
nudge 命令,還能夠不傳 View( 若是以前,已經傳了 )。調控件偏移,做用於以前的 View 上

讀源代碼,業務邏輯,關心的是,有沒有能夠執行的視圖,不關心第三個參數。

  • 命令行說,能夠不傳。

由於 python 命令行庫 argparse , 項目這麼配置的

def create_options(self): 方法中,這麼配置參數

# 必定要傳兩個 float, 
      # nargs 爲 2,要傳兩個參數
      # type 爲 float,兩個參數,做爲 float 來處理
      # Parse two floating point arguments for the x,y offset.
        self.parser.add_argument(
            'offsets',
            metavar='offset',
            type=float,
            nargs=2,
            help='x/y offsets')

        # nargs 爲 *,他的參數是任意個,0 個、1 個與多個
        # type 爲 str,他的參數,都做爲 str 來處理
        # Parse all remaining arguments as the expression to evalute for the target view.
        self.parser.add_argument(
            'view_expression',
            metavar='view_expression',
            type=str,
            nargs='*',
            help='target view expression')
複製代碼

argparse 的使用,python 文檔,寫的很清楚

  • 業務邏輯說,能夠不傳。

由於 NudgeCommand 這個類裏面,有一個屬性 target_view, 持有傳進去的 view.

class NudgeCommand:
    target_view = None 
複製代碼
  • 調用的入口方法裏面,有一段處理第三個參數的邏輯
def __call__(self, debugger, command, exe_ctx, result):
       // ...
       # If optional 3rd argument is supplied, then evaluate it to get the target view reference.
        if len(args.view_expression) > 0:
            view_expression = ' '.join(args.view_expression)
            expr_error = self.evaluate_view_expression(view_expression, target)
       # 若是有第三個參數,經過判斷第三個參數的長度
       # 就把第三個參數傳進去,走這個方法 evaluate_view_expression
複製代碼
  • 接着看方法 evaluate_view_expression
def evaluate_view_expression(self, view_expression, target):
        # Expression options to evaluate the view expression using the language of the current stack frame.
        exprOptions = lldb.SBExpressionOptions()
        exprOptions.SetIgnoreBreakpoints()

        # Get a pointer to the view by evaluating the user-provided expression (in the language of the current frame).
        # 字符串 view_expression,轉當前語言的指針
        result = target.EvaluateExpression(view_expression, exprOptions)
        
        if result.GetValue() is None:
            return result.GetError()
        else:
            # 判斷以前有沒有 target_view, 
            # 若是有 ,是否是同一個視圖
            # 若是不是,以前保留的其餘數據,清 0
            if self.target_view and self.target_view.GetValue() != result.GetValue():
                # Reset center-point offset tracking for new target view.
                self.original_center = None
                self.total_center_offset = (0.0, 0.0)
            # 將視圖的指針,賦值給屬性 target_view
            self.target_view = result
            return None     # No error
複製代碼
  • 最後,看業務邏輯, __call__ 方法,調用 nudge 方法
def __call__(self, debugger, command, exe_ctx, result):
      # 走真正作事的邏輯以前
      # 裏面有這麼一段
      // ...
      # Cannot continue if no target view has been specified.
      # 若是拿不到 target_view,就直接報錯了
        if self.target_view is None:
            result.SetError("No view expression has been specified.")
            return
       # 拿到了 target_view,纔會往下走,走到去改位置,去 nedge 
        # X and Y offsets are already parsed as floats.
        x_offset = args.offsets[0]
        y_offset = args.offsets[1]

        # We have everything we need to nudge the view.
        self.nudge(x_offset, y_offset, exe_ctx.target, result)
複製代碼

nudge 的源代碼,功能完善,至關簡單

第三步,加功能,怎樣復原修改的位置

本來只有一個邏輯,用參數修改位置, 如今要加邏輯,直接復原修改

就要用到 python 命令行庫 argparse 的選項功能,options

這裏設計是,直接輸入 nudge ,就把以前對 View 的 Layout 操做, 都撤銷了

原來必須給命令,傳兩個參數的設計,留不得

  • 首先把添加命令的兩個參數,改爲添加命令的選項的兩個參數

def create_options(self): 方法中,

self.parser.add_argument("-c","--center",type = float, dest = 'offsets', help = "x/y offsets",  default = [0, 0], nargs = 2)

# 默認給的 offsets,是一個 python 的 list, 裏面是兩個 0
複製代碼
  • 第二步,修改業務邏輯 根據不一樣的輸入,作不一樣的事情。 加了一個參數,要不要撤銷
def nudge(self, x_offset, y_offset, target, command_result, toReset):
      #...
      # 若是要撤銷,就改一下偏移的邏輯,
      # 改回去
      if self.original_center is not None and toReset:
            center_x = self.original_center[0]
            center_y = self.original_center[1]
        else:
            # Adjust the x,y center values by adding the offsets.
            center_x += x_offset
            center_y += y_offset

複製代碼
  • 第三步,連起來

__call__ 方法,調用 nudge 方法的邏輯,稍做加工,就行了

# ,,, 
        toReset = False 
        # 就是看,輸入的偏移,是否是默認值。
        # 若是不是,就走撤銷改偏移的邏輯
        if x_offset == 0 and y_offset == 0:
            toReset = True
        # We have everything we need to nudge the view.
        self.nudge(x_offset, y_offset, exe_ctx.target, result, toReset)
複製代碼

這樣調用:

(lldb) nudge 0x105426bf0
Total offset: (0.0, 0.0)
(CGRect) $1a7 = (origin = (x = 20, y = 12), size = (width = 155.5, height = 20.5))

// 移動位置
(lldb) nudge -c 20 0 0x105426bf0
Total offset: (20.0, 0.0)
(CGRect) $20 = (origin = (x = 40, y = 12), size = (width = 155.5, height = 20.5))

// 撤銷移動位置
(lldb) nudge 
Total offset: (0.0, 0.0)
(CGRect) $22 = (origin = (x = 20, y = 12), size = (width = 155.5, height = 20.5))

// 移動位置
(lldb) nudge -c 20 0 
Total offset: (20.0, 0.0)
(CGRect) $24 = (origin = (x = 40, y = 12), size = (width = 155.5, height = 20.5))

// 撤銷移動位置
(lldb) nudge 
Total offset: (0.0, 0.0)
(CGRect) $26 = (origin = (x = 20, y = 12), size = (width = 155.5, height = 20.5))
(lldb) 
複製代碼

代碼見github

相關文章
相關標籤/搜索