📱 Mobile
How StandardMessageCodec and BinaryCodec encode and decode in Flutter
originalUrl
date
Jun 1, 2025
slug
flutter-codec-deep-dive
author
status
Public
tags
#dart
#flutter
summary
In Flutter, communication between Dart and native platform code (Android/iOS) relies on platform channels, which serialize and deserialize data using MessageCodecs. Two commonly used codecs are
StandardMessageCodec
and BinaryCodec
.type
Post
thumbnail
category
📱 Mobile
updatedAt
Jun 8, 2025 04:22 PM
In Flutter, communication between Dart and native platform code (Android/iOS) relies on platform channels, which serialize and deserialize data using MessageCodecs. Two commonly used codecs are
StandardMessageCodec
and BinaryCodec
. Understanding how they encode/decode data—and how Flutter uses direct vs. indirect buffers—is critical for optimizing performance, especially when transferring large amounts of binary data.Platform Messaging in Flutter
Flutter uses platform channels to send messages between Dart (UI thread) and native code (platform thread). A
MessageCodec
defines how data is serialized (encodeMessage
) and deserialized (decodeMessage
).StandardMessageCodec
: Encodes complex structures likeMap
,List
, andUint8List
into a byte buffer.
BinaryCodec
: Passes raw binary data (asByteData
) with zero encoding overhead.
🔧 StandardMessageCodec: Structured Encoding & Decoding
Encoding in Dart
StandardMessageCodec
encodes binary data (e.g., Uint8List
) by writing:1. A type identifier (
_valueUint8List
: 0x0a
in Flutter’s code).2. The size of the data using
writeSize
(variable-length integer encoding).3. The raw bytes of the data.
void writeValue(WriteBuffer buffer, Object? value) { // ... buffer.putUint8(_valueUint8List); // write type writeSize(buffer, value.length); // write size buffer.putUint8List(value); // write value }
This creates a structured byte sequence:
[Type][Size (varint)][Raw Data...]
Decoding on Android (Java)
On the native side, Java reads the structure and allocates a heap buffer (not direct):
@NonNull protected static final byte[] readBytes(@NonNull ByteBuffer buffer) { final int length = readSize(buffer); final byte[] bytes = new byte[length]; // Heap memory buffer.get(bytes); return bytes; }
Memory Implications:
- Every decode creates a new Java heap array, which:
- Adds GC pressure for large data.
- Requires two copies:
1. From Dart to the platform’s temporary buffer.
2. From the temporary buffer to Java’s heap array.
✅ Use Case: Ideal for structured data (e.g., JSON-like maps/lists) where overhead is acceptable.
❌ Avoid: Frequent/repeated transfers of large binaries (e.g., video frames).
BinaryCodec: Zero-Overhead Binary Transfer
Encoding/Decoding in Dart
BinaryCodec
is a passthrough—simply returns the input ByteData
:class BinaryCodec implements MessageCodec<ByteData> { const BinaryCodec(); @override ByteData? decodeMessage(ByteData? message) => message; @override ByteData? encodeMessage(ByteData? message) => message; }
Dart’s
ByteData
wraps a native memory buffer (managed by the Dart VM). For large data, this is direct memory (inside the Dart heap but not a Java direct buffer).Decoding on Android (Java)
Java’s
BinaryCodec
decodes with a default indirect buffer (false
for returnsDirectByteBufferFromDecoding
):@Override public ByteBuffer decodeMessage(@Nullable ByteBuffer message) { if (message == null) { return message; } else if (returnsDirectByteBufferFromDecoding) { return message; } else { ByteBuffer result = ByteBuffer.allocate(message.capacity()); result.put(message); result.rewind(); return result; } }
Trade-Offs:
returnsDirectByteBufferFromDecoding = false
(default):- Safer: Data copied to Java heap. Valid beyond
decodeMessage
.- Slower: Extra copy required.
returnsDirectByteBufferFromDecoding = true
:- Faster: Direct buffer points to Dart memory.
- Risky: Buffer becomes invalid if Dart memory is GC’d.
✅ Use Case: Streaming large binaries (e.g., files, camera frames) with pinned direct buffers.
⚠️ Caution: Requires strict lifetime management to avoid dangling pointers.
Direct vs. Indirect Buffers in Java
Feature | ByteBuffer.allocate (Indirect) | ByteBuffer.allocateDirect (Direct) |
Memory Location | Java heap | Native memory |
GC Overhead | High (large arrays scanned) | Low (metadata only) |
Allocation Cost | Low | High |
Zero-Copy I/O | ❌ Requires heap→direct copy | ✅ Direct OS integration |
Buffer Lifetime | Long-lived | Scope-bound (pinned) |
Heap Buffers
- Pros: Fast allocation, works with standard serialization.
- Cons: GC pressure for > 1 MB data.
Direct Buffers
- Pros:
- Eliminates copying in file/socket I/O.
- Prevents heap bloat (e.g., large buffers outside XMx).
- Cons: Slow allocation/deallocation.
Performance Considerations
When to Use Which Codec?
Scenario | Suggested Codec | Buffer Type |
Structured data (e.g., JSON) | StandardMessageCodec | Indirect (default) |
Large binaries (e.g., files) | BinaryCodec | Direct (if pinned) |
Small/frequent transfers | StandardMessageCodec | Indirect |
Best Practices
- Minimize Data Copies:
- Use
BinaryCodec
for > 1 MB transfers. - Prefer direct buffers on Android (if data is processed immediately).
- Monitor GC Impact:
- Heap buffers in Java for large data slow GC. Use direct buffers to offload memory.
- Avoid Dangling Buffers:
- Re-enable
returnsDirectByteBufferFromDecoding
only if data is processed synchronously.
- Use Streams for Continuous Data:
- For real-time data (e.g., audio/video), combine
BinaryCodec
withStreamChannel
to utilize direct buffers.
Conclusion
Understanding
StandardMessageCodec
and BinaryCodec
is key to optimizing Flutter’s platform communication. StandardMessageCodec
adds structure and safety but incurs overhead, while BinaryCodec
enables zero-copy transfers at the cost of manual memory management. By leveraging direct buffers for large binaries and indirect buffers for transient data, developers can balance performance and stability.For custom plugins, choose your codec carefully based on data size, transfer frequency, and lifetime requirements! 🚀