来一点 SB 技巧,lldb 的 Scripting Bridge 框架,新手友好的 Debug 进阶

1,180 阅读10分钟

常常羡慕前端开发、RN 、Flutter 和 SwiftUI 开发的 live rendering, 即时渲染

别家项目的框架,UI 开发,修改了,很容易渲染出来。无需每次手动漫长的编译

其实,lldb 自带了一个 python 解析器。要调试代码逻辑,不需要重新编译, kill 进程。

当前进程下,可以直接调试。

常用的是,苹果封装的一些命令。也可以根据自己的工程,自己写定制化的 python 脚本

基础:先搞清楚常用命令的含义

常用三连,ppov

lldb 很强大,开发者常用 ppov

  • 使用 ppov, 查看当前函数帧的变量

po , expression --object-description , 使用对象描述的表达式,简单理解为 print object ,打印对象

p, expression, 就是不带对象描述的 po,

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