This article belongs to “Jane Book — Liu Xiaozhuang” original, please note:

< Jane books – Liu Xiaozhuang > https://www.jianshu.com/p/54da18ed1a9e


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.

The event queue

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: Handles I/O events, draws events, gestures, receives other eventsisolateExternal events such as messages.
  • Microtask Queue: Can be self-directedisolateAdd events internally, priority ratio of eventsevent queueHigh.

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.

Future<Response> dataReqeust() async {
    String requestURL = 'https://jsonplaceholder.typicode.com/posts';
    Client client = Client();
    Future<Response> response = client.get(requestURL);
    return response;
}

Future<String> loadData() async {
    Response response = await dataReqeust();
    return response.body;
}
Copy the 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.

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }}Copy the 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.

loadData() {
    // datetime.now (), get the current time
    DateTime now = DateTime.now();
    print('request begin $now');
    Future.delayed(Duration(seconds: 1), (){
      now = DateTime.now();
      print('request response $now');
    });
}
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.

Future.delayed(Duration(seconds: 1), () {int age = 18;
  return age;
}).then((onValue){
  onValue++;
  print('age $onValue');
});
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 ISOLATE makes good use of multi-core cpus to process a large number of time-consuming tasks. The main communication between the ISOLATE threads is through ports, which are asynchronous. According to the Dart source code, the process of instantiating an ISOLATE includes instantiating the ISOLATE structure, allocating thread memory in the heap, and configuring ports.

The isolate actually looks similar to processes. When I asked ali architect about Zongxin, He said, “The overall model of isolate is more like process, while async and await are more like threads in my own understanding. If you compare the definition of ISOLATE to process, it is clear that isolate is a lot like process.

Code sample

The following is an example of an ISOLATE. In this example, a newly created ISOLATE binds a method to handle network requests and data parsing, and returns the processed data to the caller through port.

loadData() async {
    Create an ISOLATE with spawn and bind static methods
    ReceivePort receivePort =ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);
    
    // Obtain the listening port of the new ISOLATE
    SendPort sendPort = await receivePort.first;
    // Call the sendReceive custom method
    List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
    print('dataList $dataList');
}

// The binding method of isolate
static dataLoader(SendPort sendPort) async{
    // Create a listener port and pass sendPort to the outside world to call
    ReceivePort receivePort =ReceivePort();
    sendPort.send(receivePort.sendPort);
    
    // Listen for external calls
    await for (var msg in receivePort) {
      String requestURL =msg[0];
      SendPort callbackPort =msg[1];
    
      Client client = Client();
      Response response = await client.get(requestURL);
      List dataList = json.decode(response.body);
      // The callback returns the value to the callercallbackPort.send(dataList); }}// Create your own listening port and send messages to the new ISOLATE
Future sendReceive(SendPort sendPort, String url) {
    ReceivePort receivePort =ReceivePort();
    sendPort.send([url, receivePort.sendPort]);
    // The return value is received and returned to the caller
    return receivePort.first;
}
Copy the 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 transmission of port is essentially the process of isolating messages. The port is passed to the other ISOLates, and the other isolates get sendPort through port and 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.

There were four runners in Embedder, and the four runners were 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 ISOLATE 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 ISOLATE is managed by the Dart VM and has nothing to do with 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. However, it should be noted that due to the Embedder Runner mechanism, a blocked Platform does not result in a stalled page.

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 practice, code on the native side of the Flutter runs in Platform Runner, while code on the Flutter side runs in Root Isolate. If time-consuming code is executed in Platform, the main thread of the native Platform 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 Runner handlers, 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 are carried out by UI Runner, if time-consuming processing is carried out in UI Runner, the page display will be affected. Therefore, time-consuming operation should be handed over to other isolate for processing, 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 was relatively independent and could not submit render information to it except for Embedder.

IO Runner

Some time-consuming operations in GPU Runner 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.


Due to typesetting problems, the reading experience is not good, such as layout, picture display, code and many other problems. So go to Github and download the PDF compilation of the Flutter programming Guide. Put all the Flutter articles in this PDF, in three total, and there is a table of contents on the left for easy reading.

Download url: Flutter programming Guide PDF Please give a thumbs up to Flutter, thank you! 😁