Bootstrap

Unity Android 调用 so 卡死问题 (so 编译踩坑)

问题描述
把 Unity 工程编译到 Android 上运行后,出现了一个必现的界面卡死bug。

表现为:每次轮到自己出大招时,必现界面卡死,但程序不会 crash。在 Unity 编辑器和 iOS 下都无法重现。

分析日志发现,卡死前最后调用了算法库中的一个函数。
算法库是 c++ 写的,用 cmake 组织,编译成 so 给 C# 层调用。

定位
怀疑是哪里死循环了。
想试下能否通过工具来定位,使用 Android Studio 的 Thread-Dump 、Unity Attach(死活不成功)发现帮助都不大。
最后还是在算法库里添加了大量日志后,发现在形如下面的代码中死循环!

for(char i = 0;i >=0;–i)
{
i = -1;
printf(“i0 = %d\n”,i);
i += 1;
printf(“i1 = %d\n”,i);
}
上面代码合理应该输出 -1,0 ,结束循环。
然而,在 Android 下却死循环了!结果是 255,0,255,0,255,…,感觉像是溢出后回环了

分析
google 一下发现:
在 x86 下,char 默认是 有符号。
在 ARM 架构下,char 默认是 无符号,即 Android 这种情况。i = 0,–i 变成 -1,无符号下就是 1111 1111 = 255,再+1就溢出了,变成0,无限循环。
而 iOS 虽然也是 ARM 架构,但它的 char 默认却是有符号的,所以也没出问题。

死循环代码快速定位方法

定位死循环代码可以是一个挑战,尤其是在大型项目中。以下是一些快速定位死循环的有效方法和工具,帮助你更高效地找到问题所在:

1. 使用日志记录

  • 添加日志:在循环的关键位置添加日志输出,记录循环的迭代次数和变量的状态。这可以帮助你确定循环是否在按预期运行。

    for (int i = 0; i < MAX_ITERATIONS; ++i) {
        printf("Iteration: %d\n", i);
        // 其他逻辑
    }
    
  • 条件日志:在循环中添加条件日志,记录特定条件下的变量值,以便更好地理解循环的行为。

2. 使用调试器

  • 设置断点:在循环的开始和结束处设置断点,观察变量的变化。如果循环没有正常结束,可以逐步调试,查看变量的状态。

  • 观察变量:在调试器中观察循环变量的值,查看它是否在预期范围内变化。

3. 使用线程转储(Thread Dump)

  • 生成线程转储:在 Android Studio 或其他 IDE 中生成线程转储,查看当前线程的状态。死循环的线程通常会在某个位置反复执行相同的代码。

  • 分析堆栈:分析线程堆栈,查找循环的调用栈,确定死循环的具体位置。

4. 使用静态分析工具

  • 静态分析:使用静态分析工具(如 Clang-Tidy、Cppcheck)来检查代码中的潜在问题。这些工具可以帮助识别可能导致死循环的代码模式。

5. 代码审查

  • 同行审查:让其他开发者审查你的代码,可能会发现你自己未注意到的问题。不同的视角可能会帮助识别潜在的逻辑错误。

6. 使用时间限制

  • 超时机制:在循环中添加超时机制,如果循环运行超过预定时间,则输出警告并退出。这可以帮助你在开发过程中快速发现死循环。

    auto start = std::chrono::high_resolution_clock::now();
    while (condition) {
        // 循环逻辑
        auto now = std::chrono::high_resolution_clock::now();
        if (std::chrono::duration_cast<std::chrono::seconds>(now - start).count() > TIME_LIMIT) {
            printf("Loop exceeded time limit!\n");
            break;
        }
    }
    

通过结合这些方法,你可以更快速地定位和解决死循环问题。希望这些建议对你有所帮助!如果你有其他问题或需要进一步讨论的内容,欢迎继续交流!

问题背景

在 Android NDK 开发中,char 类型的符号性可能会因平台而异。在 x86 架构上,char 默认是有符号的,而在 ARM 架构上,char 默认是无符号的。这种差异可能导致在某些情况下出现意想不到的行为,例如所遇到的死循环问题。

解决方案

  1. 使用 CMake 显式指定 char 符号性

    • 在 CMakeLists.txt 文件中,可以通过添加编译选项来确保 char 被视为有符号类型。可以在 CMake 文件中添加以下行:
    add_compile_options(-fsigned-char)
    

    或者,如果你使用的是 target_compile_options,可以这样写:

    target_compile_options(your_target_name PRIVATE -fsigned-char)
    
  2. 使用 CMake 的 CMAKE_C_FLAGSCMAKE_CXX_FLAGS

    • 你也可以直接在 CMakeLists.txt 中设置全局的 C 和 C++ 编译标志:
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsigned-char")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsigned-char")
    

其他建议

  • 使用标准类型

    • 为了避免与平台相关的类型问题,建议使用 C++ 标准库中的固定宽度整数类型,如 std::int8_tstd::uint8_t。这些类型在所有平台上都是一致的,能够避免因符号性引起的问题。
  • 编写文档

    • 在项目文档中记录下关于 char 符号性的注意事项,特别是在跨平台开发时,帮助团队成员理解潜在的陷阱。
  • 测试和验证

    • 在不同平台上进行充分的测试,确保代码在所有目标平台上都能正常工作。可以使用 CI/CD 工具来自动化这一过程。
  • 代码审查

    • 在代码审查过程中,特别注意数据类型的使用,确保在跨平台代码中使用了合适的类型。

通过这些方法,可以有效减少因数据类型符号性引起的问题,提高代码的可移植性和稳定性。如果你有其他问题或需要进一步讨论的内容,欢迎继续交流!

;