Android显示系统(01)- 架构分析
Android显示系统(02)- OpenGL ES - 概述
Android显示系统(03)- OpenGL ES - GLSurfaceView的使用
Android显示系统(04)- OpenGL ES - Shader绘制三角形
Android显示系统(05)- OpenGL ES - Shader绘制三角形(使用glsl文件)
Android显示系统(06)- OpenGL ES - VBO和EBO和VAO
Android显示系统(07)- OpenGL ES - 纹理Texture
Android显示系统(08)- OpenGL ES - 图片拉伸
一、前言:
之前文章讲解了Android显示系统的架构,并且花了大量时间介绍了OpenGL ES,以及为了保证OpenGL ES的平台无关性,Android引入的EGL。不过我们都是借助于GLSurfaceView
来学习的,而我们构建GLSurfaceView
对象的时候,在Native层做了很多的事情,其中,大多数逻辑都是SurfaceFlinger
辅助我们完成的,它才是整个Android显示系统的核心。
SurfaceFlinger
最重要的职责就是合成各个App传递过来的Surface。本文先学会在Native层使用SurfaceFlinger
,后续根据代码一点点引入不同知识。
**说明:**后续文章如果出现SF
那就是我对SurfaceFlinger
的缩写。
二、BootAnimation:
读者可能很郁闷,讲SF的使用,怎么突然蹦出来这么一个东西。其实,BootAnimation
是 Android 系统启动时显示的动画,通常包括启动画面、动画效果和加载过程中的图形展示。要学习Native层显示系统,它是最好的程序之一,因为,此时AMS、WMS那些复杂的服务还没有启动,干扰项少!
1、启动:
- 在
Android.bp
当中启动bootanim.rc
; - 解析文件内容启动
BootAnimation
进程; - 启动之后,就会调用
BootAnimation
的main函数;
这是base\cmds\bootanimation\bootanim.rc
内容,我都贴心的加了注释。
shell
# 这行定义了一个名为 bootanim 的服务,服务的可执行文件是 /system/bin/bootanimation。
service bootanim /system/bin/bootanimation
# 这行指定了服务的类别。将其归类为 core 和 animation 意味着该服务属于系统的核心服务及动画服务类别,可能影响其调度和优先级
class core animation
# 这行指定了运行该服务的用户身份是 graphics
user graphics
# 这行指定该服务属于 graphics 和 audio 组。这样做是为了允许该服务访问所需的资源和设备,因此图形和音频相关的权限会得到支持。
group graphics audio
# 这表示该服务在系统启动时不会自动启动。
disabled
# 这个关键字指明该服务是一种一次性服务。它在启动后会运行一次并退出,而不是保持在后台持续运行
oneshot
# 这行设置了 I/O 调度优先级,rt 表示实时优先级,0 是优先级值。设置为 0 表示它具有较低的实时优先级
ioprio rt 0
# 这一行表示将该服务的进程 ID(PID)写入到特定路径 /dev/stune/top-app/tasks 中
writepid /dev/stune/top-app/tasks
注意,其中这一行disabled
表示这个服务不会自己启动,为什么呢?因为它要渲染开机画面,离不开大佬SurfaceFlinger
,SurfaceFlinger
启动之后,会将这个改成enable
,然后,bootanimation
服务就启动了,启动后当然就要执行main函数了。
2、流程概述:
我们假设自己没有阅读过BootAnimation
,但是,我们对SurfaceFlinger
比较熟悉,我们觉得应该怎么做?
- 获得
SurfaceFlinger
服务; - 通过
SurfaceFlinger
创建Surface
; - 开始循环:
- 得到surface里面的buffer;
- 设置给渲染引擎(比如之前的OpenGL ES),往buffer填充数据;
- 提交填充好的buffer;
后续我们阅读完开机动画代码之后再总结,只是会详细很多,透过现象看本质,大体思路不会变。
3、main函数:
文件路径:base\cmds\bootanimation\bootanimation_main.cpp
代码如下:
cpp
int main()
{
setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY);
bool noBootAnimation = bootAnimationDisabled();
ALOGI_IF(noBootAnimation, "boot animation disabled");
if (!noBootAnimation) {
sp<ProcessState> proc(ProcessState::self());
ProcessState::self()->startThreadPool();
// create the boot animation object (may take up to 200ms for 2MB zip)
sp<BootAnimation> boot = new BootAnimation(audioplay::createAnimationCallbacks());
waitForSurfaceFlinger();
boot->run("BootAnimation", PRIORITY_DISPLAY);
ALOGV("Boot animation set up. Joining pool.");
IPCThreadState::self()->joinThreadPool();
}
return 0;
}
总体来看:
负责设置进程的优先级,检查是否禁用 BootAnimation
,启动并设置 BootAnimation
相关的状态和资源,同时确保在 SurfaceFlinger
可用后开始播放动画。通过这样的结构,系统能够顺利地处理BootAnimation
的显示,确保用户在设备开机时有良好的视觉体验。
详细看看:
-
设置进程优先级:
cppsetpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY);
PRIO_PROCESS
指定要修改的是进程的优先级。0
表示当前进程(即当前执行的 main 函数)。ANDROID_PRIORITY_DISPLAY
是一个常量,通常是在 Android 中用于设置与显示相关的进程优先级,确保 Boot Animation 在显示时有足够的资源进行渲染。
-
检查是否禁用
BootAnimation
:cppbool noBootAnimation = bootAnimationDisabled();
如果没有禁用,才会执行主要逻辑;
-
打开
Binder
,并启动线程池:因为SF就是一个Binder服务,要想访问SF,必须先打开Binder;
cppsp<ProcessState> proc(ProcessState::self()); ProcessState::self()->startThreadPool();
其实
ProcessState::self()
主要作用就是打开binder,并将进程和线程的相关信息设置进binder的binder_proc,binder_thread结构当中。接着通过startThreadPool()
启动线程池,主要处理 IPC(进程间通信)请求。这样可以在后台异步处理工作而不阻塞主线程。 -
创建 BootAnimation 对象,并设置音频回调函数:
cppsp<BootAnimation> boot = new BootAnimation(audioplay::createAnimationCallbacks());
这回调函数应该是播放那个开机完了
噔噔噔噔
的,我没看!! -
等待
SurfaceFlinger
可用:cppwaitForSurfaceFlinger();
-
运行
BootAnimation
:cppboot->run("BootAnimation", PRIORITY_DISPLAY);
传入"BootAnimation"的名称和显示优先级 PRIORITY_DISPLAY。
-
加入线程池:
cppIPCThreadState::self()->joinThreadPool();
当前的线程(也就是main方法所在的线程)加入Binder的线程池,并阻塞住,保证当前进程不退出。
4、启动BootAnimation:
4.1、重要方法:
- sp指针是Android当中的一个强智能指针(strong pointer,不是smart pointer),对应的还有wp就是weak pointer。
BootAnimation
继承自Thread
和DeathRecipient
(DeathRecipient
继承了RefBase
),也就是说BootAnimation
是RefBase
子类(C++没有孙子类的说法,我很认真地告诉你!!!);- 其重写了几个关键方法,其在创建其对象并调用run方法后会分别回调到:
- onFirstRef:继承自RefBase,当对象第一次被sp引用的时候,这个函数就会被调用。
- readyToRun:继承自Thread,Thread执行前的初始化工作;
- threadLoop:继承自Thread,每个线程类都要实现的,在这里定义thread的执行内容。会不断循环执行,直到调用了requestExit()退出;
- binderDied:继承自DeathRecipient,一般当binder异常结束时会调用;
也就是说:BootAnimation的onFirstRef -> readyToRun -> threadLoop方法会相继执行。千万记住这个顺序,Android源代码导出是这个。
4.2、BootAnimation::BootAnimation():
cpp
BootAnimation::BootAnimation(sp<Callbacks> callbacks)
: Thread(false), mClockEnabled(true), mTimeIsAccurate(false),
mTimeFormat12Hour(false), mTimeCheckThread(nullptr), mCallbacks(callbacks) {
// SurfaceFlinger在客户端的代理
mSession = new SurfaceComposerClient();
// 这个属性用于判断是开机还是关机
std::string powerCtl = android::base::GetProperty("sys.powerctl", "");
if (powerCtl.empty()) {
mShuttingDown = false;
} else {
mShuttingDown = true;
}
ALOGD("%sAnimationStartTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
elapsedRealtime());
}
构造函数中主要是先获取SF在客户端的代理,后面便于和SF进行通信。
4.3、BootAnimation::onFirstRef():
cpp
void BootAnimation::onFirstRef() {
status_t err = mSession->linkToComposerDeath(this);
if (err == NO_ERROR) {
// ...
preloadAnimation();
// ...
}
}
主要是通过preloadAnimation()
做了预加载开机动画的资源文件,另外监听了SF的死亡通知。
4.4、BootAnimation::readyToRun():
cpp
status_t BootAnimation::readyToRun() {
// 加载启动动画所需的资源(图像、动画等)
mAssets.addDefaultAssets();
// 获得主屏幕句柄
mDisplayToken = SurfaceComposerClient::getInternalDisplayToken();
if (mDisplayToken == nullptr)
return -1;
// 这检索显示器的相关信息,
// 例如宽度 (dinfo.w)、高度 (dinfo.h) 和其他相关参数。这些信息对于正确设置Surface至关重要。
DisplayInfo dinfo;
status_t status = SurfaceComposerClient::getDisplayInfo(mDisplayToken, &dinfo);
if (status)
return -1;
// create the native surface
// 创建SurfaceControl, 参数指定名称 ("BootAnimation")、宽度、高度和像素格式 (PIXEL_FORMAT_RGB_565)
sp<SurfaceControl> control = session()->createSurface(String8("BootAnimation"),
dinfo.w, dinfo.h, PIXEL_FORMAT_RGB_565);
// 此事务设置Surface的层级,确保其在屏幕上的正确位置。apply() 函数将事务提交到显示系统。
// 设置Layer(值越大表示在屏幕坐标系Z-order越大,也就是越靠近我们用户)
SurfaceComposerClient::Transaction t;
t.setLayer(control, 0x40000000)
.apply();
// 获得对应的Surface(画布)
sp<Surface> s = control->getSurface();
// initialize opengl and egl // 初始化OpenGL和EGL上下文
const EGLint attribs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_DEPTH_SIZE, 0,
EGL_NONE
};
EGLint w, h;
EGLint numConfigs;
EGLConfig config;
EGLSurface surface;
EGLContext context;
// 获得一块屏幕
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, nullptr, nullptr);
// 选择我们自己需要的配置
eglChooseConfig(display, attribs, &config, 1, &numConfigs);
// 创建一个EGLSurface,里面包含有普通的Surface,后面OpenGL就会绘制到渲染到这个EGLSurface的Surface当中
surface = eglCreateWindowSurface(display, config, s.get(), nullptr);
// 创建一个 EGL 上下文
context = eglCreateContext(display, config, nullptr, nullptr);
// 获取surface的宽度
eglQuerySurface(display, surface, EGL_WIDTH, &w);
// 获取surface的高度
eglQuerySurface(display, surface, EGL_HEIGHT, &h);
// 使创建的上下文成为当前上下文,这将 EGL 上下文、EGLSurface绑定,使后续的 OpenGL 绘制命令作用于此Surface。
if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE)
return NO_INIT;
mDisplay = display;
mContext = context;
mSurface = surface;
mWidth = w;
mHeight = h;
mFlingerSurfaceControl = control;
mFlingerSurface = s;
mTargetInset = -1;
return NO_ERROR;
}
总而言之,readyToRun()
函数设置了启动动画的完整渲染管道:获取显示信息、创建Surface以及初始化 OpenGL ES 和 EGL。
4.5、BootAnimation::threadLoop():
cpp
bool BootAnimation::threadLoop()
{
bool r;
// We have no bootanimation file, so we use the stock android logo
// animation.
if (mZipFileName.isEmpty()) {
// 系统默认开机动画
r = android();
} else {
// 用户自定义的开机动画
r = movie();
}
// ... 省略一些资源回收的代码
return r;
}
onFirstRef
方法预加载了开机动画文件, 并且将文件地址复制给mZipFileName
,因为在编译源码的时候并没有设置开机动画bootanimation.zip所以mZipFileName肯定是空的,因此,我们会走android()
函数。
4.6、BootAnimation::android():
主要负责循环在 Android 开机动画中绘制一个 "Android" 图标,并对其进行动态的光效处理(类似 "扫光" 效果)。
主要流程如下:
- 加载两个纹理:一个 Android 图标遮罩,一个光条(扫光效果);
- 配置 OpenGL 环境,以及裁剪和混合模式;
- 在动画循环中,不断更新扫光图像的位置并绘制,同时结合遮罩进行透明处理;
- 刷新显示帧并限制帧速率;
- 根据退出条件判断是否停止动画;
- 最后清理纹理资源。
其视觉效果是一个动态的 Android 图标,有一个扫光效果从左到右滚动,直到系统完成启动时退出动画。。
cpp
bool BootAnimation::android()
{
// 加载纹理资源
initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png");
initTexture(&mAndroid[1], mAssets, "images/android-logo-shine.png");
mCallbacks->init({});
// 清屏并设置 OpenGL 状态
glShadeModel(GL_FLAT);
glDisable(GL_DITHER);
glDisable(GL_SCISSOR_TEST);
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
eglSwapBuffers(mDisplay, mSurface);
glEnable(GL_TEXTURE_2D);
glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
// 计算 Android 图标的位置及绘图区域
const GLint xc = (mWidth - mAndroid[0].w) / 2;
const GLint yc = (mHeight - mAndroid[0].h) / 2;
// 开启裁剪并设置混合模式
const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
updateRect.height());
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
// 动画循环处理
const nsecs_t startTime = systemTime();
do {
nsecs_t now = systemTime();
double time = now - startTime;
float t = 4.0f * float(time / us2ns(16667)) / mAndroid[1].w;
GLint offset = (1 - (t - floorf(t))) * mAndroid[1].w;
GLint x = xc - offset;
glDisable(GL_SCISSOR_TEST);
glClear(GL_COLOR_BUFFER_BIT);
// 绘制与融合
glEnable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, mAndroid[1].name);
glDrawTexiOES(x, yc, 0, mAndroid[1].w, mAndroid[1].h);
glDrawTexiOES(x + mAndroid[1].w, yc, 0, mAndroid[1].w, mAndroid[1].h);
glEnable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, mAndroid[0].name);
glDrawTexiOES(xc, yc, 0, mAndroid[0].w, mAndroid[0].h);
EGLBoolean res = eglSwapBuffers(mDisplay, mSurface);
if (res == EGL_FALSE)
break;
// 限制帧率为12帧
const nsecs_t sleepTime = 83333 - ns2us(systemTime() - now);
if (sleepTime > 0)
usleep(sleepTime);
// 检查是否退出开机动画
checkExit();
} while (!exitPending());
// 清理资源
glDeleteTextures(1, &mAndroid[0].name);
glDeleteTextures(1, &mAndroid[1].name);
return false;
}
总之,就是不断的使用OpenGL ES给Surface的Buffer填充数据,并提交显示。
流程比较复杂,详细分下:
1. 初始化日志输出
cpp
SLOGD("%sAnimationShownTiming start time: %" PRId64 "ms",
mShuttingDown ? "Shutdown" : "Boot",
elapsedRealtime());
- 使用
SLOGD
输出调试日志,记录动画启动的时间以及是 "开机动画" 还是 "关机动画"(根据mShuttingDown
判断)。elapsedRealtime()
返回系统的相对启动时间。
2. 加载纹理资源
cpp
initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png");
initTexture(&mAndroid[1], mAssets, "images/android-logo-shine.png");
mCallbacks->init({});
mAndroid[0]
: 加载 Android 图片遮罩android-logo-mask.png
,此图包含透明处理的 Android 图标。mAndroid[1]
: 加载扫光效果用的光条图片android-logo-shine.png
。mCallbacks->init({});
: 初始化回调函数(可能用于处理动画流程的状态或退出信号)。
3. 清屏并设置 OpenGL 状态
cpp
glShadeModel(GL_FLAT);
glDisable(GL_DITHER);
glDisable(GL_SCISSOR_TEST);
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
eglSwapBuffers(mDisplay, mSurface);
glEnable(GL_TEXTURE_2D);
glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
- 清空屏幕: 将整个屏幕填充为黑色,并调用
eglSwapBuffers()
立即清空显示缓冲并应用更新。 - 关掉不必要的功能: 禁用 OpenGL 的光栅化效果(
GL_DITHER
),裁剪测试功能(GL_SCISSOR_TEST
),设置着色模型为平面着色(GL_FLAT
)。 - 启用纹理: 使用 2D 纹理功能,并将纹理绘制模式设置为直接替换(
GL_REPLACE
),即使用纹理本身的颜色覆盖。
4. 计算 Android 图标的位置及绘图区域
cpp
const GLint xc = (mWidth - mAndroid[0].w) / 2;
const GLint yc = (mHeight - mAndroid[0].h) / 2;
const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
- 计算中心位置: Android 图标作为覆盖物,要将其位置设置在屏幕的正中央 (
xc
,yc
)。 - 创建裁剪区域: 使用
Rect
定义需要更新的区域,以避免屏幕外的部分被绘制,提高效率。
5. 开启裁剪并设置混合模式
cpp
glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(), updateRect.height());
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
- 开启裁剪测试: 只绘制
updateRect
区域内的内容,其余区域不会被更新。裁剪的 Y 坐标需要上下翻转,因为 OpenGL 的 Y 坐标和 Android 屏幕的 Y 坐标是相反的。 - 设置混合模式: 使用透明度混合作为处理方式(
GL_SRC_ALPHA
和GL_ONE_MINUS_SRC_ALPHA
),用以支持多个透明图片的叠加效果。
6. 动画循环处理
动画入口为以下循环:
cpp
do {
nsecs_t now = systemTime();
double time = now - startTime;
float t = 4.0f * float(time / us2ns(16667)) / mAndroid[1].w;
GLint offset = (1 - (t - floorf(t))) * mAndroid[1].w;
GLint x = xc - offset;
- 动画时间计算:
startTime
: 开始时间(以纳秒为单位)。time
: 计算从动画开始经过的时间。t
: 利用时间计算扫光的位置比例,并与图片宽度匹配,以此控制扫光图片在 X 方向上的动态偏移量。
- 偏移计算: 计算当前帧的 X 偏移量
x
,使扫光(mAndroid[1]
)能够动态地从左到右移动。
7. 绘制与融合
cpp
// 清屏并禁用裁剪以绘制扫光动画
glDisable(GL_SCISSOR_TEST);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_SCISSOR_TEST);
// 绘制扫光图像
glDisable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, mAndroid[1].name);
glDrawTexiOES(x, yc, 0, mAndroid[1].w, mAndroid[1].h);
glDrawTexiOES(x + mAndroid[1].w, yc, 0, mAndroid[1].w, mAndroid[1].h);
// 启用融合并绘制遮罩与图标
glEnable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, mAndroid[0].name);
glDrawTexiOES(xc, yc, 0, mAndroid[0].w, mAndroid[0].h);
// 交换缓冲区显示动画
EGLBoolean res = eglSwapBuffers(mDisplay, mSurface);
if (res == EGL_FALSE)
break;
- 清屏: 清除缓冲区的内容,并重新启用裁剪测试准备绘制动画。
- 扫光纹理绘制:
- 绘制扫光的效果图片(
mAndroid[1]
),左侧的x
加偏移量,右偏移则从左侧图片接续平滑过渡。 - 动态更新扫光图片的位置(
GL_SCISSOR_TEST
限制绘制区域)。
- 绘制扫光的效果图片(
- 绘制遮罩(图标本体): 将遮罩图片(
mAndroid[0]
)绘制在扫光效果层之上,通过混合模式实现透明区域的显示。 - 刷新显示: 调用
eglSwapBuffers()
刷新屏幕,使这一帧内容可见。
8. 限制帧速率
cpp
const nsecs_t sleepTime = 83333 - ns2us(systemTime() - now);
if (sleepTime > 0)
usleep(sleepTime);
- 限制帧速率为 12 FPS,通过计算提前绘制的时间差进行休眠,平衡 CPU 性能消耗。
9. 退出检测
cpp
checkExit();
} while (!exitPending());
- 在每帧渲染完成后调用
checkExit()
检查是否需要退出动画(例如开机完成)。如果exitPending()
返回true
,退出循环。
10. 清理资源
cpp
glDeleteTextures(1, &mAndroid[0].name);
glDeleteTextures(1, &mAndroid[1].name);
- 删除纹理: 释放掉加载的两个图片纹理。
三、总结:
本文主要以开机画面显示流程介绍了一下一个Naitve的应用程序如何去使用SurfaceFlinger
, 但是,遗憾的是,最后的提交buffer部分,收到OpenGL干扰,貌似有点不清楚,后面文章继续深入分析!