Introduction to * * * *

Since the official release of Flutter1.0 at the end of 2018, there has been an explosive growth in China. Most companies, including Xianyu, Douyin and Toutiao, have been connected to Flutter one after another, and have carried out cross-platform development through Flutter.

While using Flutter, some of its properties need to be studied and understood. The essence of Flutter cannot be understood by simply calling the API.

This article explains the multithreading of Flutter in detail. Because the author is doing iOS development, and compares the multithreading of Flutter with the iOS GCD, to help you understand the multithreading of Flutter.

The event queue

Flutter is handled by single-thread tasks by default. If no new thread is started, the task is handled by default in the main thread.

Much like iOS apps, there are event loops and message queues in Dart threads, but in Dart threads are called ISOLATE. Once the application is started, the main function is executed and the main ISOLATE is run.

Each ISOLATE contains an event loop and two event queues, the Event Loop, and the Event Queue and MicroTask queue. The event and MicroTask queues are similar to iOS source0 and Source1.

  • Event Queue: Processes external events such as I/O events, drawing events, gesture events, and receiving other ISOLATE messages.

  • Microtask Queue: You can add events to the ISOLATE. Events have a higher priority than event queues.

The two queues also have priorities. When the ISOLATE starts to execute, microTask events are processed first, and events in the Event queue are processed only when there are no events in the MicroTask queue. The events in the Event queue are executed repeatedly in this order. However, it should be noted that when the microTask event is executed, the event execution of the event queue will be blocked, which will lead to the delay of event response such as rendering and gesture response. To ensure rendering and gesture response, place time-consuming operations on the Event queue as much as possible.

Async and await

There are three keywords in an asynchronous call, async, await, and Future, where async and await need to be used together. Asynchronous operations can be performed in Dart with async and await. Async means to start an asynchronous operation and can return a Future result. If no value is returned, a Future with a null value is returned by default.

In essence, async and await are syntactic candy of Dart for asynchronous operations, which can reduce nested calls of asynchronous calls and return a Future after async modification, which can be called in the form of chain calls. This syntax is derived from the ES7 standard for JS, and Dart’s design is identical to JS’s.

The following encapsulates an asynchronous operation of a network request and returns a Future of Response type to the outside world. The outside world can call the request with await and get the returned data. As you can see from the code, even if a string is returned directly, Dart wraps it and makes it a Future.

1Future<Response> dataReqeust() async { 2 String requestURL = 'https://jsonplaceholder.typicode.com/posts'; 3 Client client = Client(); 4 Future<Response> response = client.get(requestURL); 5 return response; 6} 7 8Future<String> loadData() async { 9 Response response = await dataReqeust(); 10 return response.body; 11}Copy the code

< Swipe right to see the full code >

In the code example, execution to the loadData method is synchronized inside the method for execution, and execution to async inside is stopped when execution to await and the outside code continues. After an await is returned, execution continues from the await position. Therefore, await operations will not affect the execution of the following code.

Here is an example of code that starts an asynchronous operation with async, waits for the request or other operation to be executed with await, and receives the return value. When data changes, the setState method is called and the data source is updated. Flutter updates the corresponding Widget node view.

1class _SampleAppPageState extends State<SampleAppPage> { 2 List widgets = []; 3 4 @override 5 void initState() { 6 super.initState(); 7 loadData(); 8 } 910 loadData() async {11 String dataURL = "https://jsonplaceholder.typicode.com/posts"; 12 http.Response response = await http.get(dataURL); 13 setState(() {14 widgets = json.decode(response.body); 15}); 17 16}}Copy the code

< Swipe right to see the full code >

**Future

**

A Future is an encapsulation of a delayed operation, and an asynchronous task can be encapsulated as a Future object. Once you get a Future object, the easiest way is to decorate it with await and wait for the return result to continue. As mentioned above in async and await, await modifier needs to be used together with async.

In Dart, time-dependent operations are future-dependent, such as delayed operations, asynchronous operations, and so on. Here is a very simple delayed operation, implemented with Future’s Delayed method.

1loadData() {2 // datetime.now (), get current datetime.now (); 4 print('request begin $now'); 5 Future.delayed(Duration(seconds: 1), (){6 now = DateTime.now(); 7 print('request response $now'); 8}); 9}Copy the code

Dart also supports chained calls to futures by appending one or more THEN methods, a very useful feature. For example, after a delayed operation completes, the then method is called and a parameter can be passed to the THEN. The invocation is a chain invocation, which means there are many layers of processing. This is somewhat similar to the RAC framework for iOS, where chained calls are made for signal processing.

Ps. IOS development exchange technology group: welcome to join, no matter you are big or small white welcome to enter, share BAT, Ali interview questions, interview experience, discuss technology, we exchange learning and growth together

1Future.delayed(Duration(seconds: 1), (){2 int age = 18; 3 return age; 4}).then((onValue){5 onValue++; 6 print('age $onValue'); 7});Copy the code

Coroutines * * * *

If you want to understand the principle of async and await, you should first understand the concept of coroutines. Async and await are essentially a syntactic sugar of coroutines. A coroutine, also known as a coroutine, is a unit smaller than a thread. In terms of unit size, it can basically be understood as process -> thread -> coroutine.

Task scheduling

Before understanding coroutines, first understand the concept of concurrency and parallelism, and refers to the system to manage the switch of multiple IO, and to the CPU to deal with. Parallelism refers to multi-core cpus performing multiple tasks at the same time.

Concurrent implementation is accomplished by non-blocking operations + event notification, also known as “interrupts”. There are two operations. One is that the CPU performs an OPERATION on the I/O and sends an interrupt to tell the I/O that the operation is complete. The other is when the IO initiates an interrupt to tell the CPU that it is ready to do something.

Threads also rely on interrupts in nature for scheduling, and there is another type of thread called “blocking interrupts”, where the thread blocks while performing AN IO operation and waits for the execution to complete before continuing. However, thread consumption is very high and is not suitable for handling large number of concurrent operations, which can be done through single-thread concurrency. With the advent of multi-core cpus, single threads can no longer take advantage of multi-core cpus, so the concept of thread pools was introduced to manage a large number of threads.

coroutines

During program execution, there are two ways to leave the current calling location: to continue calling another function or to return to leave the current function. However, when a return is executed, the state of the current function’s local variables and parameters in the call stack are destroyed.

Coroutines are divided into wireless coroutines and wired coroutines. The wireless coroutine will put the current variable in the heap when it leaves the current call location, and will continue to fetch variables from the heap when it returns to the current location. Therefore, variables are allocated directly to the heap when the current function is executed, and async and await are one of the wireless coroutines. Wired coroutines keep variables on the stack and continue to take calls off the stack when they return to the point where the pointer points.

Principle of async and await

Take async and await as an example. When the coroutine is executed, executing to async means entering a coroutine and synchronously executing the code block of async. An async block of code is essentially a function and has its own context. When an await is performed, it means that a task needs to wait, and the CPU schedules the execution of other IO, i.e., subsequent code or other coroutine code. After a period of time the CPU will rotate to see if a coroutine has finished processing and if it can continue execution, it will continue execution along the position where the pointer pointed to the last time it left, i.e. the position of the await flag.

Since no new thread is started and only I/O interrupts are performed to change CPU scheduling, asynchronous operations such as network requests can be performed with async and await. However, if a large number of time-consuming synchronous operations are performed, new threads should be created using Isolate to execute them.

If you compare coroutines with iOS dispatch_async, you can see that the two are quite similar. From the structural definition, the coroutine needs to store variables related to the code block of the current await. Dispatch_async can also store temporary variables through blocks.

I was wondering, why didn’t Apple introduce coroutines? OC thread is implemented based on Runloop. Dart also has event loop in nature. Both of them have their own event queue, but the number and classification of queues are different.

I feel that when await is performed, save the current context and mark the current location as pending task with a pointer to the current location and put the pending task in the queue of the current Isolate. The task is interrogated at each event loop, and if processing is required, the context is restored for processing.

Promise

I want to mention the Promise syntax in JS. In iOS, there are a lot of if judgments and other nested calls, and promises can change from horizontal nested calls to vertical chained calls. Introducing promises into OC makes the code look cleaner and more intuitive.

isolate

The Dart platform implements threads. Different from common threads, the ISOLATE has independent memory. The ISOLATE consists of threads and independent memory. Since memory is not shared between the ISOLATE threads, there is no problem of resource grabbing between the ISOLATE threads, so there is no need for locking.

The Isolute makes good use of multi-core cpus for processing a large number of time-consuming tasks. Communication between isolute threads takes place primarily through ports, where the port message delivery process is asynchronous. As you can see from the Dart source code, the process of instantiating an Isolute includes instantiating the Isolute structure, allocating thread memory in the heap, and configuring ports.

Isolute actually looks similar to process. When I asked ali architect about Zunxin, Zunxin also said that “the overall model of ISOLATE is more like process in my own understanding, while async and await are more like threads”. If you compare the definition of Isolute with that of a process, you will see that isolute is indeed a lot like a process.

Code sample

Here is an example of an Isolute that creates a new isolute and binds a method to handle network requests and data parsing, and returns the processed data to the caller via port.

1loadData() async {2 // Create an Isolute through spawn and bind static method 3 ReceivePort ReceivePort =ReceivePort(); 4 await Isolate.spawn(dataLoader, receivePort.sendPort); Port 7 SendPort SendPort = await ReceivePort.first; 8 / / call sendReceive custom methods 9 List dataList = await sendReceive (sendPort, 'https://jsonplaceholder.typicode.com/posts'); 10 print('dataList $dataList'); Static dataLoader(SendPort SendPort) async{15 // Create listener port, 16 ReceivePort ReceivePort =ReceivePort(); 17 sendPort.send(receivePort.sendPort); 20 await for (var MSG in receivePort) {21 String requestURL = MSG [0]; 22 SendPort callbackPort =msg[1]; 2324 Client client = Client(); 25 Response response = await client.get(requestURL); 26 List dataList = json.decode(response.body); 27 // The callback returns the value to the caller 28 callbackport.send (dataList); 29} 30}3132// create a listener port, And send a message to the new isolute 33Future sendReceive(SendPort SendPort, String URL) {34 ReceivePort ReceivePort =ReceivePort(); 35 sendPort.send([url, receivePort.sendPort]); 37 Return receiveport.first; 37 return receiveport.first; 38}Copy the code

< Swipe right to see the full code >

The ISOLATE is not the same as threads in iOS. The isolate threads are more low-level. When an ISOLATE is created, its memory is independent of each other and cannot be accessed. However, the ISOLATE provides a port-based message mechanism. The sendPort and ReceivePort are established to transfer messages to each other, which is called message transmission in Dart.

As you can see from the above example, the process of isolute message passing is essentially port passing. The port is passed to the other isolute, and the other isolute passes the port to sendPort to send messages to the caller for mutual messaging.

**Embedder

**

As its name suggests, Embedder is an Embedder layer that embeds the Flutter onto various platforms. Embedder covers native platform plug-ins, thread management, event loops, and more.

Embeder has four runners, as follows. Each of the Flutter engines corresponds to a UI Runner, GPU Runner, and IO Runner, but all engines share a Platform Runner.

Runner and Isolute are not the same thing, they are independent of each other. Take the iOS platform for example, the Runner’s implementation is CFRunloop, which continuously processes tasks in the way of an event loop. And Runner not only handles the tasks of Engine, but also the tasks of the Native platform brought about by the Native Plugin. The Isolute is managed by the Dart VM, independent of native platform threads.

Platform Runner

Platform Runner and iOS Main threads are very similar. In Flutter, all tasks except time-consuming operations should be placed in Platform. Many of the APIS in Flutter are not thread-safe, and placing them in other threads may cause bugs.

However, time-consuming operations such as I/O should be performed in other threads. Otherwise, Platform execution will be affected, or the watchdog will kill the operation. It’s important to note, however, that because of the Embeder Runner mechanism, a blocked Platform does not cause pages to stall.

Not only the Code of the Flutter Engine is executed in the Platform, but also the tasks of the Native Plugin are assigned to the Platform. In fact, code on the native side runs in Platform Runner, while code on the Flutter side runs in Root Isolute. If time-consuming code is executed in Platform, the native Platform main thread will be blocked.

UI Runner

UI Runner is responsible for executing the Root Isolate code for the Flutter Engine, in addition to handling tasks from the Native Plugin. Root Isolate binds many function methods to handle its own events. When the application starts, the Flutter Engine binds Root to UI Renner’s handler, enabling Root Isolate to submit render frames.

When the Root Isolate submits a render frame to Engine, Engine waits for the next Vsync. When the next Vsync arrives, the Root Isolate layouts Widgets and generates a description of the displayed information for Engine to process.

Since layout of widgets and generation of Layer Tree is done by UI Renner, if time-consuming processing is performed in UI Renner, the page display will be affected. Therefore, time-consuming processing should be handed over to other Isolute, such as events from Native Plugin.

GPU Runner

GPU Runner is not directly responsible for rendering operations, it is responsible for GPU-related management and scheduling. When the Layer Tree information arrives, GPU Runner submits it to the specified rendering platform, which is Skia configured, and different platforms may have different implementations.

GPU Runner is relatively independent, and no thread other than the Embeder can submit render information to it.

IO Runner

Some time-consuming operations in GPU Renner are processed in IO Runner, such as image reading, decompression, rendering and other operations. However, only GPU Runner can submit rendering information to GPU. In order to ensure that IO Runner also has this ability, IO Runner will reference the CONTEXT of GPU Runner so that it has the ability to submit rendering information to GPU.