ITADN

[cronet_http] JNI GlobalRef not released after upload request completes — CronetUrlRequest + CronetUploadDataStream leaked in Java heap

#1919Openkotdark 创建于 2026-05-16
type-bugpackage:cronet_http
K
kotdarkcommented
**Body:** ```markdown ## Environment | | | |---|---| | `cronet_http` | 1.8.0 | | Flutter | ≥ 3.24.0 | | Platform | Android (Samsung SM-A066B, API 34) | | Used via | `native_dio_adapter` 1.5.1 → `cronet_http` 1.8.0 | ## Summary When making HTTP requests with an upload body via `cronet_http` on Android, the underlying `CronetUrlRequest` Java object is **never released from the Java heap** after the request completes. A **JNI Global Reference** held by the Chromium Cronet C++ engine pins the object permanently. Each leaked request retains its `CronetUploadDataStream` (holding the raw upload bytes), resulting in ~175 KB of retained memory per request that accumulates indefinitely. ## Steps to Reproduce Use `cronet_http` (directly or via `native_dio_adapter`) to perform repeated file uploads in a single session: ```dart final client = CronetClient.defaultCronetEngine(); for (final imageBytes in images) { final request = http.Request('PUT', uploadUrl); request.bodyBytes = imageBytes; await client.send(request); } Monitor with: adb shell dumpsys meminfo <your.package.name> # Focus on: Dalvik Heap — HeapSize, HeapAlloc, HeapFree Observed Behavior Dalvik Alloc grows linearly at ~848 KB/min with no plateau over 250 uploads: Baseline: Alloc = 11,675 KB Util = 50% 250 uploads later: Alloc = 53,140 KB Util = 68% GC runs continuously (Free fixed at 24,576 KB = device heapmaxfree) but cannot reclaim the leaked objects. Root Cause — JNI Global Reference not released Heap dump analyzed with Android Studio Memory Profiler: 748 leaked instances (for ~250 uploads × ~3 requests each): ┌──────────────────────────────────────────────────────────┬───────────┬─────────────────┐ │ Class │ Instances │ Retained (each) │ ├──────────────────────────────────────────────────────────┼───────────┼─────────────────┤ │ CronetUrlRequest (org.chromium.net.impl) │ 748 │ ~175 KB │ ├──────────────────────────────────────────────────────────┼───────────┼─────────────────┤ │ UrlRequestCallbackProxy (io.flutter.plugins.cronet_http) │ 748 │ — │ ├──────────────────────────────────────────────────────────┼───────────┼─────────────────┤ │ CronetMetrics (org.chromium.net.impl) │ 748 │ — │ └──────────────────────────────────────────────────────────┴───────────┴─────────────────┘ Retention chain: [JNI Global Reference — Cronet C++ native engine] ← GC root, no Java parent └─ CronetUrlRequest (Shallow: 139 bytes, Retained: ~175 KB) └─ CronetUploadDataStream (Retained: ~169 KB — raw upload bytes) With "Show nearest GC root only" in Android Studio Memory Profiler, CronetUrlRequest appears at Depth=0 with no Java parent. This confirms the object is pinned by JNI NewGlobalRef() inside the Cronet C++ engine and DeleteGlobalRef() is never called. Expected Behavior When a request completes (success, failure, or cancel) and the response body is fully consumed, Cronet should release its internal JNI global reference so the CronetUrlRequest object can be garbage collected. Hypothesis For upload requests, one of the following may prevent the native cleanup path: 1. The response body is not fully drained after the upload body is sent — leaving the request in a non-terminal state in Cronet's internal state machine 2. onSucceeded() / onFailed() / onCanceled() are invoked on the Dart/Java side but the corresponding native DeleteGlobalRef() is not called Related Issues - dart-lang/http #1308 — Cronet resource exhaustion ("Too many Broadcast Receivers > 1000") during mass downloads — potentially same underlying lifecycle issue - cfug/dio #1359 — Big file upload causes memory growth with Dio (closed without fix) Workaround Avoid CronetClient for upload requests on Android. Fall back to IOClient (dart:io) which does not exhibit this behavior. Notes - iOS equivalent (cupertino_http / NSURLSessionUploadTask) not yet tested - Heap dump file (.hprof) available upon request - Leak is 100% reproducible and proportional to upload request count <!-- Failed to upload "heap_leak_jhat.hprof.zip" -->
0 条评论