You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CameraX无法保存图片至相册:出现错误提示‘Stop reading file since a wrong offset...’

CameraX拍照保存失败问题排查与解决

问题描述

已在Manifest配置相机和存储权限,相机页面正常显示,但点击拍照时触发保存失败错误,堆栈跟踪核心报错为java.io.IOException: Could not rename to /storage/emulated/0/Pictures/xxx.jpg.tmp。

原代码

public class CameraActivity extends AppCompatActivity {

    private final int MY_CAMERA_REQUEST_CODE = 100;
    private final int MY_EXT_STORAGE_REQUEST_CODE = 101;
    private TextureView view_finder;
    private Button closeCamBtn;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera);

        view_finder = findViewById(R.id.view_finder);

        if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.CAMERA}, MY_CAMERA_REQUEST_CODE);
        }
        if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
            requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MY_EXT_STORAGE_REQUEST_CODE);
        }
        else{
            startCamera();
        }

        // handling cancel-button from camera-activity
        closeCamBtn = findViewById(R.id.closeCam);
        closeCamBtn.setOnClickListener(v -> finish());
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == MY_CAMERA_REQUEST_CODE) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this, "camera permission granted", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "camera permission denied", Toast.LENGTH_LONG).show();
            }
        }

        if (requestCode == MY_EXT_STORAGE_REQUEST_CODE) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this, "write ext storage permission granted", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "write ext storage permission denied", Toast.LENGTH_LONG).show();
            }
        }
    }


    // TODO always yields onError()

    private void startCamera() {
        CameraX.unbindAll();

        Rational aspectRatio = new Rational(view_finder.getWidth(), view_finder.getHeight());
        Size screen = new Size(view_finder.getWidth(), view_finder.getHeight());

        PreviewConfig pConfig = new PreviewConfig.Builder().setTargetAspectRatio(aspectRatio).setTargetResolution(screen).build();
        Preview preview = new Preview(pConfig);

        preview.setOnPreviewOutputUpdateListener(
                output -> {
                    ViewGroup parent = (ViewGroup) view_finder.getParent();
                    parent.removeView(view_finder);
                    parent.addView(view_finder);

                    view_finder.setSurfaceTexture(output.getSurfaceTexture());
                }
        );

        ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig.Builder().setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY).
                setTargetRotation(getWindowManager().getDefaultDisplay().getRotation()).build();

        final ImageCapture imgCap = new ImageCapture(imageCaptureConfig);

        findViewById(R.id.imgCapture).setOnClickListener(v -> {
            String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
            File file = new File(path, "ThisIsATestFile" + System.currentTimeMillis() + ".jpg");

            Toast.makeText(getBaseContext(), "File would be: " + file.getAbsolutePath(),Toast.LENGTH_LONG).show();

            imgCap.takePicture(file, new ImageCapture.OnImageSavedListener() {

                @Override
                public void onImageSaved(@NonNull File file) {
                    String msg = "Photo capture succeeded: " + file.getAbsolutePath();
                    Toast.makeText(getBaseContext(), msg,Toast.LENGTH_LONG).show();
                }

                @Override
                public void onError(@NonNull ImageCapture.UseCaseError useCaseError, @NonNull String message, @Nullable Throwable cause) {
                    String msg = "Photo capture failed: " + message;
                    Toast.makeText(getBaseContext(), msg,Toast.LENGTH_LONG).show();
                    if(cause != null) cause.printStackTrace();
                }
            });
        });
        CameraX.bindToLifecycle(this, preview, imgCap);
    }
}

堆栈跟踪日志

D/Camera: issue capture request for camera 0
D/CaptureSession: Issuing capture request.
W/ExifInterface: Stop reading file since a wrong offset may cause an infinite loop: 0
W/ExifInterface: Skip the tag entry since data format (ULONG) is unexpected for tag: GPSAltitudeRef
W/ExifInterface: Stop reading file since a wrong offset may cause an infinite loop: 0
W/ExifInterface: Stop reading file since a wrong offset may cause an infinite loop: 0
W/System.err: java.io.IOException: Could not rename to /storage/emulated/0/Pictures/ThisIsATestFile1674077498232.jpg.tmp
W/System.err:     at androidx.exifinterface.media.ExifInterface.saveAttributes(ExifInterface.java:4307)
W/System.err:     at androidx.camera.core.Exif.save(Exif.java:132)
W/System.err:     at androidx.camera.core.ImageSaver.run(ImageSaver.java:91)
W/System.err:     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
W/System.err:     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
W/System.err:     at java.lang.Thread.run(Thread.java:923)

解决方法

1. 修复权限申请逻辑漏洞

原代码仅在存储权限已授予时启动相机,若相机和存储权限同时未授予,权限申请后startCamera()不会触发。修改权限处理逻辑:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_camera);

    view_finder = findViewById(R.id.view_finder);
    closeCamBtn = findViewById(R.id.closeCam);
    closeCamBtn.setOnClickListener(v -> finish());

    boolean cameraGranted = checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
    boolean storageGranted = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
    
    if (!cameraGranted || !storageGranted) {
        requestPermissions(
                new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                102 // 统一申请请求码
        );
    } else {
        startCamera();
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    boolean cameraGranted = checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
    boolean storageGranted = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
    
    if (cameraGranted && storageGranted) {
        startCamera();
        Toast.makeText(this, "权限已全部授予", Toast.LENGTH_LONG).show();
    } else {
        Toast.makeText(this, "缺少必要权限", Toast.LENGTH_LONG).show();
    }
}

2. 适配Android 10+存储机制

Android 10及以上推荐使用MediaStore保存媒体文件,避免直接操作外部存储的权限限制:

findViewById(R.id.imgCapture).setOnClickListener(v -> {
    ContentValues values = new ContentValues();
    values.put(MediaStore.Images.Media.DISPLAY_NAME, "ThisIsATestFile" + System.currentTimeMillis() + ".jpg");
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
    values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);

    Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    if (uri != null) {
        imgCap.takePicture(
                new ImageCapture.OutputFileOptions.Builder(getContentResolver(), uri, values).build(),
                ContextCompat.getMainExecutor(this),
                new ImageCapture.OnImageSavedCallback() {
                    @Override
                    public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
                        String msg = "照片保存成功: " + uri.toString();
                        Toast.makeText(CameraActivity.this, msg, Toast.LENGTH_LONG).show();
                        // 通知相册刷新
                        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
                    }

                    @Override
                    public void onError(@NonNull ImageCaptureException exception) {
                        String msg = "照片保存失败: " + exception.getMessage();
                        Toast.makeText(CameraActivity.this, msg, Toast.LENGTH_LONG).show();
                        exception.printStackTrace();
                    }
                }
        );
    }
});

3. 修复TextureView尺寸初始化问题

startCamera()中直接获取view_finder的宽高可能为0(视图未完成测量),导致预览配置异常,改用post()延迟执行:

private void startCamera() {
    view_finder.post(() -> {
        CameraX.unbindAll();

        Rational aspectRatio = new Rational(view_finder.getWidth(), view_finder.getHeight());
        Size screen = new Size(view_finder.getWidth(), view_finder.getHeight());

        PreviewConfig pConfig = new PreviewConfig.Builder()
                .setTargetAspectRatio(aspectRatio)
                .setTargetResolution(screen)
                .build();
        Preview preview = new Preview(pConfig);

        preview.setOnPreviewOutputUpdateListener(output -> {
            ViewGroup parent = (ViewGroup) view_finder.getParent();
            parent.removeView(view_finder);
            parent.addView(view_finder);
            view_finder.setSurfaceTexture(output.getSurfaceTexture());
        });

        // 其余ImageCapture配置和绑定逻辑保持不变
        CameraX.bindToLifecycle(this, preview, imgCap);
    });
}

4. 确保存储目录存在(可选,针对直接文件操作)

若坚持使用原始文件保存方式,需先确认目录存在:

String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
File dir = new File(path);
if (!dir.exists()) {
    dir.mkdirs(); // 创建目录及父目录
}
File file = new File(dir, "ThisIsATestFile" + System.currentTimeMillis() + ".jpg");

内容的提问来源于stack exchange,提问作者J_R

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 14:40:16