Android 11 - System.loadLibrary 加载本地 C++ 库需要60秒以上,但在 Android 10 及以下版本上运行非常快

8

在我们基于cocos2d-x游戏引擎、大部分代码由C++编写的Android游戏应用程序中,自从Android 11以后,我们遇到了一个非常奇怪且关键的问题:

当本地库在onLoadNativeLibraries中被加载时,现在突然需要60秒以上的时间。在Android 11之前,一切都正常,加载只需要0.2-3秒。现在在启动游戏时,你会看到一个持续60秒以上的灰色屏幕。

我们已经发现,在这60秒停顿结束后,JNI_OnLoad被直接调用。

下面是onLoadNativeLibraries函数的代码:

protected void onLoadNativeLibraries()
{
    try
    {
        ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
        Bundle bundle = ai.metaData;
        String libName = bundle.getString("android.app.lib_name");
        System.loadLibrary(libName); // line of 60 seconds stall
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
}

我们已经尝试了时间分析,但没有取得任何成功。它只是显示这个函数花费了很多时间。同时,通过调试暂停也没有提供任何进一步的线索。本地调试器在C++代码方面不显示任何内容。
有谁知道为什么会发生这种情况,或者我们可以尝试什么来找出原因?任何帮助都将不胜感激 :)

你在清单文件中是否明确设置了 android:extractNativeLibs?你使用的是哪个 Android Gradle 插件版本进行构建的?发布模式和调试模式下的加载时间是否相同? - Michael
@Michael 不,我们不这样做 - 我们应该将它设置为什么?我们使用 gradle-5.6.4。在发布模式下也非常耗时。 - keyboard
我指的是Android Gradle插件。你在项目级别的build.gradle文件中指定为依赖项的那个插件。它应该有一个以3或4开头的版本号。 - Michael
此外,这种情况是否总是发生?还是只有在安装新版本应用程序后的第一次发生? - Michael
@Michael 抱歉,确实有道理。我们有classpath 'com.android.tools.build:gradle:3.6.1'。 是的,这总是发生。百分之百的时间。 - keyboard
1个回答

7

简短回答:

这是Android 11中的一个错误,已被Google修复但尚未部署。

同时,如果您不关心程序退出/库卸载时是否调用静态线程局部变量的析构函数,请向编译器传递标志-fno-c++-static-destructors。(有关使用clang注释进行更细粒度解决方案,请参见长答案)

我在我的项目(不是cocos2d)上使用了此标志,并且库的加载速度甚至比以前更快,没有任何问题。

长篇回答:

不幸的是,这是由Google团队在Android 11(R)中引入的性能回归。该问题正在由Google跟踪here

总结一下,当调用System.loadLibrary()时,系统会使用__cxa_atexit()为加载的库中包含的每个C ++全局变量注册一个析构函数。

自 Android 11(R)起,Android 中此功能的实现方式已经 更改

  • 在 Q 版本中,__cxa_atexit 使用一系列链接块,并对要修改的单个块调用 mprotect 两次。
  • 在 R 版本中,__cxa_atexit 在一个连续的处理程序数组上调用 mprotect 两次。每个数组条目都是 2 个指针。

当存在许多 C++ 全局变量时(这似乎是 cocos2d so 库的情况),这种变化会严重降低性能。

Google 已经实现了修复方法https://android-review.googlesource.com/c/platform/bionic/+/1464716 ,但正如问题中所述:

这不会在最早的三月 QPR 中出现在 Android 11 中,而且由于这不是安全问题,OEM 实际上不需要采用该补丁。

Google团队还建议在应用程序级别上采取一些解决方法,例如删除或跳过全局变量的析构函数:
  • 对于特定的全局变量,[[clang::no_destroy]]属性会跳过析构函数的调用。
  • 将-fno-c++-static-destructors传递给编译器,以跳过所有静态变量的析构函数。此标志还会跳过线程本地变量的析构函数。如果有具有重要析构函数的线程本地变量,可以使用[[clang::always_destroy]]进行注释,以覆盖编译器标志。
  • 将-Wexit-time-destructors传递给编译器,使其在每个退出时析构函数实例上发出警告,以突出显示__cxa_atexit注册来自何处。

“-fno-c++-static-destructors” 似乎完美地解决了问题。非常感谢。我们已经尝试了各种技巧进行了4天的调试,但都无济于事。谢谢 :) - keyboard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接