在Android上做图片或视频帧处理一般有两种方式,一种是在CPU上运行,比如用Matrix类对bitmap做平移、缩放、旋转等变换操作,用OpenCV对图片做格式转换等;另一种是在GPU上运行,一般是通过GLSurfaceView在屏幕上申请一块Surface,调用OpenGL ES API对图片纹理做处理。CPU的图片处理耗时一般是GPU的数十倍,在实时性要求比较高的场景下还是需要用GLSurfaceView来做处理。
OpenGL ES API的调用和执行需要OpenGL上下文环境,而创建OpenGL上下文环境的过程比较复杂,GLSurfaceView的优势就在于它帮助我们封装了创建OpenGL上下文环境所需要的步骤,使我们能够直接在暴露出来的onSurfaceCreated
、onSurfaceChanged
和onDrawFrame
回调中调用OpenGL ES API。GLSurfaceView继承自SurfaceView,它是在屏幕上创建了一块可显示的Surface,我们可以把处理后的图像纹理直接显示在这个Surface上。这样的设计可以应对大部分应用场景,但是对少部分场景又存在缺陷。比如我们只是想在后台用GPU处理一些图片,并不需要把它们显示在屏幕上,或者在退后台以后图片处理任务继续执行。当然,用GLSurfaceView也可以解决不显示在屏幕的需求,比如将GLSurfaceView设置成一个像素点,但是如果要实现退后台以后图片处理任务继续执行就很困难了,Activity退出时会触发GLSurfaceView销毁,GLSurfaceView会等待任务执行完成后再销毁相关OpenGL上下文环境,如果任务执行时间过长,会报ANR,所以这种场景下用GLSurfaceView就显得力不从心了。如果我们能脱离GLSurfaceView单独创建一个包含OpenGL上下文环境的线程,并且只做离屏渲染而不将结果渲染到屏幕上,那前面的场景就会好处理很多。本文就从这种实际场景出发介绍一下如何在Android设备上不用GLSurfaceView实现GPU离屏渲染。
GLSurfaceView本质上就是一个包含OpenGL环境的普通线程,在连续渲染模式下或者调用requestRender
方法后不断调用OpenGL API将图像数据渲染到它创建的Surface上,其内部实现本来就包含了创建OpenGL上下文环境的所有步骤以及普通线程与OpenGL上下文环境绑定的过程。所以我们要想实现一个离屏渲染的GL线程,本质上就是把这部分实现抽取出来。当然GLSurfaceView创建的可见Surface和离屏渲染创建的不可见Surface是两种不同的类型,这个我们后面再详细介绍。首先我们来看一下一个离屏渲染的GL线程是长什么样子的。
GL线程模板
Android应用中创建线程可以用Java API提供的Thread
类和Android系统提供的HandlerThread
类,HandlerThread
提供了更好的生命周期封装和跨线程调用实现,所以我们以HandlerThread
为例来创建一个GL线程。GL线程的基本框架如下所示:
public class SimpleGLThread {
private Handler mHandler;
private EglCore mCore;
private OffscreenSurface mOffscreenSurface;
public SimpleGLThread(String name) {
HandlerThread ht = new HandlerThread(name); //创建线程,未来将包含OpenGL Context成为GL线程
ht.start();
mHandler = new Handler(ht.getLooper()); //线程对应的Handler
mHandler.post(new Runnable() {
@Override
public void run() {
mCore = new EglCore(); //EGL环境创建和配置
mOffscreenSurface = new OffscreenSurface(mCore, 100, 100); //创建离屏渲染Surface
mOffscreenSurface.makeCurrent(); //绑定EGLContext、EGLSurface和当前线程
}
});
}
//在该GL线程中运行Runnable,其中可以包含OpenGL ES指令
public void postJob(Runnable runnable) {
if (mHandler != null) {
mHandler.post(runnable);
}
}
//销毁EGLSurface、EGLContext和相关线程
public void destroy() {
if (mHandler != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mOffscreenSurface.release(