调试指南

作为开发者,您偶尔会在代码中遇到问题,包括逻辑错误、API 使用不当或其他各种问题。本指南将教您如何调试代码、检测一些常见问题并找到解决方案。

背景

糟糕!出现严重错误。我的程序崩溃了,或者输出了乱码,或者似乎永远运行下去。现在该怎么办?

初学者倾向于责怪编译器、库或除自身代码以外的任何东西。经验丰富的程序员也希望这样做,但他们知道,实际上,大多数问题都是他们自己的错。

幸运的是,大多数错误都很简单,可以通过简单的技术找到。检查错误输出中的证据,并尝试推断它是如何产生的。查看崩溃前的任何调试输出;如果可能,从调试器获取堆栈跟踪。现在您了解了一些发生的事情以及发生的地点。停下来反思一下。怎么会发生这种情况?从崩溃程序的状态倒推,以确定可能导致这种情况的原因。

调试涉及逆向推理,就像解决谋杀之谜一样。发生了一些不可能发生的事情,唯一可靠的信息是它确实发生了。因此,我们必须从结果倒推,找出原因。一旦我们有了完整的解释,我们就会知道要修复什么,并且在此过程中可能会发现一些我们没有预料到的事情。

这段摘自《程序设计实践》的内容强调了在进行调试任务之前收集足够数据和事实的重要性。有时,这意味着消除可能的原因以找到真正的问题。其他时候,这是一种间接解决问题的方法。

使用 Solar2D 编辑器

Solar2D 编辑器 是一个附加Sublime Text 的功能。这允许您在 Sublime Text 中查看调试消息,而无需控制台窗口。它还允许您使用交互式调试器验证代码。

安装 Sublime Text 和 Solar2D 编辑器后,它可以帮助您调试代码。例如,您可以在某些点停止模拟器并检查代码的状态,或者您可以单步执行代码并检查变量值。此外,您可以跟踪应用程序的**堆栈跟踪**,以进行更全面的调试。

使用断点

使用交互式调试器的一个关键概念是能够在某些点停止程序,以便您可以观察当前状态。这包括查看作用域内变量的值、一次单步执行一行代码或一个函数,以及检查堆栈跟踪。这称为**使用断点**,它都围绕着在您想要检查的点停止程序。

您可以通过以下两种方式之一启用或禁用(切换)断点

  • 单击代码行,然后从主菜单中选择 **Solar2D 编辑器** → **调试器** → **切换断点**。

  • 右键单击代码行,然后从弹出上下文菜单中选择 **切换断点**。

设置断点后,该行上将出现一个小图标。然后,当您通过 **Solar2D 编辑器** → **调试器** → **运行** 从 Sublime Text 运行应用程序时,它将在到达此行代码时停止并返回 Sublime Text。此时,您有几个选项

  • **运行** — 运行代码直到下一个断点或直到完成。

  • **单步跳过** — 这将向前单步执行一行代码,如果它是一个函数,它将完成该函数并停止在下一行代码上。

  • **单步进入** — 这将向前单步执行一行代码,如果它是一个函数,它将进入该函数并停止在该函数的第一行。

  • **切换断点** — 关闭断点或设置新的断点。

  • **检查变量** — 请参阅下面的检查变量

  • **停止** — 停止程序。

检查变量

从主菜单中,选择 **调试器** → **检查变量**。Sublime Text 底部将打开一个搜索栏,您可以输入任何有效的变量。然后,您将看到该变量的详细信息,如果它是一个表,则与通常可用的内容相比,这可以提供更好的可视化表示。

堆栈跟踪

有时,了解错误的来源很有帮助。当您调用一个函数并且它行为异常时,一个常见的原因是调用函数传递了无效数据。通过使用**堆栈跟踪**,您可以快速识别哪一行代码调用了您当前正在执行的函数。更具体地说,您可以在该函数中设置一个断点,继续运行您的应用程序,当该函数退出并到达下一个断点时,您可以检查用于调用该函数的变量。

有关堆栈跟踪的更多信息,请参阅下面的查看运行时错误

控制台/设备调试

查看运行时错误

如果您在模拟器首选项中启用了**显示运行时错误**,则运行时错误(导致应用程序崩溃的错误)将中止程序并显示一个消息对话框,其中包含有关错误的更多信息,包括

  • 发生错误的文件名,例如 `main.lua`。
  • 文件中发生错误的行号,例如第 218 行.
  • 错误原因,例如尝试对 nil 值执行算术运算。.
  • **堆栈跟踪**可以帮助您找到错误的来源,例如
stack traceback:
    main.lua:218: in function 'start'
    main.lua:222: in main chunk

这些集体信息通常可以精确定位代码中问题所在的确切位置。在本例中,您可以打开 `main.lua` 文件,找到第 `218` 行,并查找其中一个值为 `nil` 的数学运算 - 特别是尝试对未定义的变量执行数学运算的位置。此外,堆栈跟踪显示错误位于名为 `start` 的函数内,并且该函数是从第 `222` 行调用的。

有时,堆栈跟踪不太具体,必须仔细跟踪。例如

?: in function <?:221>
    main.lua:218: in function 'start'
    main.lua:222: in main chunk

由此,您知道错误位于第 `221` 行,但您必须向后跟踪以获取更多详细信息。在这种情况下,问题发生在第 `221` 行,并且该行是从 `main.lua` 的第 `218` 行名为 `start` 的函数调用的。因此,即使堆栈跟踪的第一行没有精确定位问题发生的位置,它通常也可以暴露错误的根源。

调试符号

为 iOS 构建时,您可以选择**开发**构建或**分发**构建。在为 Android 构建时,您可以使用**调试**密钥库或**发布**密钥库进行构建。通常,您的选择会影响堆栈跟踪中可用的信息,如下所示

  • 如果您使用开发配置文件或调试密钥库进行构建,Solar2D 将在 Lua 代码中保留调试符号。这会导致最小的性能影响和更大的应用程序包。

  • 如果您使用分发配置文件或发布密钥库进行构建,Solar2D 将从 Lua 代码中去除调试符号。

如果您希望保留这些符号,无论构建类型如何,只需覆盖默认行为,如此处所示。这将使跟踪代码更容易,但如果错误发生在 Solar2D 核心代码内部,调试符号仍然不可用。在这种情况下,您需要向后跟踪并确定您传递给 Solar2D 库函数的信息导致错误的原因。

Solar2D 模拟器控制台

诊断输出也可以在**模拟器控制台**中查看。这里将显示有用的调试消息,以及代码中 `print()` 语句的值。虽然这种做法对某些开发人员来说可能已经过时,但跟踪 `print()` 命令的输出通常可以揭示错误的原因或条件逻辑的问题。

Xcode 模拟器 — macOS

上述方法可以帮助诊断大多数编码问题,但可能需要 **Xcode 模拟器**(适用于 macOS)来预览应用程序在实际 iOS 设备上的运行情况。这有助于模拟模拟器无法模拟的一些设备功能,但它仍然不如真实设备准确。

要在 Xcode 模拟器中测试应用程序

  1. 在 Solar2D 模拟器中选择**文件** → **构建** → **构建 iOS…**。

  2. 在对话框窗口中,在**构建目标:**下拉菜单中,选择 **Xcode 模拟器**。

  3. 继续构建过程,应用程序将在模拟设备窗口中打开。

  4. 从此应用程序中,选择**调试** →打开系统日志…以查看系统日志。

设备调试 — iOS

有些错误只有在将应用程序安装到设备上后才会出现。幸运的是,每个设备都有自己的控制台日志,您可以访问它,对于 Apple,您可以在 Xcode 中查看此控制台日志,如下所示

  1. 如果 Xcode 尚未运行,请打开它。

  2. 打开**设备和模拟器**窗口(**窗口** → **设备和模拟器**)。

  3. 将设备连接到您的计算机,并等待它出现在窗口的左列中。出现后,选择它。

  4. 单击**打开控制台**按钮以打开设备的控制台日志。

设备调试 — Android

在 Android 上进行调试更像是“面向命令行”的,但是一旦您弄清楚了命令,安装应用程序、在应用程序运行时查看日志以及查找错误消息就相当快了。

首先,您必须从 Google 安装一些名为**Android 调试桥**的免费工具。只需按照以下步骤操作即可

  1. 访问下载页面。不要单击大的下载按钮,而是单击**下载选项**链接。然后,在仅限命令行工具部分中,为您的系统选择正确的选项。或者,您可以从平台工具页面下载适用于您操作系统的 SDK 平台工具。

  2. 安装工具。在 Windows 上,运行安装程序;在 macOS 上,解压缩文件并将文件夹移动到合适的位置。

  3. 按照添加 SDK 包的说明进行操作。至少安装Android SDK 工具Android SDK 平台工具.

安装工具后,您可以使用命令行工具。连接您的 Android 设备后,只需在终端 (macOS) 或命令提示符 (Windows) 中键入以下命令,即可查看显示的消息。此命令将过滤掉除 Solar2D 生成的消息之外的所有消息。

adb logcat Corona:v *:s

有时,Solar2D 以外的其他因素也会产生错误,例如 Google Play 与 Google应用内购买相关的消息。要查看整个日志,请改用以下命令

adb logcat

这将阻止过滤Solar2D 特定的活动,但这会生成大量消息,并且很难找到与问题相关的消息。因此,在代码中您怀疑存在问题的区域附近添加一些 print() 语句可能会有所帮助。例如

print( "MYAPP - Purchase: *********************************************" )
store.purchase( "com.coronalabs.NewExampleInAppPurchase.NonConsumableTier1" )

如果您更喜欢可视化工具,adb 还提供了一个 GUI 版本的adb logcat称为monitor。您可能需要将设备设置为“开发者模式”才能使用这些工具,并且每个设备和 Android 版本的显示方式都不同,因此我们建议您在线搜索您的特定操作系统和“开发者模式”以找到说明。

调试 — Win32 桌面

如果您正在开发 Win32 桌面应用程序,可以在此处找到有关调试的一些详细信息。

调试 — macOS 桌面

如果您正在开发 macOS 桌面应用程序,可以在此处找到有关调试的一些详细信息。

技巧和常见陷阱

有一些常见问题,一旦您了解它们,通常很容易解决。

正确的缩进

代码可读性最重要的实践是正确的缩进(在行前添加制表符或空格以区分代码块)。您应该使用能够自动处理缩进的文本编辑器或集成开发环境 (IDE)。考虑以下代码

local function doSomething( params )
if params.someValue == 10 then
if params.someOtherValue == "test" then
params.yetAnotherValue = params.someValue
end
end

如果不缩进代码行以显示不同的代码块,则很难阅读。实际上,如果代码缩进正确,则会发现一个明显的错误。比较一下区别

local function doSomething( params )
    if params.someValue == 10 then
        if params.someOtherValue == "test" then
            params.yetAnotherValue = params.someValue
        end
    end

如您所见,doSomething 函数缺少 end 关键字,并且在代码正确缩进后,这一点就清晰地显现出来了。

许多代码编辑器可以通过自动缩进代码来帮助您,并且当您关闭一个代码块时,它会退回一级并将 end 放在正确的位置。每个操作系统都有一系列代码编辑器可供选择,而更好的编辑器则具有 Lua 语法高亮显示功能。有关推荐编辑器的列表,请参阅我们针对 macOSWindows 的安装指南。

描述性命名

另一个重要方面是命名变量和函数,以便您和其他人知道它们的含义。考虑以下两个代码块

local function a( b )
    local c = b.target
    if c.x < 100 then
        c.alpha = 0
    end
    return true
end
local function touchEventHandler( event )
    local target = event.target
    if target.x < 100 then
        target.alpha = 0
    end
    return true
end

这些函数在行为上是相同的,但是通过使用更清晰的变量和函数名称,大多数开发人员可以很容易地理解您想要完成的任务。

设备注意事项

一些开发人员沮丧地发现他们的项目可以在模拟器中运行,但在实际设备上却无法运行。通常,以下因素之一是罪魁祸首

  • 文件名不区分大小写。在 macOS 或 Windows 上进行测试时,文件名不区分大小写,但在设备上运行时,文件名必须大小写匹配。例如,文件名 titleimage.pngTitleImage.PNG 在模拟器中是相同的,但在设备上被认为是不同的。

  • build.settings 文件中存在错误,这通常是因为各个块和表没有正确嵌套。请参阅项目构建设置指南,了解如何正确构造 build.settings 文件及其中的每个部分。

  • 插件存在问题。每个插件都必须按照指定的方式包含,并且一些插件需要设备权限才能正常运行。请参阅相应的插件文档了解更多详细信息。

移除监听器

在某些情况下,当您尝试删除侦听器时,事件侦听器函数可能仍在运行,或者当您尝试调用某个操作时,Solar2D 库将完成一个内部进程。例如,您可能尝试在 GPS 事件仍在处理时删除GPS侦听器,或者您可能尝试在物理实体与另一个对象碰撞的那一刻停用它。这两种情况都可能导致错误/警告。

解决这类问题的办法是在短暂的计时器延迟后执行必要的操作

local function handleGPS( event )
    timer.performWithDelay( 10, function() Runtime:removeEventListener( "location", handleGPS ); end )
end

社区支持

查找解决方案

如果您已经尝试了所有上述调试方法,请前往论坛。在那里,您可以搜索描述您遇到的相同问题的主题。通常,其他开发人员会报告相同的问题,并且在许多情况下,帖子中会包含解决方案——因此,请不要在没有先在现有帖子中搜索答案的情况下发布新主题。

请注意,新贡献者的第一篇帖子将受到审核——这些帖子将被录入系统,但在版主批准之前,其他用户将无法看到它们。

请求协助

如果您在之前的任何论坛帖子中都找不到解决方案,请发布新的请求并遵循以下准则

  • 不要多次发布相同的主题。此外,请选择最合适的子论坛发布您的主题,并避免在多个子论坛

  • 中发布相同的主题。 请提供足够的信息,以便社区能够提供帮助。您应提供以下信息

    •  您遇到的问题的详细描述。
    •  如有必要,请提供问题区域周围的简明代码。
    •  您使用的是 macOS 还是 Windows。
    •  您是为 iOS、Android、macOS 桌面还是 Windows 桌面构建应用程序。
    •  您正在使用的 Solar2D 版本/构建版本。
    •  控制台日志中的相关错误消息。
    •  可以帮助社区直观了解问题的屏幕截图/图像。
  • 在论坛帖子中包含代码时,请使用 [lua] [/lua] 标签将其括起来,以提高清晰度和可读性

[lua]
...
[/lua]
  • 要包含屏幕截图或图像,请单击消息输入框下方的更多回复选项按钮。在下一个屏幕上,使用附件部分上传图像文件。对于 YouTube 或 Vimeo 视频,只需将视频 URL 与消息一起粘贴即可。