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

React Native Expo APK构建后出现React Native实例消失及动画节点连接失败的循环加载问题

React Native Expo APK构建后出现React Native实例消失及动画节点连接失败的循环加载问题

问题描述

我在React Native Expo应用的APK构建版本中遇到了一个仅出现在生产包的错误,开发构建完全正常。错误触发场景非常特定:

  • 调用通用事件组件中的createEvento函数后,至少等待5分钟
  • 关闭应用,15分钟后再次打开并尝试执行相同操作
  • 应用随即陷入无限加载的循环

我通过Android Studio的LogCat捕获到两个核心错误:

错误1:React Native Instance已消失

E  Unhandled SoftException
java.lang.RuntimeException: React Native Instance has already disappeared: requested by Timing
at com.facebook.react.bridge.BaseJavaModule.getReactApplicationContextIfActiveOrWarn(BaseJavaModule.java:125)
at com.facebook.react.modules.core.TimingModule.access$000(TimingModule.java:22)
at com.facebook.react.modules.core.TimingModule$BridgeTimerExecutor.callTimers(TimingModule.java:28)
at com.facebook.react.modules.core.JavaTimerManager.createAndMaybeCallTimer(JavaTimerManager.java:345)
at com.facebook.react.modules.core.TimingModule.createTimer(TimingModule.java:84)
at java.lang.reflect.Method.invoke(Native Method)
at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:146)
at com.facebook.jni.NativeRunnable.run(Native Method)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:27)
at android.os.Looper.loop(Looper.java:246)
at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:233)

错误2:动画节点无法连接到UIManager

E  Unhandled SoftException
com.facebook.react.bridge.ReactNoCrashSoftException: connectAnimatedNodeToView: Animated node could not be connected to UIManager - uiManager disappeared for tag: 797
at com.facebook.react.animated.NativeAnimatedNodesManager.connectAnimatedNodeToView(NativeAnimatedNodesManager.java:438)
at com.facebook.react.animated.NativeAnimatedModule$18.execute(NativeAnimatedModule.java:838)
at com.facebook.react.animated.NativeAnimatedModule$ConcurrentOperationQueue.executeBatch(NativeAnimatedModule.java:171)
at com.facebook.react.animated.NativeAnimatedModule$3.execute(NativeAnimatedModule.java:365)
at com.facebook.react.uimanager.UIViewOperationQueue$UIBlockOperation.execute(UIViewOperationQueue.java:557)
at com.facebook.react.uimanager.UIViewOperationQueue$1.run(UIViewOperationQueue.java:910)
at com.facebook.react.uimanager.UIViewOperationQueue.flushPendingBatches(UIViewOperationQueue.java:1023)
at com.facebook.react.uimanager.UIViewOperationQueue.-$$Nest$mflushPendingBatches(Unknown Source:0)
at com.facebook.react.uimanager.UIViewOperationQueue$2.runGuarded(UIViewOperationQueue.java:979)
at com.facebook.react.bridge.GuardedRunnable.run(GuardedRunnable.java:29)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8653)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)

相关组件代码

import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, TextInput, Alert, AppState, AppStateStatus } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import BackgroundCustomColor from '@/components/Jornada/BackgroundCustomColor';
import { MaterialCommunityIcons, Octicons } from '@expo/vector-icons';
import { ScrollView } from 'react-native-gesture-handler';
import { ConfirmationJourney } from '@/components/Jornada/ConfirmationJourneyBackground';
import { Timer } from '@/components/Jornada/Timer';
import EventoService from '@/services/api/eventosService';
import QRCodeReader from '@/components/QRCodeReader';
import { verificarLiberacaoQRCode } from '@/utils/verifyParametros';
import UsuarioService from '@/services/api/usuarioService';
import Loading from '@/components/Loading';
import AlertService from '@/services/api/alertService';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@/types/navigation';
import { useEventosDatabase } from '@/database/useEventosDatabase';
import deactivateBackButton from '@/utils/deactivateBackButton';
import DropDownPicker from 'react-native-dropdown-picker';
import { LISTA_VEICULOS_KEY } from '@/constants/consts';
import { IVeiculo } from '@/types/Veiculo';
import { useAuth } from '@/services/auth/AuthContext';

interface EventProps {
    title: string;
    color: string;
    startEvent: string;
    endEvent: string;
    icon: React.ReactNode;
    confirmationIcon: React.ReactNode;
    isRefeicao?: boolean;
    isDirecao?: boolean;
    isAbastecimento?: boolean;
}

export default function Event({ title, color, startEvent, endEvent, icon, confirmationIcon, isRefeicao, isDirecao, isAbastecimento }: EventProps) {
    const navigation = useNavigation<NavigationProp>();
    const { authState } = useAuth();
    const { createEvento, checkIsUltimoEvento, sendDirecaoData, sendAbastecimentoData } = EventoService();
    const { checkTempoMinRefeicao, checkTempoMaxRefeicao, checkDirecaoTempoMax, checkEncerramentoTurno, checkLimiteEncerramentoTurno } = AlertService();
    const [time, setTime] = useState<number>(0);
    const [startTimer, setStartTimer] = useState<boolean>(false);
    const [confirmationBox, setConfirmationBox] = useState(false);

    const { confirmarLeituraQRCode } = UsuarioService();
    const [QRCodeScanned, setQRCodeScanned] = useState<string | null>();
    const [openScanner, setOpenScanner] = useState<boolean>(false);
    const [loading, setLoading] = useState<boolean>(false);

    const [instanteEvento, setInstanteEvento] = useState<number | null>(null);

    // State para Direcao
    const [dataEvento, setDataEvento] = useState<number>();
    const [dataFimEvento, setDataFimEvento] = useState<number>();
    const [hodometroValue, setHodometroValue] = useState<string>();
    const [hodometroInicial, setHodometroInicial] = useState<string>();
    const [hodometroFinal, setHodometroFinal] = useState<string>();
    const [openFirstDropdown, setOpenFirstDropdown] = useState(false);
    const [itemsFirstDropdown, setItemsFirstDropdown] = useState([
        { label: 'Carregando', value: 1 },
        { label: 'Vazio', value: 2 }
    ]);
    const [valueFirstDropdown, setValueFirstDropdown] = useState(itemsFirstDropdown[0].value);

    const [openSecondDropdown, setOpenSecondDropdown] = useState(false);
    const [itemsSecondDropdown, setItemsSecondDropdown] = useState<{ label: string, value: string | number, hodometro: string | number }[]>([]);
    const [valueSecondDropdown, setValueSecondDropdown] = useState<string>();

    // States para Abastecimento
    const [litrosValue, setLitrosValue] = useState<number | null>(null);
    const [valorPorLitroValue, setValorPorLitroValue] = useState<number | null>(null);
    const [valorTotalValue, setValorTotalValue] = useState<number | null>(null);
    const [openDropdown, setOpenDropdown] = useState(false);
    const [itemsDropdown, setItemsDropdown] = useState([
        { label: 'Alcool', value: 'AL' },
        { label: 'Gasolina', value: 'GA' },
        { label: 'Diesel', value: 'DI' }
    ]);
    const [valueDropdown, setValueDropdown] = useState(itemsDropdown[0].value);

    useEffect(() => {
        deactivateBackButton(startTimer);

        checkIsUltimoEvento(startEvent).then((isLastEvento) => {
            if (isLastEvento?.isUltimoEvento) {
                setStartTimer(true);
                setInstanteEvento(isLastEvento.instanteEvento);
            }
        });

        SecureStore.getItemAsync(LISTA_VEICULOS_KEY).then((result) => {
            const veiculosData = JSON.parse(result!);

            setHodometroValue(veiculosData[0].ultimoHodometro.toString() + ".00");

            if (veiculosData) {
                setItemsSecondDropdown(veiculosData?.map((item: IVeiculo) => {
                    return { label: item.placa, value: item.placa, hodometro: item.ultimoHodometro?.toString() };
                }));

                setValueSecondDropdown(veiculosData[0]?.placa);
            }
        }).catch((error) => console.log("Erro ao buscar lista de veiculos", error));
    }, [startTimer]);

    const handleAppStateChange = (nextAppState: AppStateStatus) => {
        if (nextAppState === 'active') {
            console.log('App has come to the foreground!');
        }
    };

    useEffect(() => {
        const subscription = AppState.addEventListener('change', handleAppStateChange);
        return () => {
            subscription.remove();
        };
    }, []);

    const handleConfirmJourney = async () => {
        try {
            if (!startTimer) {
                setLoading(true);

                setStartTimer(true);
                const res = await createEvento(startEvent);
                setDataEvento(res);

                setInstanteEvento(res);

                if (isDirecao) {
                    setHodometroInicial(hodometroValue);
                    await checkDirecaoTempoMax();
                }

                await checkTempoMaxRefeicao();
                setLoading(false);
            } else if (startTimer) {
                setLoading(true);

                const res = await createEvento(endEvent);
                setStartTimer(false);
                setDataFimEvento(res);
                
                // 省略后续业务逻辑
                setLoading(false);
            }
        } catch (error) {
            setLoading(false);
            Alert.alert("Erro", "Ocorreu um erro ao confirmar a jornada");
        }
    };

    return (
        <BackgroundCustomColor color={color}>
            <ScrollView>
                {/* 组件渲染内容 */}
                <Loading visible={loading} />
            </ScrollView>
        </BackgroundCustomColor>
    );
}

问题分析

从错误日志和触发场景来看,核心问题是应用进入后台后,React Native的JS Bridge实例被系统回收,但仍有定时器、动画或异步任务在尝试访问已失效的React上下文:

  1. TimingModule的定时器在应用被系统回收资源后,仍尝试触发回调,但此时React Native实例已消失
  2. 动画节点在应用重启后,尝试连接到已被销毁的UIManager实例
  3. 应用后台时未正确清理定时器、动画或异步任务,导致这些任务在应用重启后变成"僵尸任务"

解决方案

1. 完善AppState监听,清理后台任务

修改AppState监听逻辑,在应用进入后台时主动清理定时器、动画和异步请求:

useEffect(() => {
    let timerRef: NodeJS.Timeout | null = null;
    const cancelSource = axios.CancelToken.source();

    const handleAppStateChange = async (nextAppState: AppStateStatus) => {
        if (nextAppState === 'background' || nextAppState === 'inactive') {
            // 清理定时器
            if (timerRef) clearInterval(timerRef);
            // 取消未完成的异步请求
            cancelSource.cancel('App进入后台,取消所有请求');
            // 重置定时器状态
            setStartTimer(false);
            // 清理动画(如果使用Animated API)
            Animated.timing(/* 你的动画实例 */).stop();
        } else if (nextAppState === 'active') {
            // 应用回到前台时重新初始化状态
            const isLastEvento = await checkIsUltimoEvento(startEvent);
            if (isLastEvento?.isUltimoEvento) {
                setStartTimer(true);
                setInstanteEvento(isLastEvento.instanteEvento);
            }
        }
    };

    const subscription = AppState.addEventListener('change', handleAppStateChange);
    return () => {
        subscription.remove();
        if (timerRef) clearInterval(timerRef);
        cancelSource.cancel('组件卸载,取消请求');
    };
}, [startEvent]);

2. 给异步请求添加取消支持

修改EventoService的createEvento方法,支持axios取消令牌:

// EventoService.ts
import axios, { CancelToken } from 'axios';

export const createEvento = async (eventType: string, cancelToken?: CancelToken) => {
    try {
        const res = await axios.post(`/eventos/${eventType}`, {}, {
            cancelToken: cancelToken?.token
        });
        return res.data;
    } catch (error) {
        if (axios.isCancel(error)) {
            console.log('请求已取消:', error.message);
            return null;
        }
        throw error;
    }
};

3. 优化Timer组件的生命周期

确保Timer组件在卸载或暂停时清理定时器:

// Timer.tsx
import { useEffect, useState } from 'react';
import { Text } from 'react-native';

export const Timer = ({ isRunning }: { isRunning: boolean }) => {
    const [time, setTime] = useState(0);
    let intervalId: NodeJS.Timeout | null = null;

    useEffect(() => {
        if (isRunning) {
            intervalId = setInterval(() => {
                setTime(prev => prev + 1);
            }, 1000);
        } else {
            if (intervalId) clearInterval(intervalId);
        }
        return () => {
            if (intervalId) clearInterval(intervalId);
        };
    }, [isRunning]);

    return <Text>{time}</Text>;
};

4. 临时解决方案:禁用Hermes引擎

如果使用了Hermes引擎,可能存在JS Bridge回收的兼容性问题,可尝试在app.json中切换回JSC:

{
  "expo": {
    "android": {
      "jsEngine": "jsc"
    }
  }
}

备注:内容来源于stack exchange,提问作者Carlos Curcino

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 03:23:26