Author: Idle fish technology – Hao An
We believe that after reading our previous articles, readers will have a better understanding of Platform Channel. However, due to limited space, the above section does not provide a detailed explanation of how Platform Channel works. How Platform Channel works, how messages are transferred from Flutter to Platform, how messages are encoded and decoded, on what thread does Platform Channel work, is it thread-safe, and can Platform Channel transfer large memory blocks? This paper tries to explain the above problems in detail with official examples.
1. Understand how Platform Channel works
Flutter defines three different types of channels
- BasicMessageChannel: Used to pass strings and semi-structured information.
- MethodChannel: Used for method invocation.
- EventChannel: Communication for Event streams.
The three channels are independent and serve different purposes, but they are very similar in design. Each Channel has three important member variables:
- Name: represents the name and unique identifier of a Channel.
- Messager: Type of BinaryMessenger, which stands for message messenger and is a tool for sending and receiving messages.
- Codec: The MessageCodec or MethodCodec type, which represents the codec for the message.
1.1. The Channel name
A Flutter application may have multiple channels. Each Channel must be created with a unique name. A name is used to distinguish one Channel from another. When a message is sent from the Flutter end to the Platform end, the message Handler corresponding to the channel will be found based on its channel name.
1.2. Message messenger :BinaryMessenger
Although the three channels have their own purposes, they communicate with the Flutter using the same tool: BinaryMessager.
BinaryMessenger is a tool for Platform to communicate with Flutter. The message format is binary data. When we initialize a Channel and register a Handler to handle the message with that Channel, we actually generate a corresponding BinaryMessageHandler, registered with the Channel name as the key, into BinaryMessenger. When a message is sent to BinaryMessenger by the Flutter end, BinaryMessenger will find the corresponding BinaryMessageHandler based on its input channel and hand it over for processing.
Binarymessenger is an interface on Android, and its specific implementation is FlutterNativeView. On iOS it is a protocol called FlutterBinaryMessenger, which the FlutterViewController follows.
Binarymessenger does not know that a Channel exists, and it only deals with BinaryMessageHandler. A Channel and a BinaryMessageHandler correspond one to one. The message received by a Channel from BinaryMessageHandler is in binary format and cannot be used directly. Therefore, a Channel decodes the binary message through the Message Codec into an recognized message and passes it to the Handler for processing.
When the Handler has processed the message, the result is returned via the callback function. The result is encoded as binary data through the codec and sent back to the Flutter end via BinaryMessenger.
1.3. Message Codec :Codec
The MessageCodec is primarily used to convert data in binary format into data that can be recognized by handlers. Flutter defines two types of Codec: MessageCodec and MethodCodec.
1.3.1. MessageCodec
MessageCodec is used for codec between binary format data and underlying data. The codec used by BasicMessageChannel is MessageCodec.
On Android, MessageCodec is an interface that defines two methods :encodeMessage receives a specific data type T and encodes it as binary ByteBuffer, while decodeMessage receives binary ByteBuffer, Decode it to a specific data type T. In iOS, its name is FlutterMessageCodec, which is a protocol that defines two methods: Encode receives a message of type ID and encodes it as NSData, while decode receives a message of type NSData and decodes it as ID data.
MessageCodec has several different implementations:
-
BinaryCodec
BinaryCodec is the simplest Codec because its return value type is the same as the input parameter type, both in binary format (ByteBuffer on Android, NSData on iOS). In fact, BinaryCodec does nothing in the codec process, just returns the binary data message as it is. BinaryCodec may not make sense to you, but it can be very useful in situations such as when passing blocks of memory without copying them in the codec phase.
-
StringCodec
StringCodec is used to encode and decode strings and binary data in utF-8 format.
-
JSONMessageCodec
JSONMessageCodec is used to encode and decode basic data and binary data. It supports basic data types as well as lists and dictionaries. It uses NSJSONSerialization as a serialization tool on iOS and its custom JSONUtil and StringCodec as serialization tools on Android.
-
StandardMessageCodec
StandardMessageCodec is the default codec for BasicMessageChannel, which supports basic data types, binary data, lists, dictionaries, and how it works is described in more detail below.
1.3.2. MethodCodec
MethodCodec is used to codec binary data with method calls and returned results. The codec used by MethodChannel and EventChannel is MethodCodec.
Unlike MessageCodec, MethodCodec is used to codec a MethodCall object. A MethodCall object represents a MethodCall from the Flutter side. MethodCall has two member variables: Method (String) denotes the name of the method to be called, and arguments (generic) denotes the method input to be called.
Because it handles method calls, MethodCodec handles more of the result of the call than MessageCodec. When the method call succeeds, encodeSuccessEnvelope is used to encode result as binary data, while when the method call fails, encodeErrorEnvelope is used to encode error code, message, detail as binary data.
MethodCodec has two implementations:
-
JSONMethodCodec
JSONMethodCodec relies on JSONMessageCodec, which first converts methodCalls to dictionaries {“method”:method,”args”:args} when encoding methodCalls. As it encodes the result of the call, it converts it to an array, with success as [result] and failure as [code,message,detail]. JSONMessageCodec is then used to convert dictionaries or arrays into binary data.
-
StandardMethodCodec
The default implementation of MethodCodec, StandardMethodCodec relies on StandardMessageCodec, which encodes method and args in sequence when it encodes methodcalls, Write to the binary data container. When encoding the result of the method call, if the call is successful, it writes the value 0 to the binary data container first (representing the success of the call), and then writes the StandardMessageCodec encoded result. When the call fails, data 1 is written to the container first (representing call failure), followed by StandardMessageCodec encoded code, message and detail.
1.4. Message Handler: Handler
Once we receive a message in binary format and use Codec to decode it into a message that Handler can handle, it’s time for Handler to come into play. A Flutter defines three types of handlers that correspond to the Channel type. When we register a Handler with a Channel, we are actually registering a corresponding BinaryMessageHandler with a BinaryMessager. When a message is sent to a BinaryMessageHandler, the Channel decodes the message via Codec and passes it to the Handler for processing.
1.4.1. MessageHandler
The onMessage method receives a message of type T and asynchronously returns a result of the same type. MessageHandler is basic and has few usage scenarios, but when used in conjunction with BinaryCodec, it can easily pass binary data messages.
1.4.2. MethodHandler
MethodHandler handles method calls. Its onMessage method receives a MethodCall message and calls the corresponding API based on the method variable. When the process is complete, the corresponding result is returned based on the success or failure of the method call.
1.4.3. StreamHandler
StreamHandler is slightly different from the previous two. It is used to communicate with event streams. The most common use is for Platform to send event messages to Flutter. When we implement a StreamHandler, we need to implement its onListen and onCancel methods. In the onListen method, there is an EventSink (which is an object on Android and a block on iOS). After holding EventSink, we can send event messages to the Flutter terminal through EventSink.
Actually, StreamHandler doesn’t work that complicated. When we register a StreamHandler, we actually register a corresponding BinaryMessageHandler into BinaryMessager. When the Flutter side starts listening for events, it sends a binary message to the Platform side. The Platform side decodes the message as MethodCall with MethodCodec. If the value of MethodCall’s method is “listen”, the StreamHandler’s onListen method is called. Pass an EventSink to the StreamHandler. When a message is sent to the Flutter end by EventSink, it is actually passed to the Flutter end by BinaryMessager’s Send method.
2. Understand the message codec process
In the example of getting device power in Writing Custom Platform-Specific Code with Platform Channels, the return value on Android is java.lang.Integer. The value returned by iOS is an NSNumber (NSNumber numberWithInt:). When the Flutter ends, the return value automatically “becomes” an INT in the DART language. So what happened?
Standard Platform Channels uses standard Messsage Codec to serialize and deserialize message and response. Message and Response can be Booleans, numbers, Strings, byte buffers,List, Maps, etc., and the resulting data is in binary format after serialization.
So in the previous example, a java.lang.Integer or NSNumber return value is first serialized into a binary format, which is then passed to the flutter side and deserialized into an INT in dart.
The default message codec for Flutter is StandardMessageCodec, which supports the following data types:
When message or response needs to be encoded as binary data, StandardMessageCodec’s writeValue method is called, which takes a parameter named value and, based on its type, Writes the corresponding type value to the binary data container (NSMutableData or ByteArrayOutputStream), converts the data to a binary representation, and writes to the binary data container.
When message or response needs to be decoded, StandardMessageCodec’s readValue method is used. After receiving data in binary format, the method first reads a byte to represent its type, and then converts the binary data into the corresponding data type according to its type.
In the example of obtaining the power of the device, suppose that the power of the device is 100. When this value is converted to binary data, the type value corresponding to int :3 is written to the binary data container first, and then four bytes are written from the power value 100. When the Flutter receives the binary data, the first byte value is read, based on which the data is of type INT. The next four bytes are read and converted to An INT of type DART.
Encoding for strings, lists, and dictionaries is a little more complicated. The length of binary data encoded using UTF-8 is variable. Therefore, after type is written, a size representing the length of binary data is written before data is written. For list and dictionary, write a size representing the number of elements in the list or dictionary after type, and then recursively call writeValue to write the elements in sequence.
3. Understand the messaging process
How do messages get from the Flutter side to the Platform side? Let’s use a call to MethodChannel as an example to understand the message delivery process.
3.1. Message passing: From Flutter to Platform
3.1.1. Dart layer
When we make a method call on the Flutter side using MethodChannel’s invokeMethod method, we start our message passing journey. The invokeMethod method wraps its input arguments message and arguments into a MethodCall object, encodes it into binary format data using MethodCodec, and emits the message through BinaryMessages. (Note that the class and method names mentioned here are implementations of the DART layer.)
The above process will eventually call the _sendPlatformMessage method of UI. Window, which is a native method that is actually very similar to Java’s JNI technology at the native layer. We send three parameters to the Native layer:
name
The String type represents the Channel namedata
, the ByteData type, which is the previously encapsulated binary datacallback
, Function type for the result callback
3.1.2. Native layer
After arriving at the native layer, the SendPlatformMessage method of window.cc accepts three parameters from the Dart layer and does some processing to them: The dart layer correction callback encapsulation for native PlatformMessageResponseDart type of response; The binary data of dart layer is converted to STD ::vector
data; Create a PlatformMessage object based on response,data, and Channel name, The PlatformMessage object is processed via the dart_state->window()->client()->HandlePlatformMessage method.
Dart_state ->window()-> Client () is a WindowClient, which is implemented as a RuntimeController. The RuntimeController passes messages to its delegate RuntimeDelegate.
The RuntimeDelegate is implemented as an Engine. When a Message is processed, the Engine determines whether the Message is to fetch resources (channel equals to “flutter/assets”). Otherwise call Engine: : Delegate OnEngineHandlePlatformMessage method.
Engine: : the realization of a Delegate for the Shell, its OnEngineHandlePlatformMessage message is received, to add a Task PlatformTaskRunner, The Task calls the HandlePlatformMessage method of PlatformView. It is worth noting that the code in Task is executed in the Platform Task Runner, while the previous code is executed in the UI Task Runner.
3.2. Message processing
PlatformView’s HandlePlatformMessage method has different implementations on different platforms, but the basic principles are the same.
3.2.1. PlatformViewAndroid
PlatformViewAndroid is a subclass of Platformview, which is implemented in Android. When PlatformViewAndroid receives PlatformMessage type of news, if you have any response message (type for PlatformMessageResponseDart), Pending_responses_ generates a response_id and stores response as value in the pending_responses_ dictionary. Next, we convert both channel and data to Java-recognized data and make a call to the Java layer via JNI passing response_id, channel, and data.
In the Java layer, the code called is the handlePlatformMessage of the FlutterNativeView (the concrete implementation of BinaryMessager), The method finds the corresponding BinaryMessageHandler by channel and passes the message to it for processing. The specific processing process has been analyzed in detail above and will not be repeated here.
After processing by BinaryMessageHandler, FlutterNativeView will invoke native methods through JNI and pass response_data and response_id to native layer.
Native layer, PlatformViewAndroid InvokePlatformMessageResponseCallback received respond_id and response_data. Its response_data first converted to binary results, and according to response_id, found in the panding_responses_ corresponding PlatformMessageResponseDart object, call the Complete method will binary results back.
3.2.2. PlatformViewIOS
PlatformViewIOS is a subclass of PlatformView and an implementation of PlatformView on iOS. When PlatformViewIOS receives a message, it sends it to PlatformMessageRouter for processing.
PlatformMessageRouter via a channel to find the corresponding PlatformMessage FlutterBinaryMessageHandler, and binary message processing, message processing is completed, Direct call PlatformMessage object in the Complete method PlatformMessageResponseDart object binary results back.
3.3. Result return: from Platform to Flutter
PlatformMessageResponseDart Complete method to the UI Task Runner has added a new Task, is the role of the Task of binary results from native binary data types into a binary data type response, Dart Dart’s callback is called to pass the response to the DART layer.
When the Dart layer receives the binary data, it decodes it using MethodCodec and returns it to the business layer. At this point, a method call from Flutter is complete.
4
4.1. What thread does Platform Channel code run on
As mentioned in the article “Understanding the Thread Model of the Flutter Engine”, the Flutter Engine does not create its own threads. The thread creation and management is provided by Enbedder. The Flutter Engine requires Embedder to provide four Task Runners, which are Platform Task Runner,UI Task Runner, GPU Task Runner and IO Task Runner.
In effect, code executed on the Platform side runs in Platform Task Runner, while code executed on the Flutter APP side runs in UI Task Runner. On Android and iOS, the Platform Task Runner runs on the main thread. Therefore, time-consuming operations should not be handled in the Handler on the Platform side.
4.2. Whether Platform Channel is thread safe
Platform Channel is not thread-safe, as mentioned in the official documentation. Multiple components of the Flutter Engine are non-thread-safe. Therefore, all interactions with the Flutter Engine (interface calls) must occur on the Platform Thread. Therefore, when transferring Platform message processing results back to Flutter, ensure that the callback function is executed in the Platform Thread (the main Thread of Android and iOS).
4.3. Whether the transfer of large memory data blocks is supported
Platform Channel actually supports the transfer of large memory blocks. When passing large memory blocks, you need to use the Base Messagechannel and BinaryCodec. In the entire data transfer process, the only possible data copy location is converted from native binary data to Dart binary data. If the binary data is larger than the threshold (the current threshold is 1000 bytes), the binary data is not copied and directly converted. Otherwise, the binary data is copied and then converted.
4.4. How to apply Platform Channel principles to development work
In fact, there are many application scenarios for Platform Channel. Here’s an example:
In normal business development, we need to use some local image resources, but the Flutter side cannot use the existing image resources on the Platform side. To use an image resource that exists on the Flutter terminal, copy the image resource to the Assert directory of the Flutter terminal. In fact, it is not difficult to get the Flutter side to use the resources on the Platform side.
We can do this using BasicMessageChannel. The Flutter end transfers the image resource name to the Platform end. The Native end uses the Platform end to receive the name, locate the image resource according to the name, and transfer the image resource in binary data format through BasicMessageChannel. Pass back to the Flutter end.
conclusion
Under the mixed development mode of Flutter and Native, there are many application scenarios of Platform Channel. Understanding the working principle of Platform Channel helps us to be handy in this aspect of development.
Finally, xianyu technology team is recruiting all kinds of talents, whether you are proficient in mobile terminal, front-end, background, machine learning, audio and video, automatic testing, etc., welcome to join us, with technology to improve your life!
Resume: [email protected]
reference
Flutter. IO/platform – ch…
Github.com/flutter/flu…
Github.com/flutter/eng…