我们知道OpenGLES里面有个函数叫GLES20.glReadPixels,可以帮助我们从FrameBuffer里面把纹理像素拷贝到数组里面,但是这个方法有几个弊端:
①耗时,花费的时间和截图的大小成正比关系,在一些差的设备上耗时非常严重,如果是播放视频,有明显卡顿;
②耗内存,一般截图就是为了把像素数据输出到文件,通常都用Bitmap,这里有个问题是,用Bitmap的话,需要消耗两份内存,创建Bitmap的时候一份,截图时还需要申请一份。
接下来我就针对这两个弊端给出优化的方案。
耗时有两种优化方案:
①使用PBO(Pixel Buffer Object)
PBO(Pixel Buffer Object)是OpenGL ES 3.0开始提供的一种方式,主要应用于从内存快速复制纹理到显存,或从显存复制像素数据到内存。
它的原理就是先创建一个PBO,在需要截图的时候先绑定这个PBO,再调用glReadPixel的时候,glReadPixel发现有PBO,这时会交给GPU去完成,GPU异步去完成,不会消耗CPU,过一段时间再从PBO把像素映射出来。
// 读取像素时先绑定到PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pbos[0])
// 将数据从fbo读到pbo,由于这个是异步的,我们要等待一段时间再回来拿像素。因为OpenGLES的API glReadPixels需要传个buffer,但又不能传null,所以需要调用C的这个接口
GLHelper.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE)
过个一两帧之后,再从PBO把数据读出来:
// 从PBO里面读取像素
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pbos[0])
// glMapBufferRange,映射内存,会等待DMA传输完成,把pbo数据读到buffer里面
val bf = GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER,
0, width * height * 4, GLES30.GL_MAP_READ_BIT)
//解除映射
GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER)
//解除绑定PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, GLES30.GL_NONE)
PBO其实最适合使用的场景在录屏这一块,使用双缓冲技术,每次从上一帧readPixel后的PBO读取像素,交替使用PBO。
②使用共享纹理
在一些低端设备上还没法用PBO,所以有另外个方案就是共享纹理。
我们可以自己创建个线程,线程里面有个Surface,把画面渲染到这个Surface上,再从这个Surface把像素读取出来就行了,这里就要用到同一套渲染管线。
如果要把同一套渲染管线,在其他线程也能渲染,有两个条件:
①OpenGLES所有的API,都必须在有EGLContext的环境下调用,否则无效,所以,要在其他线程使用OpenGLES的API去截图,要先在这个线程初始化EGLContext。
②不同EGLContext下,纹理,顶点数据等是不能共享的,所以初始化其他EGLContext时,需要把已有的渲染管线创建时所在的EGLContext拿过来使用。
一般是从GLSurfaceView拿EGLContext,通过反射拿就行了。
// 使用EGL10,是为了和GLSurfaceView使用一样的EGL版本
val mEgl = EGLContext.getEGL() as EGL10
// share_context就是从GLSurfaceView拿出来的
mEgl.eglCreateContext(mEglDisplay, eglConfig, share_context, contextAttr)
然后在截图的时候,先在另外一个线程渲染,再跳回来继续当前的渲染流程,完后在另外一个线程去读像素数据就行了。
内存优化同样有两种解决方案:
①使用libjpeg库
既然要输出到文件,我们可以不用Bitmap,Bitmap只是个手段,但这个手段太鸡肋了。
我们可以在JNI里面申请一份内存,调用glReadPixels,然后再通过jpeg库把数据输出到本地文件:
unsigned char* data = new unsigned char[width * height * 3];
glReadPixels(x, y, width, height, GL_RGB, GL_UNSIGNED_BYTE, data);
const char *filepath = env->GetStringUTFChars(fileName, NULL);
generateJPEG(data, width, height, quality, filepath, false);
delete[] data;
这里有很多好处:
1.只需要用到一份内存保留pixel;
2.用原生Opengl接口能支持更多颜色通道;
3.用完这段内存后能手动直接释放;
4.申请的这份内存在Native Heap,不影响到Java Heap
5.Native去压缩图片,速度更快;
②还是使用Bitmap的方案
可以先创建个Bitmap,然后在Native层lock_Pixels把Bitmap的数据地址拿出来,然后把这段数据塞给glReadPixels,这样就不需要再申请一份内存了:
unsigned char* datas = NULL;
AndroidBitmap_lockPixels(env, bitmap, (void**) &datas);
glReadPixels(x, y, width, height, GL_RGB, GL_UNSIGNED_BYTE, datas);
AndroidBitmap_unlockPixels(env, bitmap);