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

React Native:上传相机拍摄照片至API时应用崩溃问题

问题描述

React Native应用在Android模拟器中运行正常,但在Galaxy A30真机上,用户尝试上传相机拍摄的照片到API时会崩溃。从相册选择照片提交无异常,模拟器中无报错输出,仅相机上传场景出现崩溃。

相关代码
import React, {useState, useContext} from 'react';
import {ScrollView, View, Text, TextInput, TouchableOpacity, Alert} from 'react-native';
import { AuthContext } from '../Context/AuthContext';
import { URLs } from '../constants/links';
import * as ImagePicker from 'expo-image-picker';
import axios from 'axios';
import * as Permissions from "expo-permissions";
import { CAMERA } from "expo-permissions";

const MyScreen = ({navigation}) => {
    const { myToken } = useContext(AuthContext)
    const [allImage, setAllImage] = React.useState([]);
    const [pickedImage, setPickedImage] = useState("");
    const [fileName, setFileName] = React.useState(""); 
    const formdata = new FormData();
    
    const cameraPermission = async () => {
      const result = await Permissions.askAsync(CAMERA);
      if (result.status != "granted") {
        Alert.alert(
          "Insufficient Permission",
          "You need to grant camera permission to use this app",
          [{ text: "Okay" }]
        );
        return true;
      }
      return true;
    };
    
    const useCamera = async () => {
        const hasPermissions = await cameraPermission();
        if (!hasPermissions) {
            return;
        }
        if(allImage.length < 4){
            let result = await ImagePicker.launchCameraAsync({
                allowsEditing: true,
                quality: 0.5,
            });

            if (!result.cancelled) { 
                const name = result.uri.split('/').pop();
                let match = /\.(\w+)$/.exec(name);
                let type = match ? `image/${match[1]}` : `image`;
                let newFile = {
                    uri: result.uri,
                    type: type,
                    name: name
                }
                setAllImage(newFile)
                setPickedImage(result.uri)
                if (!pickedImage && allImage.length === 0) {
                    setAllImage([newFile]); 
                    setFileName("Photo 1")
                }else {
                    setAllImage([...allImage, newFile]); 
                    setFileName(fileName + ", Photo " + (allImage.length + 1))  
                }
            }
        } else  { 
            Alert.alert("Image", "You have reach the image upload limit");
        }
    };

    const fetchData = () => {
        const abortCont = new AbortController(); 
        allImage.forEach((file) => {
            formdata.append('files[]', file);
        });
        axios({
            method: 'post',
            url: URLs,
            headers: {
                Accept: "application/json",
                Authorization: myToken,
                'Content-Type': "multipart/form-data", 
            },
            data: formdata,
            signal: abortCont.signal,
        }).then(function (result) { 
            if(result.data.message === "Successfully added") {
                Alert.alert("Upload Successufull", result.data.message);
                navigation.goBack()
            }

        }).catch(function (error) { 
            Alert.alert("Error", error); 
            formdata = new FormData();
        });
        
        return () => abortCont.abort();
    }
 

    return (
        <ScrollView>
            <View>
                <View>
                    <Text>Attach Receipt File</Text>
                    <View>
                        <TextInput 
                            editable={false}
                            placeholder="Select files.."
                            value={fileName}
                        />
                    </View>
                    <View>
                        <TouchableOpacity activeOpacity={0.8} onPress={useCamera}>
                            <Text>Camera</Text>
                        </TouchableOpacity>
                    </View>

                    <View>
                        <TouchableOpacity activeOpacity={0.9} onPress={fetchData}>
                            <Text>Submit</Text>
                        </TouchableOpacity>
                    </View>
                </View>
            </View>
        </ScrollView>
    );
}

export default MyScreen;
问题排查与修复方案

核心问题分析

  1. FormData实例复用异常:formdata定义在组件顶层,每次提交都会往同一实例追加文件,导致数据混乱;catch块中重新赋值formdata会引发变量绑定异常,无法清空原有数据。
  2. 数组状态被覆盖为对象:useCamera中存在setAllImage(newFile),直接将数组状态改为单个文件对象,后续allImage.forEach会因对象无此方法抛出错误,真机环境下直接触发崩溃。
  3. 废弃权限API兼容性问题:expo-permissions已被废弃,旧权限请求方法在部分Android真机上可能存在权限获取不完整的情况。
  4. Android相机URI格式问题:相机返回的content://格式URI在FormData中无法被正确解析,导致上传失败崩溃。

修复步骤

  1. 在fetchData内部创建FormData
    每次提交时新建FormData实例,避免数据复用:

    const fetchData = () => {
        const abortCont = new AbortController(); 
        const formdata = new FormData(); // 移到内部创建
        allImage.forEach((file) => {
            formdata.append('files[]', file);
        });
        // 后续请求代码不变
    }
    
  2. 修复useCamera中的状态错误
    删除错误的setAllImage(newFile)行,确保allImage始终保持数组类型:

    const useCamera = async () => {
        // 权限判断代码不变
        if (!result.cancelled) { 
            // newFile创建代码不变
            // 删除 setAllImage(newFile) 这一行
            setPickedImage(result.uri)
            if (allImage.length === 0) {
                setAllImage([newFile]); 
                setFileName("Photo 1")
            }else {
                setAllImage([...allImage, newFile]); 
                setFileName(fileName + ", Photo " + (allImage.length + 1))  
            }
        }
    };
    
  3. 替换为最新的权限请求API
    使用expo-image-picker自带的权限方法,替代已废弃的expo-permissions:

    // 删除 import * as Permissions from "expo-permissions"; 和 import { CAMERA } from "expo-permissions";
    const cameraPermission = async () => {
        const { status } = await ImagePicker.requestCameraPermissionsAsync();
        if (status !== "granted") {
            Alert.alert(
                "权限不足",
                "需要授予相机权限才能使用此功能",
                [{ text: "确定" }]
            );
            return false; // 权限未授予返回false
        }
        return true;
    };
    
  4. 处理Android相机URI格式
    安装expo-file-system,将content:// URI转换为应用可访问的文件路径:

    npx expo install expo-file-system
    

    在useCamera中处理URI:

    import * as FileSystem from 'expo-file-system';
    import { Platform } from 'react-native';
    
    const useCamera = async () => {
        // 权限判断和相机调用代码不变
        if (!result.cancelled) { 
            let uri = result.uri;
            // 处理Android的content:// URI
            if (Platform.OS === 'android' && uri.startsWith('content://')) {
                const fileInfo = await FileSystem.getInfoAsync(uri);
                uri = fileInfo.uri; // 转换为file://格式
            }
            // newFile创建和状态更新代码不变
        }
    };
    
  5. 优化状态逻辑(可选)
    移除冗余的pickedImage状态,直接通过allImage数组管理选中图片,简化逻辑:

    // 移除 const [pickedImage, setPickedImage] = useState("");
    const useCamera = async () => {
        // 代码不变,仅删除与pickedImage相关的判断和赋值
        if (!result.cancelled) { 
            // newFile创建代码不变
            if (allImage.length === 0) {
                setAllImage([newFile]); 
                setFileName("Photo 1")
            }else {
                setAllImage([...allImage, newFile]); 
                setFileName(fileName + ", Photo " + (allImage.length + 1))  
            }
        }
    };
    

修复后完整代码

import React, {useState, useContext} from 'react';
import {ScrollView, View, Text, TextInput, TouchableOpacity, Alert, Platform} from 'react-native';
import { AuthContext } from '../Context/AuthContext';
import { URLs } from '../constants/links';
import * as ImagePicker from 'expo-image-picker';
import axios from 'axios';
import * as FileSystem from 'expo-file-system';

const MyScreen = ({navigation}) => {
    const { myToken } = useContext(AuthContext)
    const [allImage, setAllImage] = React.useState([]);
    const [fileName, setFileName] = React.useState(""); 
    
    const cameraPermission = async () => {
        const { status } = await ImagePicker.requestCameraPermissionsAsync();
        if (status !== "granted") {
            Alert.alert(
                "权限不足",
                "需要授予相机权限才能使用此功能",
                [{ text: "确定" }]
            );
            return false;
        }
        return true;
    };
    
    const useCamera = async () => {
        const hasPermissions = await cameraPermission();
        if (!hasPermissions) {
            return;
        }
        if(allImage.length < 4){
            let result = await ImagePicker.launchCameraAsync({
                allowsEditing: true,
                quality: 0.5,
            });

            if (!result.cancelled) { 
                let uri = result.uri;
                if (Platform.OS === 'android' && uri.startsWith('content://')) {
                    const fileInfo = await FileSystem.getInfoAsync(uri);
                    uri = fileInfo.uri;
                }
                const name = uri.split('/').pop();
                let match = /\.(\w+)$/.exec(name);
                let type = match ? `image/${match[1]}` : `image`;
                let newFile = {
                    uri: uri,
                    type: type,
                    name: name
                }
                if (allImage.length === 0) {
                    setAllImage([newFile]); 
                    setFileName("Photo 1")
                }else {
                    setAllImage([...allImage, newFile]); 
                    setFileName(fileName + ", Photo " + (allImage.length + 1))  
                }
            }
        } else  { 
            Alert.alert("提示", "已达到图片上传上限");
        }
    };

    const fetchData = () => {
        const abortCont = new AbortController(); 
        const formdata = new FormData();
        allImage.forEach((file) => {
            formdata.append('files[]', file);
        });
        axios({
            method: 'post',
            url: URLs,
            headers: {
                Accept: "application/json",
                Authorization: myToken,
                'Content-Type': "multipart/form-data", 
            },
            data: formdata,
            signal: abortCont.signal,
        }).then(function (result) { 
            if(result.data.message === "Successfully added") {
                Alert.alert("上传成功", result.data.message);
                navigation.goBack()
            }

        }).catch(function (error) { 
            Alert.alert("错误", error.message || "上传失败"); 
        });
        
        return () => abortCont.abort();
    }
 

    return (
        <ScrollView>
            <View style={{padding: 16}}>
                <View>
                    <Text style={{fontSize: 16, marginBottom: 8}}>Attach Receipt File</Text>
                    <View style={{borderWidth: 1, borderColor: '#ccc', borderRadius: 4, padding: 8, marginBottom: 16}}>
                        <TextInput 
                            editable={false}
                            placeholder="Select files.."
                            value={fileName}
                        />
                    </View>
                    <View style={{marginBottom: 16}}>
                        <TouchableOpacity activeOpacity={0.8} onPress={useCamera} style={{backgroundColor: '#e0e0e0', padding: 12, borderRadius: 4}}>
                            <Text style={{textAlign: 'center'}}>Camera</Text>
                        </TouchableOpacity>
                    </View>

                    <View>
                        <TouchableOpacity activeOpacity={0.9} onPress={fetchData} style={{backgroundColor: '#2196F3', padding: 12, borderRadius: 4}}>
                            <Text style={{textAlign: 'center', color: 'white'}}>Submit</Text>
                        </TouchableOpacity>
                    </View>
                </View>
            </View>
        </ScrollView>
    );
}

export default MyScreen;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 15:57:51