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

Android下react-native-vision-camera过滤后图片无法保存到外部存储求助

问题根因

  1. convertImageProxyToBitmap逻辑完全错误
    react-native-vision-camera返回的ImageProxy默认是YUV_420_888格式的原始帧数据,不是JPEG编码的字节流,你直接读取第一个平面的字节调用BitmapFactory.decodeByteArray解码,最终得到的是null,后续滤镜处理、图片保存步骤全部因为空对象报错。
  2. 存储路径不兼容高版本Android
    Android 10(API 29)开始分区存储规则生效,Environment.getExternalStorageDirectory()返回的公共存储根目录已经禁止直接写入,即使申请了存储权限也会触发IO异常。
  3. 缺少空判断逻辑
    你没有对转码后的Bitmap、滤镜处理后的Bitmap做非空校验,发生异常时没有错误日志定位具体问题。

修复后完整代码

package com.story;

import android.util.*;
import android.content.Context;
import android.graphics.YuvImage;
import android.graphics.Rect;
import java.lang.String;
import java.io.File;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.*;

import androidx.camera.core.ImageProxy;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import org.wysaid.nativePort.CGENativeLibrary;

import com.facebook.react.bridge.WritableNativeMap;
import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin;
import org.jetbrains.annotations.NotNull;

public class FrameEffectPlugin extends FrameProcessorPlugin {

    public static String status = "";
    // 新增Context变量,用来获取App专属缓存目录,初始化的时候传入
    private Context mContext;

    // YUV_420_888转Bitmap的正确实现
    private Bitmap convertImageProxyToBitmap(ImageProxy image) {
        ImageProxy.PlaneProxy[] planes = image.getPlanes();
        ByteBuffer yBuffer = planes[0].getBuffer();
        ByteBuffer uBuffer = planes[1].getBuffer();
        ByteBuffer vBuffer = planes[2].getBuffer();

        int ySize = yBuffer.remaining();
        int uSize = uBuffer.remaining();
        int vSize = vBuffer.remaining();

        byte[] nv21 = new byte[ySize + uSize + vSize];
        yBuffer.get(nv21, 0, ySize);
        vBuffer.get(nv21, ySize, vSize);
        uBuffer.get(nv21, ySize + vSize, uSize);

        YuvImage yuvImage = new YuvImage(nv21, android.graphics.ImageFormat.NV21, image.getWidth(), image.getHeight(), null);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        yuvImage.compressToJpeg(new Rect(0, 0, image.getWidth(), image.getHeight()), 100, out);
        byte[] imageBytes = out.toByteArray();
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
    }

    // 修改存储路径为App专属缓存目录,无需动态权限即可写入
    public String saveBitmap(Bitmap finalBitmap, String fname) {
        // 用App外部缓存目录,兼容所有Android版本
        File myDir = new File(mContext.getExternalCacheDir(), "cache-images");
        if(!myDir.exists() || !myDir.isDirectory()){
            myDir.mkdirs();
        }

        File file = new File(myDir, fname);
        if (file.exists()) file.delete();
        try {
            FileOutputStream out = new FileOutputStream(file);
            finalBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
            out.flush();
            out.close();
            status = "success";
        } catch (Exception e) {
            status = "exception while saving bitmap: " + e.getMessage();
            e.printStackTrace();
            return "";
        }
        return file.getAbsolutePath();
    }

    @Override
    public Object callback(@NotNull ImageProxy image, Object[] params) {
        status = "";
        Bitmap bitmapImg = convertImageProxyToBitmap(image);
        // 加非空判断
        if (bitmapImg == null) {
            status = "convert ImageProxy to Bitmap failed";
            WritableNativeMap map = new WritableNativeMap();
            map.putString("path", "");
            map.putString("status", status);
            return map;
        }

        String ruleString = "@adjust hsl 0.02 -0.31 -0.17";
        Bitmap dstImage = CGENativeLibrary.filterImage_MultipleEffects(bitmapImg, ruleString, 1.0f);
        if (dstImage == null) {
            status = "filter image failed";
            WritableNativeMap map = new WritableNativeMap();
            map.putString("path", "");
            map.putString("status", status);
            return map;
        }

        String timeStamp = System.currentTimeMillis() + "";
        String fname = "storyCache_"+ timeStamp +".jpg";
        String path = saveBitmap(dstImage, fname);

        WritableNativeMap map = new WritableNativeMap();
        map.putString("path", path);
        map.putString("status", status);
        // 回收Bitmap避免内存泄漏
        bitmapImg.recycle();
        dstImage.recycle();
        return map;
    }

    // 修改构造方法传入Context
    FrameEffectPlugin(Context context) {
        super("addFrameEffect");
        mContext = context;
    }
}

额外注意事项

  • 注册插件的时候需要传入Context,示例:FrameProcessorPlugin.register(new FrameEffectPlugin(context))
  • 如果你使用的是react-native-vision-camera v2+版本,确保帧处理器的格式配置正确,不需要额外修改格式默认就是YUV_420_888
  • 如果还是报错可以查看logcat里的具体异常信息,修复后的status会返回具体错误原因方便定位

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 08:36:03