摘要: 关于Android开发渲染图片,详细介绍SurfaceView传统方案和GLSurfaceView硬件加载两种方案的具体实现,适配灰度图、BGR、YUV420、YUYV等多种格式图片
关键词:Android 图像渲染 SurfaceView GLSurfaceView
一、背景:
项目开发中需连续绘制多种格式图片(如BGR、YUV420、灰度图等),初始采用SurfaceView实现渲染,暴露以下关键问题:
- 帧率不稳定: SurfaceView虽支持子线程绘制,但在高频更新场景(如>30fps)下,帧率波动显著,单帧绘制耗时50-80ms,难以满足流畅性要求。
- CPU占用过高: 处理非RGB格式时(如YUV420→RGB转换),需依赖CPU格式转换,单帧图像消耗15ms以上,同时Bitmap内存复制进一步加剧负载,总CPU占用率达50%-70%。
GLSurfaceView通过OpenGL ES API将计算负载卸载至GPU,大大减少CPU占用,提高刷新率,现总结SurfaceView和GLSurfaceView两种渲染方案实现方案
二、SurfaceView
结合SurfaceView与OpenCV实现多格式图片加载的方案,主要通过SurfaceView的子线程绘制能力OpenCV的图片格式转换能力协同工作,实现步骤:
-
OpenCV 环境初始化,支持跨格式转换
// 加载图片之前完成初始化 val initOpenCV = OpenCVLoader.initLocal() if (initOpenCV){ // 初始化成功 } -
SurfaceView 基础设置,监听Surface的生命周期
mSurfaceView.holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(p0: SurfaceHolder) { //当Surface首次创建完成后调用 mHolder = p0 //保存SurfaceHolder,供后续使用 isDraw = true //设置变量控制绘制线程 startDrawThread() // 启动绘制线程 } override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) { //Surface的尺寸或格式发生变化时调用 mHolder = p0 // 更新全局变量mHolder } override fun surfaceDestroyed(p0: SurfaceHolder) { //当Surface被销毁前调用 isDraw = false clearQueue() } }) -
创建绘制线程,通过独立线程避免 UI 阻塞
3.1. 新建阻塞队列,使用LinkedBlockingQueue存储待绘制的Bitmap,控制内存占用var bitmapQueue = LinkedBlockingQueue<Bitmap>(1) // 大小为1的阻塞队列 // 添加Bitmap到队列(队列满时自动丢弃最旧的帧) fun addBitmap(bitmap: Bitmap?) { //剩余容量为0,丢弃队首的旧帧 if (bitmapQueue.remainingCapacity() == 0) { val oldBitmap: Bitmap = bitmapQueue.poll() if (oldBitmap != null && !oldBitmap.isRecycled) { oldBitmap.recycle() // 回收Bitmap资源 } } bitmapQueue.offer(bitmap) // 非阻塞添加新帧 } // 清理队列中的Bitmap资源 fun clearQueue() { for (bitmap in bitmapQueue) { if (bitmap != null && !bitmap.isRecycled) { bitmap.recycle() } } bitmapQueue.clear() }3.2. 绘制线程,不断获取Bitmap并绘制到SurfaceView上
fun startDrawThread(){ lifecycleScope.launch(Dispatchers.IO) { while (isDraw){ // 从队列获取Bitmap(阻塞直到有可用帧) var drawBitmap = bitmapQueue.take() drawBitmap?.let { if (!it.isRecycled){ var canvas:Canvas?=null try { canvas = mHolder?.lockCanvas() // 获取画布 canvas?.let {it1-> val viewRect = Rect(0, 0, viewW, viewH) it1.drawBitmap(it, null, viewRect, imgPaint) } }catch (e:Exception){ e.printStackTrace() }finally { mHolder?.unlockCanvasAndPost(canvas) // 提交绘制内容 it.recycle() // 绘制完成后回收资源 } } } } } }关键说明
- 线程安全
- surfaceCreated()中启动绘制线程
- surfaceDestroyed()中停止线程并清理资源
- 使用isDraw控制线程生命周期
- 队列管理
- 队列大小设为1(根据需求可调整),避免内存堆积
- addBitmap()方法实现队列满时丢弃最旧帧并回收资源
- 绘制线程通过take()阻塞获取新帧,队列空时自动等待
- 资源回收
- 绘制完成后立即recycle() Bitmap,减少内存占用
- surfaceDestroyed()中清理队列残留资源
- 添加前检查Bitmap有效性,避免操作已回收资源
- 线程安全
-
OpenCV格式转换,实现多种图像格式字节流(ByteArray)到Android Bitmap的转换
4.1. 灰度图->Bitmapval grayMat = Mat(height, width, CvType.CV_8UC1).apply { put(0, 0, gray) } Imgproc.cvtColor(grayMat, rgbMat, Imgproc.COLOR_GRAY2RGB) val bitmap = Bitmap.createBitmap(rgbMat.cols(), rgbMat.rows(), Bitmap.Config.RGB_565) Utils.matToBitmap(rgbMat, bitmap)4.2. BGR->Bitmap
val bgrMat = Mat(height, width, CvType.CV_8UC3).apply { put(0, 0, bgr) } Imgproc.cvtColor(bgrMat, rgbMat, Imgproc.COLOR_BG

最低0.47元/天 解锁文章
1515

被折叠的 条评论
为什么被折叠?



