Android Camera预览:SurfaceView与ImageView显示差异及帧残留Artifact问题
Camera预览Artifact问题排查与解决
问题现象
基于AOSP及SOM提供商源码开发时,使用SurfaceView搭配Camera类预览清晰,但通过onPreviewFrame回调将预览数据显示在ImageView中时,出现持续的线条类Artifact,疑似前一帧残留:
- 图像可划分为5个均等垂直区域;高分辨率下,Artifact仅出现在每个区域的第一个垂直象限,位置固定、间距均等,但并非每帧都出现所有Artifact,水平方向看似随机分布;
- 分辨率减半后,Artifact覆盖每个区域的整个水平范围,但5区域划分仍清晰可见。

相关代码片段
import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageFormat; import android.graphics.Rect; import android.graphics.YuvImage; import android.hardware.Camera; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import com.example.testapp.R; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Timer; import java.util.TimerTask; public class CameraTestFragment extends Fragment implements Camera.PreviewCallback, SurfaceHolder.Callback { private View root; private SurfaceView surfacePreview; private ImageView testPreview; private Camera camera; private boolean isActive; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { root = inflater.inflate(R.layout.fragment_camera_test, container, false); surfacePreview = root.findViewById(R.id.surface_preview_capture); testPreview = root.findViewById(R.id.testPreview); surfacePreview.getHolder().addCallback(this); updateVideoPreview(surfacePreview.getHolder()); return root; } @Override public void onDestroyView() { super.onDestroyView(); if(camera != null){ isActive = false; camera.setPreviewCallback(null); camera.stopPreview(); camera.release(); camera = null; } } @Override public void onPreviewFrame(byte[] data, Camera camera) { if(isActive) { Camera.Size previewSize = camera.getParameters().getPreviewSize(); YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, previewSize.width, previewSize.height, null); ByteArrayOutputStream out = new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0,0,previewSize.width,previewSize.height),100,out); byte[] jpegData = out.toByteArray(); Bitmap bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length); testPreview.setImageBitmap(bitmap); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d("VideoPreviewHandler", "surfaceChanged"); updateVideoPreview(holder); } @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.d("VideoPreviewHandler", "surfaceDestroyed"); } public void updateVideoPreview(SurfaceHolder holder) { Log.d("VideoPreviewHandler", "surfaceDestroyed"); camera = Camera.open(); if (camera != null) { try { Camera.Parameters cp = camera.getParameters(); cp.setPreviewSize(640, 360); cp.setPreviewFormat(ImageFormat.NV21); cp.setPreviewFpsRange(60, 60); try { camera.setParameters(cp); } catch (RuntimeException e) { Log.d("RuntimeException", e.getMessage()); } camera.setPreviewDisplay(holder); camera.setPreviewCallback(this); camera.startPreview(); isActive = true; } catch (IOException e) { e.printStackTrace(); } } else { new Timer().schedule(new TimerTask() { @Override public void run() { updateVideoPreview(surfacePreview.getHolder()); } }, 1000); } } }
疑问解答
1. SurfaceView与ImageView预览差异原因
SurfaceView预览走硬件加速的直接渲染路径,数据从Camera HAL直接输出到Surface的GPU缓冲区,无用户空间内存拷贝和额外编解码操作;而onPreviewFrame回调是将原始YUV数据拷贝到用户空间,再经过YUV转JPEG、JPEG转Bitmap的软件编解码流程,若数据处理不及时、缓冲区复用异常或格式转换对齐错误,就会出现Artifact。
2. AOSP中两种流程的差异
- SurfaceView预览流程:Camera HAL → Camera Service → SurfaceFlinger → 硬件显示控制器。数据在GPU/硬件层面流转,使用连续硬件缓冲区,由系统负责帧同步和缓冲区管理,无帧残留问题。
- onPreviewFrame回调流程:Camera HAL → Camera Service → 应用进程用户空间(
byte[] data)→ 应用内YUV转JPEG、Bitmap解码 → ImageView绘制。数据需多次内存拷贝,且应用层需自行处理帧同步和缓冲区复用,处理不当易出现数据覆盖、残留。
3. Artifact的成因与解决方法
成因
从固定区域、分辨率相关性来看,核心原因是YUV数据行对齐/stride不匹配:
- Camera输出的NV21格式数据中,实际行字节数(stride)可能因硬件要求(如16字节对齐)大于设定的预览宽度,但代码中直接用
previewSize.width创建YuvImage,导致每行数据读取不完整,下一行数据错位覆盖,形成固定位置线条。 - 分辨率变化时,stride对齐规则不变,因此区域划分模式保留,仅覆盖范围随分辨率调整。
另外,onPreviewFrame的byte[] data是复用缓冲区,若当前帧未处理完Camera已写入下一帧,会导致数据污染出现前帧残留。
解决方法
- 使用正确的stride值:通过数据总长度反推实际stride,避免直接用预览宽度:
@Override public void onPreviewFrame(byte[] data, Camera camera) { if(isActive) { Camera.Size previewSize = camera.getParameters().getPreviewSize(); int actualWidth = previewSize.width; int actualHeight = previewSize.height; // 反推Y分量的stride int yStride = (data.length - actualWidth * actualHeight / 2) / actualHeight; // 传入正确的stride数组(Y和UV分量的stride) YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, actualWidth, actualHeight, new int[]{yStride, yStride/2}); ByteArrayOutputStream out = new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0,0,actualWidth,actualHeight),100,out); byte[] jpegData = out.toByteArray(); Bitmap bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length); testPreview.setImageBitmap(bitmap); } } - 避免缓冲区复用污染:改用
setPreviewCallbackWithBuffer自行管理缓冲区:// 初始化时申请缓冲区 Camera.Size previewSize = camera.getParameters().getPreviewSize(); int bufferSize = previewSize.width * previewSize.height * 3 / 2; byte[] buffer = new byte[bufferSize]; camera.addCallbackBuffer(buffer); camera.setPreviewCallbackWithBuffer(this); // onPreviewFrame处理完后重新添加缓冲区 @Override public void onPreviewFrame(byte[] data, Camera camera) { if(isActive) { // 数据处理逻辑... camera.addCallbackBuffer(data); } } - 优化性能:将YUV转Bitmap操作放到子线程,避免主线程阻塞导致帧处理不及时,减少缓冲区被覆盖概率。
内容的提问来源于stack exchange,提问作者Sefa Teber
相关产品推荐
相关产品推荐

