📱 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 like Map, List, and Uint8List into a byte buffer.
  • BinaryCodec: Passes raw binary data (as ByteData) 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

  1. Minimize Data Copies:
      • Use BinaryCodec for > 1 MB transfers.
      • Prefer direct buffers on Android (if data is processed immediately).
  1. Monitor GC Impact:
      • Heap buffers in Java for large data slow GC. Use direct buffers to offload memory.
  1. Avoid Dangling Buffers:
      • Re-enable returnsDirectByteBufferFromDecoding only if data is processed synchronously.
  1. Use Streams for Continuous Data:
      • For real-time data (e.g., audio/video), combine BinaryCodec with StreamChannel 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! 🚀