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上下文:
TimingModule的定时器在应用被系统回收资源后,仍尝试触发回调,但此时React Native实例已消失- 动画节点在应用重启后,尝试连接到已被销毁的UIManager实例
- 应用后台时未正确清理定时器、动画或异步任务,导致这些任务在应用重启后变成"僵尸任务"
解决方案
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
相关产品推荐
相关产品推荐

