Flutter应用批量上传MP4至Firebase Cloud Storage时内存溢出崩溃求助
Flutter Firebase Storage批量上传MP4文件导致OutOfMemoryError的解决方案
问题背景
在Android设备的Flutter应用中,批量上传外部存储中的MP4文件至Firebase Cloud Storage时,上传15-19个文件后应用因OutOfMemoryError崩溃。测试环境为Android Pixel 3模拟器,使用54个约50MB的20秒MP4文件,连接Firebase Storage模拟器本地测试。同步、异步上传逻辑均触发该问题,实际项目使用队列结构(示例代码简化为Completer)。
复现代码
import 'dart:async'; import 'dart:io'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:path_provider/path_provider.dart'; import 'firebase_options.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); try { await FirebaseStorage.instance.useStorageEmulator('10.0.2.2', 9199); } catch (e) { print(e); } runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Firebase Upload Test'), ), body: Center( child: ElevatedButton( onPressed: () => uploadFromExternalDirectory(), child: const Text('Upload Assets'), ), ), ), ); } void uploadFromExternalDirectory() async { try { final directory = await getExternalStorageDirectory(); if (directory == null) { print('Error: Could not get external storage directory'); return; } Directory videos = directory; final storage = FirebaseStorage.instance; // Iterate through files in the directory (synchronously) for (var entity in videos.listSync(recursive: false)) { print('Processing $entity'); if (entity is File) { String fileName = entity.path; try { UploadTask task = storage.ref('test/$fileName').putFile(entity); // Use a Completer to create a Future that completes when the upload is done final Completer<void> completer = Completer<void>(); final StreamSubscription<TaskSnapshot> streamSubscription = task.snapshotEvents.listen((TaskSnapshot snapshot) { double progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; print('Upload progress for $fileName: $progress%'); }, onError: (error) { completer .completeError(error); // Complete the future with an error }, onDone: () { completer.complete(); // Complete the future successfully }); // Await the Completer's future to wait for the upload to finish await completer.future; // Cancel the stream subscription after the task is complete await streamSubscription.cancel(); print('Upload of $fileName complete!'); } on FirebaseException catch (e) { print('Error uploading $fileName: $e'); } } } } catch (e) { // Handle any potential errors that might occur print('An error occurred: $e'); } } }
崩溃日志
I/pload_issue_app(24622): Alloc concurrent copying GC freed 273(182KB) AllocSpace objects, 0(0B) LOS objects, 0% free, 191MB/192MB, paused 28us,137us total 9.132ms I/pload_issue_app(24622): WaitForGcToComplete blocked Alloc on Alloc for 9.821ms I/pload_issue_app(24622): Starting a blocking GC Alloc I/pload_issue_app(24622): WaitForGcToComplete blocked Alloc on Alloc for 9.805ms I/pload_issue_app(24622): Starting a blocking GC Alloc I/pload_issue_app(24622): Waiting for a blocking GC Alloc I/pload_issue_app(24622): Clamp target GC heap from 215MB to 192MB W/pload_issue_app(24622): Throwing OutOfMemoryError "Failed to allocate a 59 byte allocation with 88776 free bytes and 86KB until OOM, target footprint 201326592, growth limit 201326592; giving up on allocation because <1% of heap free after GC." (VmSize 19423944 kB) I/pload_issue_app(24622): WaitForGcToComplete blocked Background on Alloc for 19.661ms I/pload_issue_app(24622): Waiting for a blocking GC Alloc I/pload_issue_app(24622): Forcing collection of SoftReferences for 16B allocation I/pload_issue_app(24622): Waiting for a blocking GC Alloc E/AndroidRuntime(24622): FATAL EXCEPTION: main E/AndroidRuntime(24622): Process: com.example.upload_issue_app, PID: 24622 E/AndroidRuntime(24622): java.lang.OutOfMemoryError: Failed to allocate a 59 byte allocation with 88776 free bytes and 86KB until OOM, target footprint 201326592, growth limit 201326592; giving up on allocation because <1% of heap free after GC. E/AndroidRuntime(24622): at dalvik.system.VMRuntime.newNonMovableArray(Native Method) E/AndroidRuntime(24622): at java.nio.DirectByteBuffer$MemoryRef.<init>(DirectByteBuffer.java:71) E/AndroidRuntime(24622): at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:265) E/AndroidRuntime(24622): at io.flutter.plugin.common.StandardMessageCodec.encodeMessage(StandardMessageCodec.java:77) E/AndroidRuntime(24622): at io.flutter.plugin.common.BasicMessageChannel$IncomingMessageHandler$1.reply(BasicMessageChannel.java:266) E/AndroidRuntime(24622): at io.flutter.plugins.firebase.storage.GeneratedAndroidFirebaseStorage$FirebaseStorageHostApi$14.success(GeneratedAndroidFirebaseStorage.java:1312) E/AndroidRuntime(24622): at io.flutter.plugins.firebase.storage.GeneratedAndroidFirebaseStorage$FirebaseStorageHostApi$14.success(GeneratedAndroidFirebaseStorage.java:1309) E/AndroidRuntime(24622): at io.flutter.plugins.firebase.storage.FlutterFirebaseStoragePlugin.referencePutFile(FlutterFirebaseStoragePlugin.java:509) E/AndroidRuntime(24622): at io.flutter.plugins.firebase.storage.GeneratedAndroidFirebaseStorage$FirebaseStorageHostApi$-CC.lambda$setup$13(GeneratedAndroidFirebaseStorage.java:1321) E/AndroidRuntime(24622): at io.flutter.plugins.firebase.storage.GeneratedAndroidFirebaseStorage$FirebaseStorageHostApi$$ExternalSyntheticLambda13.onMessage(Unknown Source:2) E/AndroidRuntime(24622): at io.flutter.plugin.common.BasicMessageChannel$IncomingMessageHandler.onMessage(BasicMessageChannel.java:261) E/AndroidRuntime(24622): at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295) E/AndroidRuntime(24622): at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$io-flutter-embedding-engine-dart-DartMessenger(DartMessenger.java:322) E/AndroidRuntime(24622): at io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0.run(Unknown Source:12) E/AndroidRuntime(24622): at android.os.Handler.handleCallback(Handler.java:958) E/AndroidRuntime(24622): at android.os.Handler.dispatchMessage(Handler.java:99) E/AndroidRuntime(24622): at android.os.Looper.loopOnce(Looper.java:205) E/AndroidRuntime(24622): at android.os.Looper.loop(Looper.java:294) E/AndroidRuntime(24622): at android.app.ActivityThread.main(ActivityThread.java:8177) E/AndroidRuntime(24622): at java.lang.reflect.Method.invoke(Native Method) E/AndroidRuntime(24622): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) E/AndroidRuntime(24622): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) I/pload_issue_app(24622): WaitForGcToComplete blocked Alloc on Background for 33.559ms I/pload_issue_app(24622): Starting a blocking GC Alloc I/pload_issue_app(24622): WaitForGcToComplete blocked Alloc on Background for 19.223ms I/pload_issue_app(24622): Starting a blocking GC Alloc I/pload_issue_app(24622): Clamp target GC heap from 215MB to 192MB I/pload_issue_app(24622): Alloc concurrent copying GC freed 568(58KB) AllocSpace objects, 0(0B) LOS objects, 0% free, 191MB/192MB, paused 21us,11us total 10.624ms I/pload_issue_app(24622): WaitForGcToComplete blocked Background on Alloc for 12.056ms I/Process (24622): Sending signal. PID: 24622 SIG: 9 Lost connection to device. Exited.
已尝试的方案
- 同步/异步上传逻辑,均触发OOM
- 改用
putData方法,内存占用暴涨,崩溃更快;Flutter版无putStream方法 - 通过Flutter和Android内存分析工具发现:Flutter RSS持续增长,Android侧Java和Native内存周期性上升
解决方案建议
1. 优化上传任务的资源释放
- 上传完成后主动释放File对象引用,帮助GC回收内存:
// 每个文件上传完成后 entity = null; // 手动触发GC(可选,加速内存回收) System.runFinalization(); - 确保错误路径下也取消
StreamSubscription,避免资源泄漏:StreamSubscription<TaskSnapshot>? streamSubscription; try { UploadTask task = storage.ref('test/$fileName').putFile(entity); streamSubscription = task.snapshotEvents.listen(...); await completer.future; await streamSubscription.cancel(); } catch (e) { await streamSubscription?.cancel(); print('Error uploading $fileName: $e'); }
2. 限制并发上传数量
即使串行上传,底层Native侧资源可能未及时释放,通过Semaphore控制并发数(如最多2个):
// 引入semaphore依赖或自行实现 final semaphore = Semaphore(2); for (var entity in videos.listSync(recursive: false)) { if (entity is File) { await semaphore.acquire(); try { // 上传逻辑 } finally { semaphore.release(); entity = null; } } }
3. 通过Method Channel调用Android原生Firebase Storage API
Flutter插件可能存在内存泄漏,直接调用原生putFile方法,原生侧资源管理更高效:
- 原生侧Kotlin代码:
class FirebaseUploadHandler(private val context: Context) { private val storage = FirebaseStorage.getInstance() fun uploadFile(filePath: String, storagePath: String, result: MethodChannel.Result) { val file = File(filePath) val ref = storage.reference.child(storagePath) val uploadTask = ref.putFile(Uri.fromFile(file)) uploadTask.addOnCompleteListener { task -> if (task.isSuccessful) { result.success("Upload completed") } else { result.error("UPLOAD_FAILED", task.exception?.message, null) } } } } - Flutter侧调用代码:
const platform = MethodChannel('com.example.firebase_upload'); Future<void> uploadFileNative(String filePath, String storagePath) async { try { await platform.invokeMethod('uploadFile', { 'filePath': filePath, 'storagePath': storagePath, }); } on PlatformException catch (e) { print("Native upload error: ${e.message}"); } }
4. 调整Android应用内存配置
在android/app/build.gradle中增加堆内存,缓解内存压力:
android { defaultConfig { multiDexEnabled true javaMaxHeapSize "512m" } }
5. 更新Firebase Storage插件版本
使用最新版firebase_storage,官方可能已修复内存泄漏问题:
dependencies: firebase_storage: ^latest_version
内容的提问来源于stack exchange,提问作者Dana Lambert
相关产品推荐
相关产品推荐

