Android下react-native-vision-camera过滤后图片无法保存到外部存储求助
问题根因
convertImageProxyToBitmap逻辑完全错误
react-native-vision-camera返回的ImageProxy默认是YUV_420_888格式的原始帧数据,不是JPEG编码的字节流,你直接读取第一个平面的字节调用BitmapFactory.decodeByteArray解码,最终得到的是null,后续滤镜处理、图片保存步骤全部因为空对象报错。- 存储路径不兼容高版本Android
Android 10(API 29)开始分区存储规则生效,Environment.getExternalStorageDirectory()返回的公共存储根目录已经禁止直接写入,即使申请了存储权限也会触发IO异常。 - 缺少空判断逻辑
你没有对转码后的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
相关产品推荐
相关产品推荐

