常常羡慕前端开发、RN 、Flutter 和 SwiftUI 开发的 live rendering, 即时渲染
别家项目的框架,UI 开发,修改了,很容易渲染出来。无需每次手动漫长的编译
其实,lldb 自带了一个 python 解析器。要调试代码逻辑,不需要重新编译, kill 进程。
当前进程下,可以直接调试。
常用的是,苹果封装的一些命令。也可以根据自己的工程,自己写定制化的 python 脚本
基础:先搞清楚常用命令的含义
常用三连,p
、po
、 v
lldb 很强大,开发者常用 p
、po
、 v
- 使用
p
、po
、v
, 查看当前函数帧的变量
po
, expression --object-description , 使用对象描述的表达式,简单理解为 print object ,打印对象
p
, expression, 就是不带对象描述的 po,
p
、po
只是 lldb 中,简单的别名。
v
, frame variable, 函数帧变量
Swift 中,编译时声明的类型,可以与运行时赋值过去的类型,不一致。
v
多了一个类型解析功能,Type Resolution
p
、po
,是根据编译时的数据类型操作。当然可以用as
做一个类型强转,cast 到运行时的类型。
v
,可以直接取出实际数据,也就是运行时的数据。( 如果编译与运行类型不一致,lldb 中需要手动输入字符串,没有代码自动补全)
- 通过
p
、po
,执行代码语句
看数据,v
优先。执行代码语句, p
、po
优先。
v
直接访问调试信息,直接读内存。步骤相对少,
p
、po
需要去解析和执行。多了一些 JITing,跑代码的工作。
这些在 Xcode 的控制台,点一下暂定按钮, 开启 lldb, 都可以知道的
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 打头的工具。
1.0 ,刷新页面,先封装一个经典的 Python 调用 lldb 小方法
常用的断点大法,是在源代码的侧边栏打断点,从函数帧里面拿信息。直接访问变量名,自带上下文。
操作简易
这里介绍 UI 全局断点大法。直接取内存。修改当前界面,挺方便的
- 进入 lldb
- 进入调试界面
- 开始调试,先拿一下地址,
( 对哪个控件,感兴趣。就去取他的地址 )
再转换地址为你需要的对象
(目前,UIKit 框架,还是 Objective-C 写的 )
换一下颜色:
先改颜色,再刷新界面
需要手动调用,因为当前进程冻结了,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
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 调试界面操作
第二步,改 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)