React Native中byte[]与JS Bridge互传及音频流卡顿问题咨询
Let's tackle your questions one by one—you've got two main goals here: cutting out the inefficient byte[] ↔ String conversions between React Native's JS Bridge and Java, plus building a custom native View that handles your audio/WebSocket logic and integrates smoothly with React Navigation. Here's how to make it happen:
1. Bidirectional byte[] Transfer Without String/WritableArray Conversions
The root cause of your GC thrashing and playback lag is the constant Base64 encoding/decoding and array conversions—these create tons of temporary objects that overwhelm low-memory devices. Here's how to pass raw byte[] data directly between JS and Java:
Sending byte[] from Java to JS
Instead of converting to a String, wrap the byte[] in a ByteBuffer and pass it to JS as a Uint8Array (JS's native equivalent of a byte array):
import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.UiThreadUtil; import java.nio.ByteBuffer; public class AudioModule extends ReactContextBaseJavaModule { public AudioModule(ReactApplicationContext reactContext) { super(reactContext); } @Override public String getName() { return "AudioModule"; } // Example: Send raw audio bytes from Java (WebSocket) to JS private void onWebSocketAudioReceived(byte[] audioBytes) { UiThreadUtil.runOnUiThread(() -> { // Get the JS callback to send data Callback dataCallback = getReactApplicationContext().getJSModule(Callback.class); ByteBuffer buffer = ByteBuffer.wrap(audioBytes); dataCallback.invoke(null, buffer); // JS receives this as Uint8Array }); } }
In JS, you'll receive the raw data directly as a Uint8Array:
import { NativeModules } from 'react-native'; // Set up listener for audio data NativeModules.AudioModule.setAudioDataCallback((audioBytes) => { // audioBytes is a Uint8Array—no conversion needed console.log("Received raw audio chunk:", audioBytes.length); });
Receiving byte[] from JS to Java
Accept a Uint8Array in your Java method, which maps directly to a byte[] without conversion:
import com.facebook.react.bridge.Uint8Array; import com.facebook.react.bridge.Promise; @ReactMethod public void sendAudioToWebSocket(Uint8Array audioBytes, Promise promise) { byte[] rawBytes = audioBytes.getByteArray(); // Send raw bytes directly to your WebSocket endpoint yourWebSocketClient.send(rawBytes); promise.resolve(null); }
In JS, pass the raw Uint8Array directly:
// Assume you have raw audio data as Uint8Array const rawAudioChunk = new Uint8Array(yourRecordingData); await NativeModules.AudioModule.sendAudioToWebSocket(rawAudioChunk);
2. Building a Custom Java View with WebSocket/STOMP/Audio Logic + React Navigation Integration
Moving all your WebSocket, STOMP, and audio logic to a custom native View is the best way to eliminate JS Bridge overhead entirely. Here's a step-by-step implementation:
Step 1: Create the Custom View Class
Build a View that handles all native logic and manages lifecycle to avoid resource leaks:
import android.content.Context; import android.view.ViewGroup; import android.media.AudioRecorder; import android.media.AudioTrack; import okhttp3.WebSocket; public class AudioWebSocketView extends ViewGroup { private WebSocket webSocketClient; private AudioRecorder audioRecorder; private AudioTrack audioTrack; public AudioWebSocketView(Context context) { super(context); initWebSocket(); initAudioComponents(); } private void initWebSocket() { // Initialize your WebSocket/STOMP client here (e.g., using OkHttp) webSocketClient = new WebSocket.Builder() .url("your_websocket_endpoint") .build(); // Handle incoming audio data directly webSocketClient.setListener(new WebSocket.Listener() { @Override public void onMessage(WebSocket webSocket, byte[] bytes) { // Play audio immediately without JS Bridge audioTrack.write(bytes, 0, bytes.length); } }); } private void initAudioComponents() { // Initialize AudioRecorder and AudioTrack with your desired parameters audioTrack = new AudioTrack(...); audioRecorder = new AudioRecorder(...); // Send recorded bytes directly to WebSocket audioRecorder.setOnDataAvailableListener((byte[] data) -> { webSocketClient.send(data); }); } // Expose control methods for JS public void startRecording() { audioRecorder.startRecording(); } public void stopRecording() { audioRecorder.stopRecording(); } public void startPlaying() { audioTrack.play(); } public void stopPlaying() { audioTrack.stop(); } // Clean up resources when view is detached @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (webSocketClient != null) webSocketClient.close(1000, "View detached"); if (audioTrack != null) audioTrack.release(); if (audioRecorder != null) audioRecorder.release(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // Required for ViewGroup—leave empty if no child views } }
Step 2: Create a View Manager
Register your custom view with React Native using a ViewManager:
import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.annotations.ReactMethod; public class AudioWebSocketViewManager extends SimpleViewManager<AudioWebSocketView> { public static final String REACT_CLASS = "AudioWebSocketView"; @Override public String getName() { return REACT_CLASS; } @Override protected AudioWebSocketView createViewInstance(ThemedReactContext reactContext) { return new AudioWebSocketView(reactContext); } // Expose control methods to JS @ReactMethod public void startRecording(AudioWebSocketView view) { view.startRecording(); } @ReactMethod public void stopRecording(AudioWebSocketView view) { view.stopRecording(); } @ReactMethod public void startPlaying(AudioWebSocketView view) { view.startPlaying(); } @ReactMethod public void stopPlaying(AudioWebSocketView view) { view.stopPlaying(); } }
Step 3: Register the View in a React Package
Create a package to add your ViewManager to React Native:
import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class AudioWebSocketPackage implements ReactPackage { @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { List<ViewManager> managers = new ArrayList<>(); managers.add(new AudioWebSocketViewManager()); return managers; } }
Add this package to your MainApplication.java:
@Override protected List<ReactPackage> getPackages() { List<ReactPackage> packages = new PackageList(this).getPackages(); packages.add(new AudioWebSocketPackage()); // Add your custom package return packages; }
Step 4: Integrate with React Navigation
Create a JS component for your custom view, then add it to your navigation stack:
import React, { useRef } from 'react'; import { requireNativeComponent, UIManager, findNodeHandle } from 'react-native'; import { Button, View } from 'react-native'; const AudioWebSocketView = requireNativeComponent('AudioWebSocketView'); export default function AudioScreen() { const viewRef = useRef(null); const triggerAction = (command) => { const viewId = findNodeHandle(viewRef.current); UIManager.dispatchViewManagerCommand( viewId, UIManager.AudioWebSocketView.Commands[command], [] ); }; return ( <View style={{ flex: 1 }}> <AudioWebSocketView ref={viewRef} style={{ flex: 1 }} /> <View style={{ flexDirection: 'row', padding: 16, gap: 8 }}> <Button title="Start Recording" onPress={() => triggerAction('startRecording')} /> <Button title="Stop Recording" onPress={() => triggerAction('stopRecording')} /> <Button title="Start Playing" onPress={() => triggerAction('startPlaying')} /> <Button title="Stop Playing" onPress={() => triggerAction('stopPlaying')} /> </View> </View> ); }
Add the screen to your React Navigation stack:
import { createStackNavigator } from '@react-navigation/stack'; import { NavigationContainer } from '@react-navigation/native'; import AudioScreen from './AudioScreen'; const Stack = createStackNavigator(); export default function App() { return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name="AudioStream" component={AudioScreen} /> {/* Add other screens here */} </Stack.Navigator> </NavigationContainer> ); }
3. Fixing the Playback Lag Issue
With the custom view approach, you eliminate two major performance bottlenecks:
- No more Base64 conversions: Raw byte[] data flows directly between the recorder, WebSocket, and player—no temporary objects to trigger GC.
- No JS Bridge overhead: All audio and WebSocket logic runs natively, so there's no back-and-forth with JS that slows down playback.
This should give you smooth audio playback even on low-memory 512MB virtual devices.
内容的提问来源于stack exchange,提问作者Jason Rich Darmawan

