Android 11 - 本机 C++ 库的 System.loadLibrary 需要 60 秒以上,在 Android 10 及以下版本上运行速度非常快

Android 11 - System.loadLibrary for native C++ library takes 60+ seconds, works perfectly fast on Android 10 and below

在我们基于游戏引擎 cocos2d-x 的 Android 游戏应用程序中,大部分代码是用 C++ 编写的,我们有一个非常奇怪和关键的问题,因为 Android 11:

当原生库在 onLoadNativeLibraries 中加载时,现在突然需要 60 多秒。在 Android 11 之前,一切正常,加载时间为 0.2-3 秒。现在当你开始游戏时,你有一个 60+ 秒的灰色屏幕。

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

这是 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 11 中的一个错误,由 google 修复但尚未部署。

同时,如果您不关心在程序运行时在您的库中调用 staticthread_local 变量析构函数exiting/library 被卸载,将标志 -fno-c++-static-destructors 传递给编译器。 (使用 clang 注释查看更详细的解决方案的长答案)

我在我的项目(不是 cocos2d)中使用了这个标志,没有任何问题,而且库的加载速度比以前更快。

长答案:

不幸的是,这是 Google 团队在 android 11 (R) 中引入的性能回归。 google here.

正在跟踪此问题

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

因为Android11(R),android中这个函数的实现有changed:

  • In Q, __cxa_atexit uses a linked list of chunks, and calls mprotect twice on the single chunk to be modified.
  • In R, __cxa_atexit calls mprotect twice on a single contiguous array of handlers. Each array entry is 2 pointers.

当它们是许多 C++ 全局变量时,此更改会显着降低性能,这在 cocos2d so 库中似乎就是这种情况。

Google 已经实施了修复 https://android-review.googlesource.com/c/platform/bionic/+/1464716 但是 如问题所述:

this won't be in Android 11 until the March QPR at the earliest, and since this isn't a security issue it won't be mandatory for OEMs to actually take that patch.

Google 团队还建议 workarounds 通过删除或跳过全局变量的析构函数在应用程序级别:

  • For a particular global variable, the [[clang::no_destroy]] attribute skips the destructor call.
  • Pass -fno-c++-static-destructors to the compiler to skip the destructors for all static variables. This flag also skips destructors for thread_local variables. If there are thread_local variables with important destructors, those can be annotated with [[clang::always_destroy]] to override the compiler flag.
  • Pass -Wexit-time-destructors to the compiler to make it warn on every instance of an exit-time destructor, to highlight where the __cxa_atexit registrations are coming from.