背景
GPU 驱动自升级,是在以下两个痛点下催生出来的:
- 换机频率降低,手机的生命周期越来越长
智能手机已经不再是一个增量市场,变成一个存量市场。消费者换机频率逐年降低,手机生命周期变长。生命周期变长了以后,那么如何更好地维护老机型就变成一个难题 - 开发者需要升级 ROM 才能用到 GPU driver 的新功能
一直以来,游戏开发者想要升级到最新的 GPU 驱动的渠道跟普通的用户一样,只能靠 OEM 厂商的推送系统更新,他们没有一个渠道能够直接拿到最新的 GPU 驱动,因此无法第一时间对驱动的新特性进行适配。如何建立一个渠道能够让游戏开发者能够及时获取到最新的 GPU 驱动也是一个难题
有痛点就会有需求,GPU 驱动自升级就是针对这两个痛点提出的解决方案。
不过,这个方案要落地还会遇到很多实际问题:首先,GPU 驱动跟整个操作系统依赖极大,影响的模块很多,如 Display,Camera,Media 等,稍有不慎整个系统就会不稳定甚至 crash。
同时,这个功能要真正落地,还涉及到多方:GPU 驱动来自 SoC 厂商;Android 的 OEM 厂商等。每一方里涉及到厂商的又十分众多。因此在确定方案的时候需要保证能够有足够的兼容性。
以上这么多难题注定了这个方案从提出到落地,需要花很长的时间,走很长的路。
而事实也是如此,从 2017 年开始,Android O 提出了 Project Treble,引入了 VNDK 和 linker namespace,那时候其实已经在对这个方案进行技术铺垫了;再到 2018 年的 Android P,谷歌在 framework 加了很多 dummy code,此时整个方案的框架已经基本完成;再到 2019 年的 Android Q,这个方案作为 Q 的核心功能跟大家见面了。而小米十周年旗舰——小米 10/小米 10 Pro 也成为国内第一款落地 GPU 驱动自升级功能的 Android 智能手机。
在 GPU 驱动自升级方案中,可升级的 GPU 驱动分为两种,一种称为 Game Driver,面向的普通用户,主要是针对游戏这种十分依赖 GPU 的场景进行特殊优化,稳定的 driver;另一种称为 Prerelease Driver,面向游戏开发者,目的是让游戏开发者能够提前获取到包含新特性的,不稳定的驱动,进行提前的适配和利用。这两种驱动分别封装在两个 apk,分别通过两个 property 来分区:ro.gfx.driver.1
指定的是 Game Driver 的包名,ro.gfx.driver.1
指定的是 Prerelease Driver 的包名。
需要说明的是,虽然称为 apk,但是实际上更多地可以理解为是两个盛放 driver 的容器,apk 本身不具备任何的代码,而之所以要将其打包为 apk,更多的是为了利用 Android 的签名机制以及利用 PMS 能够对所有安装的 apk 进行统一的管理。
将 apk 解包以后可以放在整个包的结构如下:
├── AndroidManifest.xml
├── assets
│ ├── sphal_libraries.txt
│ └── whitelist.txt
├── lib
│ ├── arm64-v8a
│ └── armeabi-v7a
└── res
分别说明一下各个结构的作用:
- sphal_libraries.txt
能够被gfx driver
访问的,sphal
中的库列表,后面会详细讲解 - whitelist.txt
使用 Game Driver 的包名白名单 - lib
driver 本体,分为 32 位和 64 位,根据应用的 abi 选择加载对应的 driver
Game Driver 和 Prerelease Driver (后面统称为 updated driver)这两个 apk 的结构是一样的,只不过 lib 文件夹里面的驱动不一样而已。
updated driver 配置流程
这部分讲讲在 app 启动的时候,是怎么给 app 挑选对应的 GPU driver 的。
app 在启动的过程中,经过下面的流转(详细的流程省略,非本文主题):
ActivityThread::handleBindApplication()
\__ActivityThread::setupGraphicsSupport()
\__GraphicsEnvironment.setup()
最终走到核心代码:
public void setup(Context context, Bundle coreSettings) {
final PackageManager pm = context.getPackageManager();
final String packageName = context.getPackageName();
....
if (!chooseDriver(context, coreSettings, pm, packageName)) {
setGpuStats(SYSTEM_DRIVER_NAME, SYSTEM_DRIVER_VERSION_NAME, SYSTEM_DRIVER_VERSION_CODE,
SystemProperties.getLong(PROPERTY_GFX_DRIVER_BUILD_TIME, 0), packageName,
getVulkanVersion(pm));
}
}
核心函数 chooseDriver()
:
private static boolean chooseDriver(
Context context, Bundle coreSettings, PackageManager pm, String packageName) {
// PART 1
final String driverPackageName = chooseDriverInternal(context, coreSettings);
if (driverPackageName == null) {
return false;
}
......
}
chooseDriverInternal()
chooseDriverInternal()
,这个函数的主要作用是,根据系统的配置,为当前启动的 app 判断是否需要加载 Game/Prerelease Driver,如果需要那么返回对应的 driver apk 的包名;如果不需要,那么返回 null
,表明当前启动的 app 只需要加载 system gpu driver。判断的逻辑:
private static String chooseDriverInternal(Context context, Bundle coreSettings) {
// 判断 game driver 的 property 是否配置了
final String gameDriver = SystemProperties.get(PROPERTY_GFX_DRIVER);
final boolean hasGameDriver = gameDriver != null && !gameDriver.isEmpty();
// 判断 prerelease driver 的 property 是否配置了
final String prereleaseDriver = SystemProperties.get(PROPERTY_GFX_DRIVER_PRERELEASE);
final boolean hasPrereleaseDriver = prereleaseDriver != null && !prereleaseDriver.isEmpty();
// 两个 property 都没有配置,前面提到这两个 property 代表的是两个包名
// 如果都没有配置,那么说明这项功能没有使能,使用 system gpu driver
if (!hasGameDriver && !hasPrereleaseDriver) {
if (DEBUG) Log.v(TAG, "Neither Game Driver nor prerelease driver is supported.");
return null;
}
// To minimize risk of driver updates crippling the device beyond user repair, never use an
// updated driver for privileged or non-updated system apps. Presumably pre-installed apps
// were tested thoroughly with the pre-installed driver.
// 不是所有的 app 都可以使用这个功能的,如果当前是预装应用,那么也不会对其使用该功能
final ApplicationInfo ai = context.getApplicationInfo();
if (ai.isPrivilegedApp() || (ai.isSystemApp() && !ai.isUpdatedSystemApp())) {
if (DEBUG) Log.v(TAG, "Ignoring driver package for privileged/non-updated system app.");
return null;
}
// 接下来看下面的流程图:
在判断完当前的 app 需要使用的 driver 类型以后,返回到 chooseDriver()
。
chooseDriver()
private static boolean chooseDriver(
Context context, Bundle coreSettings, PackageManager pm, String packageName) {
final String driverPackageName = chooseDriverInternal(context, coreSettings);
if (driverPackageName == null) {
return false;
}
// PART 2
final PackageInfo driverPackageInfo;
// 容纳 game driver 的 apk 必须是一个 system app,否则会直接跳过
try {
driverPackageInfo = pm.getPackageInfo(driverPackageName,
PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "driver package '" + driverPackageName + "' not installed");
return false;
}
// Android O 及以后,谷歌提出了 Project Triple,其中包括 VNDK,规定了 GPU driver 必须是在 sphal 的 linker space
// 因此 driver apk 的 targetSdkVersion 必须大于等于 O
// O drivers are restricted to the sphal linker namespace, so don't try to use
// packages unless they declare they're compatible with that restriction.
final ApplicationInfo driverAppInfo = driverPackageInfo.applicationInfo;
if (driverAppInfo.targetSdkVersion < Build.VERSION_CODES.O) {
if (DEBUG) {
Log.w(TAG, "updated driver package is not known to be compatible with O");
}
return false;
}
// 判断当前运行 app 是 32 位的还是 64 位的,以便加载正确 abi 的 gpu driver
final String abi = chooseAbi(driverAppInfo);
if (abi == null) {
if (DEBUG) {
// This is the normal case for the pre-installed empty driver package, don't spam
if (driverAppInfo.isUpdatedSystemApp()) {
Log.w(TAG, "updated driver package has no compatible native libraries");
}
}
return false;
}
final StringBuilder sb = new StringBuilder();
sb.append(driverAppInfo.nativeLibraryDir)
.append(File.pathSeparator);
sb.append(driverAppInfo.sourceDir)
.append("!/lib/")
.append(abi);
final String paths = sb.toString();
// 读取 driver apk 里面的 sphal_libraries.txt 的内容
final String sphalLibraries = getSphalLibraries(context, driverPackageName);
if (DEBUG) {
Log.v(TAG,
"gfx driver package search path: " + paths
+ ", required sphal libraries: " + sphalLibraries);
}
// 配置阶段最重要的一个函数,下面详解
setDriverPathAndSphalLibraries(paths, sphalLibraries);
......
return true;
}
其中,path
这个变量存放了当前 app 的 GPU Driver 的加载路径,类似于:
/system/app/xxx/lib/:/system/app/xxx/xxx.apk!/lib/armeabi-v7a/
包括两部分,使用 : 分隔,一部分是 driver apk 所在的 lib 路径,另一个的 driver apk 包内部的 lib 路径,请注意这里有一个特殊的符号:**!/**,这个符号的意义后面会详细说明。
在确定了启动的 apk 需要用到的 driver apk 以后,最终会走到一个最重要的函数:setDriverPathAndSphalLibraries()
,通过这个函数,带上两个参数—— driver path 和 sphal_libraries.txt 里面的内容。查看这个函数的定义,其实这个是一个 JNI 函数,真正的实现在这里(frameworks/native/libs/graphicsenv/GraphicsEnv.cpp
):
void GraphicsEnv::setDriverPathAndSphalLibraries(const std::string path,
const std::string sphalLibraries) {
......
mDriverPath = path;
mSphalLibraries = sphalLibraries;
}
这里就是简单地将这两个变量存起来。至此,updated driver 的配置流程就走完了,而要看这两个变量到底怎么用,得看下面的加载流程。
VNDK 和 linker namespace
在说明加载流程之前,需要说明一下 linker namespace,这个是 GPU 驱动自升级用到的核心技术,而这个技术是在 Google 在 Android O 的 Project Treble 引入的。要理解这个技术的作用先要理解一下 Project Treble。
Project Treble 将系统拆分解耦为两部分——system 和 vendor,诣在分离 android framework 和硬件驱动的耦合,system 分区只存放原生 android 相关的内容,vendor 分区存放厂商相关定制的内容。目的是为了达到在保持 vendor 分区不变的情况下,仅通过升级 system 分区即可升级到新版本的 Android。前面提到 system 和 vendor 要保持独立,不能相互依赖。但是也不可能完全独立,双方还是存在一些依赖的,。基于这样的需求,将 system 分区的 so 分为以下:
- FMK-ONLY
framework 专有库,称为 FMK-ONLY,vendor 无法访问 - LLNDK
API/ABI 稳定的,system 和 vendor 共享一套的库,称为 LLNDK - VNDK
vendor 如果需要访问到一部分的 FMK-ONLY 的库,那么可以通过 VNDK。VNDK 是专门给 vendor 模块维护的一部分 system 库,位于/system/lib[64]/vndk-${version}/
,在 FMK-ONLY 可以找到同名的库,但是两者的符号表是不一致,是 FMK-ONLY 的同名不同副本。如下图: - VNDK-SP
VNDK-SP 属于 VNDK, 是 SP-HAL 维二能够依赖的模块(另一个是上面提到的 LLNDK)。而 SP-HAL 是 system 访问 vendor 的唯一途径,位于 vendor 分区。如果此时 system 和 vendor 需要涉及到相同的库,那么 vendor 调用的就是 VDNK-SP,路径是/system/lib[64]/vndk-sp-${version}/
。
这么说可能比较难以理解,举个例子:
surfaceflinger 属于 system 的进程,但是当它要进行 GPU 合成的时候,需要访问位于 vendor 分区的 GPU 驱动,此时需要通过 SP-HAL——libGLES_${chipset}.so
,而此时 surfaceflinger 和libGLES_${chipset}.so
都要依赖libcutils.so
,我们就称libGLES_${chipset}.so
依赖的libcutils.so
为 VNDK-SP。如下图:
用一张图展示上面各个名词在各个分区的位置:
通过上面的例子可以看到,在 surfaceflinger 其实是有两个 libcutils.so
的副本的,两个库名字一样,但是符号表完全不一样,如何正确加载不引起冲突呢?
linker namespace 就是为解决这个问题而提出的技术。LLNDK 和 VNDK-SP 的 libcutils.so
分别隶属于不同的 liner namespace,这样就可以避免冲突问题,各自加载各自的库。降维理解就是:linker namespace 相当于 shell 的 $PATH
环境变量。当我们在 shell 里面敲入 java
的时候,shell 会从 $PATH
里面一个一个寻找 java
可执行二进制文件。同样的, so 库在被加载的时候同样有一个类似环境变量的东西(称为“搜索路径”)告诉 linker 链接器去哪里找到这个 so,而 linker namespace 相当于就是通过创建两个隔离的空间,不同的空间有不同的搜索路径,不同的进程从不同的空间加载 so 库,从而避免了加载库的时候出现的名字一致的问题。详细的在后面的加载流程会具体说明。
driver 加载时机
在 Android Q 上,EGL/OpenGL driver 加载时机有两种,一种是 zygote 在启动的时候会先将他们进行预加载,另一种是 Activity 在启动的时候也会去进行加载,下面分别说明:
zygote 预加载 GPU driver
我们知道,所有的 Android 进程都是由 zygote 或者 zygote64 孵化出来的,而当 zygote 和 zygote64 在启动的时候,会去将 gpu driver 进行预加载(省略了 zygote 启动的完整流程,与主题无关):
frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
ZygoteInit::main()
\__ ZygoteInit::preload()
\__ZygoteInit:maybePreloadGraphicsDriver()
\__ZygoteInit::nativePreloadGraphicsDriver()
然后通过 JNI 走到:frameworks/base/core/jni/com_android_internal_os_ZygoteInit.cpp
的 android_internal_os_ZygoteInit_nativePreloadGraphicsDriver
:
void android_internal_os_ZygoteInit_nativePreloadGraphicsDriver(JNIEnv* env, jclass) {
ScopedSCSExit x;
// 这里通过判断 debug.hwui.renderer 这个 property 的值:
// skiavk: hwui 使用 Vulkan
// skiagl: hwui 使用 OpenGL
if (Properties::peekRenderPipelineType() == RenderPipelineType::SkiaGL) {
eglGetDisplay(EGL_DEFAULT_DISPLAY);
}
}
然后通过下面路径:
eglGetDisplay()
\__egl_init_drivers()
\__egl_init_drivers_locked()
\__Loader::open()
走到最终的 driver 加载函数 Loader::open()
。接下来看第二种:
activity 加载 GPU driver
当启动一个 Activity 的时候会调用 startActivity
的方法,这个方法会 Binder 调用到 system_server AMS,AMS 作为中转经过一系列处理后会 Binder 调用到目标进程 ApplicationThread
(Activity 的启动这里就不展开了,与主题无关),最终会调用到 ActivityThread
的 handleLaunchActivity
方法,而 gpu driver 就是在这里加载的:
ActivityThread::handleLaunchActivity()
\__HardwareRenderer::preload()
这里的 preload()
这一个 JNI 方法,具体的实现流程:
android_view_ThreadedRenderer_preload()
\__RenderProxy::preload()
\__RenderThread::preload()
而 RenderThread::preload()
里面会去调用 eglGetDisplay()
:
void RenderThread::preload() {
// EGL driver is always preloaded only if HWUI renders with GL.
if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaGL) {
std::thread eglInitThread([]() { eglGetDisplay(EGL_DEFAULT_DISPLAY); });
eglInitThread.detach();
} else {
requireVkContext();
}
HardwareBitmapUploader::initialize();
}
最终也会走到 Loader::open()
这个函数,在这个函数完成真正完成的加载流程:
void* Loader::open(egl_connection_t* cnx)
在说明加载流程之前,得先说明一下 egl_connection_t
这个结构体,整个加载过程就是对这个结构体的填充赋值,因此需要对这个结构体有深刻地理解:
egl_connection_t
struct egl_connection_t {
enum {
GLESv1_INDEX = 0,
GLESv2_INDEX = 1
};
inline egl_connection_t() : dso(nullptr),
libEgl(nullptr),
libGles1(nullptr),
libGles2(nullptr),
systemDriverUnloaded(false) {
......
}
void * dso;
gl_hooks_t * hooks[2];
EGLint major;
EGLint minor;
EGLint driverVersion;
egl_t egl;
// Functions implemented or redirected by platform libraries
platform_impl_t platform;
void* libEgl;
void* libGles1;
void* libGles2;
// 当需要使能 updated driver 的时候,需要将默认的 gpu driver unload,再加载 updated driver
// 当 updated driver 加载完成以后,会将 systemDriverUnloaded 设置为 true
bool systemDriverUnloaded;
......
};
结构体中有几个重要的成员变量:
void * dso
存放的其实是这个结构的地址:struct driver_t { explicit driver_t(void* gles); ~driver_t(); // returns -errno int set(void* hnd, int32_t api); void* dso[3]; };
主要的是
void *dso[]
:dso[0]
存放了 libEGL.so 的句柄(dlopen
的返回值,后面一致);dso[1]
存放了 libGLESv1_CM.so 的句柄;dso[2]
存放了libGLESv2.so
的句柄。作用是判断 GPU driver 是否已经加载了。gl_hooks_t * hooks[2]
hooks[0] 保存了所有 GLESv1_CM 函数的符号地址;hooks1 保存了所有 GLESv2 函数的符号地址egl_t egl
egl 保存了所有 EGL 函数的符号地址void * libEgl/libGles1/libGles2
保存了libEGL.so
,libGLESv1_CM.so
,libGLESv2.so
这三个 wrapper 库的句柄
updated driver 加载流程
libEGL/libGLES(后面统称为 GPU driver) 这些库都是在 Loader::open()
里面完成的。这个函数分为两部分——首先判断是否需要 unload system GPU driver,如果需要会先执行 unload 操作;接着尝试加载 game/prerelease driver(这两种 driver 在这里走的流程是完全一致的,因此后面统称为 updated driver);如果失败会再尝试多次加载 system GPU driver。虽然是先 unload 再 load,但是如果不先说明 load 的流程,unload 会难以说明清楚,所以下面的内容会先说明 load 的流程,再说明 unload 的流程:
加载 updated driver
加载 updated driver 在 attempt_to_load_updated_driver()
这个函数中完成,这个函数用到了三个核心函数:
getDriverNamespace()
配置 linker namespaceload_updated_driver()
加载 GPU driverinitialize_api()
获取符号地址
getDriverNamespace() 配置 linker namespace
android_namespace_t* GraphicsEnv::getDriverNamespace() {
std::lock_guard<std::mutex> lock(mNamespaceMutex);
if (mDriverNamespace) {
return mDriverNamespace;
}
if (mDriverPath.empty()) {
return nullptr;
}
auto vndkNamespace = android_get_exported_namespace("vndk");
if (!vndkNamespace) {
return nullptr;
}
mDriverNamespace = android_create_namespace("gfx driver",
mDriverPath.c_str(), // ld_library_path
mDriverPath.c_str(), // default_library_path
ANDROID_NAMESPACE_TYPE_ISOLATED,
nullptr, // permitted_when_isolated_path
nullptr);
if (!linkDriverNamespaceLocked(vndkNamespace)) {
mDriverNamespace = nullptr;
}
return mDriverNamespace;
}
getDriverNamespace()
这个函数主要做了以下的事情:
- 使用前面配置流程的最后一步,updated driver 的路径就是被存放在
mDriverPath
这个变量。而现在mDriverPath
被当做参数传给android_create_namespace()
。这里就需要说明一下android_create_namespace()
这个函数,它的作用是创建一个 linker namespace,各个参数:
android_create_namespace(
const char *name, // 这个 link namespace 的名字
const char *ld_library_path,
const char *default_library_path,
unint64_t type,
const char *permitted_when_isolated_path,
android_namespace_t *parent);
参数中有很多个路径,这些表明了这个 linker namespace 中库的搜索路径,按照先 ld_library_path
后 default_library_path
的顺序,并且如果 namespace 的 type
是 ANDROID_NAMESPACE_TYPE_ISOLATED
,那么如果在前面的路径找不到,还会在 permitted_when_isolated_path
里面寻找。
这些路径最终会传到 create_namespace()
:
ns->set_ld_library_paths(std::move(ld_library_paths));
ns->set_default_library_paths(std::move(default_library_paths));
ns->set_permitted_paths(std::move(permitted_paths));
这里只是把路径保存在 app 的 gfx driver
的 linker namespace 里面。至于这些路径怎么用,后面加载部分会详细介绍。
因此这里创建里一个名为 gfx driver
的 linker namespace,并且指定 mDriverPath
为该 linker namespace 的库搜索路径。
2. 调用 linkDriverNamespaceLocked()
:
bool GraphicsEnv::linkDriverNamespaceLocked(android_namespace_t* vndkNamespace) {
const std::string llndkLibraries = getSystemNativeLibraries(NativeLibrary::LLNDK);
if (llndkLibraries.empty()) {
return false;
}
if (!android_link_namespaces(mDriverNamespace, nullptr, llndkLibraries.c_str())) {
ALOGE("Failed to link default namespace[%s]", dlerror());
return false;
}
const std::string vndkspLibraries = getSystemNativeLibraries(NativeLibrary::VNDKSP);
if (vndkspLibraries.empty()) {
return false;
}
if (!android_link_namespaces(mDriverNamespace, vndkNamespace, vndkspLibraries.c_str())) {
ALOGE("Failed to link vndk namespace[%s]", dlerror());
return false;
}
if (mSphalLibraries.empty()) {
return true;
}
// Make additional libraries in sphal to be accessible
auto sphalNamespace = android_get_exported_namespace("sphal");
if (!sphalNamespace) {
ALOGE("Depend on these libraries[%s] in sphal, but failed to get sphal namespace",
mSphalLibraries.c_str());
return false;
}
if (!android_link_namespaces(mDriverNamespace, sphalNamespace, mSphalLibraries.c_str())) {
ALOGE("Failed to link sphal namespace[%s]", dlerror());
return false;
}
return true;
}
这个函数多次调用了 android_link_namespaces()
这个函数:
bool android_link_namespaces(struct android_namespace_t* from,
struct android_namespace_t* to,
const char* shared_libs_sonames);
作用是,from
这个 namespace 能够访问到 shared_libs_sonames
(内容是一连串以 : 分隔开来的 so 的名字,例如:libc.so:libmath.so
)的这些库,但是 shared_libs_sonames
却是从 to
这个 namespace 中加载的。
因此,linkDriverNamespaceLocked()
这个函数做了这几件事:
- 让
gfx driver
能够访问 LLNDK 的库 - 让
gfx driver
能够访问 VNDKSP 的库 - 让
gfx driver
能够从sphal
这个 link namespace 访问到mSphalLibraries
的库。前面提到,sphallibraries.txt
的内容被存放在mSpahlLibraries
这个变量中。至此,我们终于能够理解sphallibraries.txt
这个文件里面的内容的意义,就是能够被gfx driver
访问的,sphal
中的库列表。
通过 linkDriverNamespaceLocked()
,gfx driver
这个 linker namespace 变得非常完备以及真正可用。updated driver 能够正确地在这个新创建的 linker namespace 中被搜索和加载。
通过一张图总结上面的内容:
getDriverNamespace()
创建,配置完 gfx driver
这个 linker namespace 之后,就开始真正的加载流程,主要都是在 load_updated_driver()
这个函数完成的:
load_updated_driver() 加载 GPU driver
static void* load_updated_driver(const char* kind, android_namespace_t* ns) {
ATRACE_CALL();
// 当 flags 为 ANDROID_DLEXT_USE_NAMESPACE 的时候,说明库是从 library_namespace 指定的 link namespace 加载
const android_dlextinfo dlextinfo = {
.flags = ANDROID_DLEXT_USE_NAMESPACE,
.library_namespace = ns,
};
void* so = nullptr;
// static const char* DRIVER_SUFFIX_PROPERTY = ;
//
// static const char* HAL_SUBNAME_KEY_PROPERTIES[2] = {
// "ro.hardware.egl" // 在 8250 平台是 adreno
// "ro.board.platform", // 在 8250 平台是 kona
// };
// 这里拼凑出 SoC 私有的 GPU driver 的名字,例如高通的 GPU driver 名字为:libGLESv2_adreno.so
char prop[PROPERTY_VALUE_MAX + 1];
for (auto key : HAL_SUBNAME_KEY_PROPERTIES) {
if (property_get(key, prop, nullptr) <= 0) {
continue;
}
std::string name = std::string("lib") + kind + "_" + prop + ".so";
so = do_android_dlopen_ext(name.c_str(), RTLD_LOCAL | RTLD_NOW, &dlextinfo);
if (so) {
return so;
}
}
return nullptr;
}
do_android_dlopen_ext
实际上调用的就是 android_dlopen_ext(const char* __filename, int __flags, const android_dlextinfo* __info)
,前两个参数的意义跟 dlopen()
是一致的(参见这里),另外的 __info
这个参数是 Android 自定义的,例如这里的 dlextinfo
就是表明当前这个 so 是从前面创建的 gfx driver
这个 link namespace 中加载。
这里需要注意 kind 有两种情况:在具体的实现中,GPU driver 要么全都集成在一个库:/vendor/lib/egl/libGLES.so
,此时 kind
就是 GLES
;要么是(最常见的)拆分成三个库:/vendor/lib/egl/libEGL.so|libGLESv1_CM.so|libGLESv2.so
,此时 kind
总共有三类:EGL
,GLESv1_CM
,GLESv2
,其中 GLESv1_CM
代表的是 OpenGL ES 1.1 的驱动,GLESv2
代表的是 OpenGL ES 2.0/3.0/3.1 的驱动。
加载函数 android_dlopen_ext()
经过下面的调用:
android_dlopen_ex()
\_ _loader_android_dlopen_ext()
\_ dlopen_ext()
\_ do_dlopen()
\_ find_library()
\_ find_libraries()
\_ find_library_internal()
\_ load_library()
\_ open_library()
open_library
() 把存起来的路径传给 open_library_on_paths
:
int fd = open_library_on_paths(zip_archive_cache, name, file_offset, ns->get_ld_library_paths(), realpath);
这个函数把前面创建 link namespace 的时候保存的路径当做参数,然后会去检查 path 里面是否有 !/ 分隔符,如果有,那么就从 apk 这个 zip 包里面把 so 解压出来:
static int open_library_at_path(ZipArchiveCache* zip_archive_cache,
const char* path, off64_t* file_offset,
std::string* realpath) {
int fd = -1;
// const char* const kZipFileSeparator = "!/";
if (strstr(path, kZipFileSeparator) != nullptr) {
fd = open_library_in_zipfile(zip_archive_cache, path, file_offset, realpath);
}
因此现在可以解答前面的疑问:path
里面带 !/ 的意义是:从 apk 的包里面解压拿到包里面 !/ 后面的路径代表的内容。,例如前面的 /system/app/xxx/xxx.apk!/lib/armeabi-v7a/
意义就是从 /system/app/xxx/xxx.apk
这个 zip 包里面的 lib/armeabi-v7a/
。
同样用一张图来总结上面的内容:
initialize_api 获取符号地址
这个函数依据不同的类型(EGL
,GLESv1_CM
,GLESv2
),获取符号地址的方式也不同:
- EGL
这里首先获取到if (mask & EGL) { getProcAddress = (getProcAddressType)dlsym(dso, "eglGetProcAddress"); ALOGE_IF(!getProcAddress, "can't find eglGetProcAddress() in EGL driver library"); egl_t* egl = &cnx->egl; __eglMustCastToProperFunctionPointerType* curr = (__eglMustCastToProperFunctionPointerType*)egl; char const * const * api = egl_names; while (*api) { char const * name = *api; __eglMustCastToProperFunctionPointerType f = (__eglMustCastToProperFunctionPointerType)dlsym(dso, name); if (f == nullptr) { // couldn't find the entry-point, use eglGetProcAddress() f = getProcAddress(name); if (f == nullptr) { f = (__eglMustCastToProperFunctionPointerType)nullptr; } } *curr++ = f; api++; } }
eglGetProcAddress()
的符号地址并且保存在getProcAddress
这个变量,后面会用到。eglGetProcAddress
这个函数的作用是返回一个 EGL/GL(ES) 的符号地址,作用跟后面的dlsym()
类似。
需要注意的是egl_names
:
而char const * const egl_names[] = { nullptr };
elg_entries.in
的内容是一系列的 EGL 标准接口的名称字符串:
#define EGL_ENTRY(_r, _api, ...) #_api,
EGL_ENTRY(EGLDisplay, eglGetDisplay, NativeDisplayType)
因此 egl_names
的内容就是一个 EGL 接口函数的字符串数组,遍历该数组,并且从 load_updated_driver()
加载的库调用 dlsym()
获取接口函数的符号地址(前面提到 load_updated_driver()
调用的 android_dlopen_ext()
跟 dlopen()
作用类似),获取到的函数符号地址最终都保存在 cnx->egl
:
struct egl_connection_t {
...
egl_t egl;
}
struct egl_t {
};
神奇的事情来了,这里 struct egl_t
的定义和上面的 egl_names[]
用到了同样的 egl_entries.in
,但是在 struct egl_t
中 EGL_ENTRY
的定义不一样了,这里变成了函数指针:
#define EGL_ENTRY(_r, _api, ...) _r (*(_api))(__VA_ARGS__);
所以 cnx->egl
保存了所有 EGL 函数的符号地址。
- GLESv1_CM/GLESv2
GLESv1_CM 和 GLESv2 加载主要都在init_api()
完成,函数的作用是将 GLESv1_CM 的所有函数符号地址保存到cnx->hooks[0]
,GLESv2 保存在cnx->hooks[1]
:
解释一下各个参数的意义:void init_api(void* dso, char const * const * api, char const * const * ref_api, __eglMustCastToProperFunctionPointerType* curr, getProcAddressType getProcAddress);
api
这个参数,GLESv1_CM 传的是gl_names_1
,GLESv2 传的是gl_names
这两个跟前面的egl_names
一样,同样是#include
一个 in 文件(gl_names_1
是entries_gles1.in
,gl_names
是entries.in
),这些 in 文件跟egl_entries.in
类似:
跟前面GL_ENTRY(void, glActiveShaderProgram, GLuint pipeline, GLuint program)
EGL_ENTRY
一样,也是有两个定义,这里是将接口转成接口的函数声明字符串:#define GL_ENTRY(_r, _api, ...) #_api,
ref_api
这个参数的作用是加速 GLESv1_CM 符号表地址的加载速度,在获取 GLESv1_CM 所有函数的符号表时,ref_api
赋值为gl_names
(也就是 GLESv2 的函数列表),然后在获取符号表的时候,如果当前的函数属于 GLESv2,那么就将地址赋值为 NULL 并且跳过。原理是 GLESv1_CM 是 GLESv2 的子集,而在cnx->hook[2]
的声明中:
数组两个都是struct egl_connection_t { ...... gl_hooks_t * hooks[2]; }; struct gl_hooks_t { struct gl_t { } gl; struct gl_ext_t { __eglMustCastToProperFunctionPointerType extensions[MAX_NUMBER_OF_GL_EXTENSIONS]; } ext; };
gl_hooks_t
,即 GLESv2 的长度。因此对于 GLESv1_CM 的库,如果函数名属于 GLESv2,那么可以直接跳过符号表获取的逻辑转而赋值为 NULL,从而加快 GLESv1_CM 的处理速度。curr
分别对应了上面提到的cnx->hooks[]
,GLESv1_CM 对应cnx->hooks[0]
,GLESv2 对应cnx->hooks[1]
,代表这符号表地址保存的位置。getProcAddress
前面获取 EGL 库的符号地址的时候提到的,eglGetProcAddress
的符号地址。
接着,获取 GLESv1_CM/GLESv2 的符号地址按照下面的顺序进行:
- 直接
dlsym()
尝试获取 - 如果失败,使用
eglGetProcAddress()
尝试获取 - 如果失败,尝试使用
dlsym()
加载非OES
的版本。例如glAlphaFuncxOES()
获取符号地址失败,那么尝试获取glAlphaFuncx()
这个非OES
的符号地址(注:假设 OpenGL 针对移动平台作了专门的修改或者优化,就会加上 OES) - 如果失败,尝试使用
dlsym()
加载OES
的版本 - 如果都失败,那么将当前函数的符号表赋值为
gl_unimplemented
,意为没有实现。
总结
总结一下,updated driver 的加载主要在 attempt_to_load_system_driver()
中完成,主要分为三步:
- 创建/配置 link namespace,主要作用是将 app 加载 GPU driver 的路径转到 updated driver apk 所在的位置
- 从 updated driver apk 中依次将
libEGL
,libGLESv1_CM
和libGLESv2
打开获取 handle - 从第二步获取的 handle 中获取各个 EGL, GLES 的函数的符号地址
其实看上去跟常规的加载库的步骤是一直的,只不过多了第一步的创建 link namespace 限定 so 的加载路径。
至此,整个 updated driver 的加载流程就完全讲完了。再用一张图总结前面所有的内容:
unload system driver
如driver 加载时机那一节提到的那样,zygote[64] 在启动的时候会去将 system driver 预加载,而所有 Android 进程都是由 zygote[64] 孵化的,因此每一个进程默认 system driver 都是已经加载完毕的,因此对于那些需要 updated driver 的 app,就需要将 system driver unload。而是否需要 unload system driver 是通过判断 mDriverPath
是否为空(updated driver 的配置过程提到,这个变量的意义是 updated driver 的具体路径)。而 unload 的操作也非常简单,跟前面的加载恰恰相反,就是将存放 EGL 和 GLES 库的 handle 和函数符号地址的 hooks[]
和 dso[]
这两个变量的内容都置为 nullptr,这里就不赘述了。
Author: simowce
Permalink: https://blog.simowce.com/all-about-updated-driver/
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。