FlutterEngine執行緒管理與DartIsolate機制

閒魚技術發表於2018-06-29

閒魚技術-福居
本文結合Flutter Engine官方文件討論了Flutter Engine內的執行緒管理模式以及Dart Isolate機制。

Flutter 是什麼?

Flutter簡介

Flutter是Google主導開發的高質量高效能移動跨平臺UI開發套件。使用Flutter你可以使用Dart語言高效快速開發高質量的跨平臺App,同時Flutter還可以可以與現存的Native程式碼相容。目前在世界範圍內被眾多開發者和組織使用,而且它是開源免費的!

Flutter優勢與前景

目前Flutter拿來比較最多的是Reactive Native,實際上Flutter跟RN有本質的區別。Flutter UI渲染是自己實現,這跟RN JS Bridge的形式有區別。這也是Flutter效能的一個突破點。使用Flutter用開發效率高,執行效率高,UI靈活性擴充套件性高等特點。相對於JS Bridge擴充套件型的跨平臺實現,Flutter有著更加廣闊的想象空間。

更加詳細的介紹可以瀏覽此連結:Flutter IO

Flutter 執行緒管理簡述

Flutter Engine自己不建立管理執行緒。Flutter Engine執行緒的建立和管理是由embedder負責的。

注意:Embeder是指將引擎移植的平臺的中間層程式碼。

Flutter Engine要求Embeder提供四個Task Runner。儘管Flutter Engine不在乎Runner具體跑在哪個執行緒,但是它需要執行緒配置在整一個生命週期裡面保持穩定。也就是說一個Runner最好始終保持在同一執行緒執行。這四個主要的Task Runner包括:

Flutter Engine執行架構

Platform Task Runner

Flutter Engine的主Task Runner,執行Platform Task Runner的執行緒可以理解為是主執行緒。類似於Android Main Thread或者iOS的Main Thread。但是我們要注意Platform Task Runner和iOS之類的主執行緒還是有區別的。

對於Flutter Engine來說Platform Runner所在的執行緒跟其它執行緒並沒有實質上的區別,只不過我們人為賦予它特定的含義便於理解區分。實際上我們可以同時啟動多個Engine例項,每個Engine對應一個Platform Runner,每個Runner跑在各自的執行緒裡。這也是Fuchsia(Google正在開發的操作引擎)裡Content Handler的工作原理。一般來說,一個Flutter應用啟動的時候會建立一個Engine例項,Engine建立的時候會建立一個執行緒供Platform Runner使用。

跟Flutter Engine的所有互動(介面呼叫)必須發生在Platform Thread,試圖在其它執行緒中呼叫Flutter Engine會導致無法預期的異常。這跟iOS UI相關的操作都必須在主執行緒進行相類似。需要注意的是在Flutter Engine中有很多模組都是非執行緒安全的。一旦引擎正常啟動執行起來,所有引擎API呼叫都將在Platform Thread裡發生。

Platform Runner所在的Thread不僅僅處理與Engine互動,它還處理來自平臺的訊息。這樣的處理比較方便的,因為幾乎所有引擎的呼叫都只有在Platform Thread進行才能是安全的,Native Plugins不必要做額外的執行緒操作就可以保證操作能夠在Platform Thread進行。如果Plugin自己啟動了額外的執行緒,那麼它需要負責將返回結果派發回Platform Thread以便Dart能夠安全地處理。規則很簡單,對於Flutter Engine的介面呼叫都需保證在Platform Thread進行。

需要注意的是,阻塞Platform Thread不會直接導致Flutter應用的卡頓(跟iOS android主執行緒不同)。儘管如此,平臺對Platform Thread還是有強制執行限制。所以建議複雜計算邏輯操作不要放在Platform Thread而是放在其它執行緒(不包括我們現在討論的這個四個執行緒)。其他執行緒處理完畢後將結果轉發回Platform Thread。長時間卡住Platform Thread應用有可能會被系統Watchdot強行殺死。

UI Task Runner Thread(Dart Runner)

UI Task Runner被Flutter Engine用於執行Dart root isolate程式碼(isolate我們後面會講到,姑且先簡單理解為Dart VM裡面的執行緒)。Root isolate比較特殊,它繫結了不少Flutter需要的函式方法。Root isolate執行應用的main code。引擎啟動的時候為其增加了必要的繫結,使其具備排程提交渲染幀的能力。對於每一幀,引擎要做的事情有:

  • Root isolate通知Flutter Engine有幀需要渲染。
  • Flutter Engine通知平臺,需要在下一個vsync的時候得到通知。
  • 平臺等待下一個vsync
  • 對建立的物件和Widgets進行Layout並生成一個Layer Tree,這個Tree馬上被提交給Flutter Engine。當前階段沒有進行任何光柵化,這個步驟僅是生成了對需要繪製內容的描述。
  • 建立或者更新Tree,這個Tree包含了用於螢幕上顯示Widgets的語義資訊。這個東西主要用於平臺相關的輔助Accessibility元素的配置和渲染。

除了渲染相關邏輯之外Root Isolate還是處理來自Native Plugins的訊息響應,Timers,Microtasks和非同步IO。
我們看到Root Isolate負責建立管理的Layer Tree最終決定什麼內容要繪製到螢幕上。因此這個執行緒的過載會直接導致卡頓掉幀。
如果確實有無法避免的繁重計算,建議將其放到獨立的Isolate去執行,比如使用compute關鍵字或者放到非Root Isolate,這樣可以避免應用UI卡頓。但是需要注意的是非Root Isolate缺少Flutter引擎需要的一些函式繫結,你無法在這個Isolate直接與Flutter Engine互動。所以只在需要大量計算的時候採用獨立Isolate。

GPU Task Runner

GPU Task Runner被用於執行裝置GPU的相關呼叫。UI Task Runner建立的Layer Tree資訊是平臺不相關,也就是說Layer Tree提供了繪製所需要的資訊,具體如何實現繪製取決於具體平臺和方式,可以是OpenGL,Vulkan,軟體繪製或者其他Skia配置的繪圖實現。GPU Task Runner中的模組負責將Layer Tree提供的資訊轉化為實際的GPU指令。GPU Task Runner同時也負責配置管理每一幀繪製所需要的GPU資源,這包括平臺Framebuffer的建立,Surface生命週期管理,保證Texture和Buffers在繪製的時候是可用的。

基於Layer Tree的處理時長和GPU幀顯示到螢幕的耗時,GPU Task Runner可能會延遲下一幀在UI Task Runner的排程。一般來說UI Runner和GPU Runner跑在不同的執行緒。存在這種可能,UI Runner在已經準備好了下一幀的情況下,GPU Runner卻還正在向GPU提交上一幀。這種延遲排程機制確保不讓UI Runner分配過多的任務給GPU Runner。

前面我們提到GPU Runner可以導致UI Runner的幀排程的延遲,GPU Runner的過載會導致Flutter應用的卡頓。一般來說使用者沒有機會向GPU Runner直接提交任務,因為平臺和Dart程式碼都無法跑進GPU Runner。但是Embeder還是可以向GPU Runner提交任務的。因此建議為每一個Engine例項都新建一個專用的GPU Runner執行緒。

IO Task Runner

前面討論的幾個Runner對於執行任務的型別都有比較強的限制。Platform Runner過載可能導致系統WatchDog強殺,UI和GPU Runner過載則可能導致Flutter應用的卡頓。但是GPU執行緒有一些必要操作是比較耗時間的,比如IO,而這些操作正是IO Runner需要處理的。

IO Runner的主要功能是從圖片儲存(比如磁碟)中讀取壓縮的圖片格式,將圖片資料進行處理為GPU Runner的渲染做好準備。在Texture的準備過程中,IO Runner首先要讀取壓縮的圖片二進位制資料(比如PNG,JPEG),將其解壓轉換成GPU能夠處理的格式然後將資料上傳到GPU。這些複雜操作如果跑在GPU執行緒的話會導致Flutter應用UI卡頓。但是隻有GPU Runner能夠訪問GPU,所以IO Runner模組在引擎啟動的時候配置了一個特殊的Context,這個Context跟GPU Runner使用的Context在同一個ShareGroup。事實上圖片資料的讀取和解壓是可以放到一個執行緒池裡面去做的,但是這個Context的訪問只能在特定執行緒才能保證安全。這也是為什麼需要有一個專門的Runner來處理IO任務的原因。獲取諸如ui.Image這樣的資源只有通過async call,當這個呼叫發生的時候Flutter Framework告訴IO Runner進行剛剛提到的那些圖片非同步操作。這樣GPU Runner可以使用IO Runner準備好的圖片資料而不用進行額外的操作。

使用者操作,無論是Dart Code還是Native Plugins都是沒有辦法直接訪問IO Runner。儘管Embeder可以將一些一般複雜任務排程到IO Runner,這不會直接導致Flutter應用卡頓,但是可能會導致圖片和其它一些資源載入的延遲間接影響效能。所以建議為IO Runner建立一個專用的執行緒。

各個平臺目前預設Runner執行緒實現

前面我們提到Engine Runner的執行緒可以按照實際情況進行配置,各個平臺目前有自己的實現策略。

iOS和Android

Mobile平臺上面每一個Engine例項啟動的時候會為UI,GPU,IO Runner各自建立一個新的執行緒。所有Engine例項共享同一個Platform Runner和執行緒。

Fuchsia

每一個Engine例項都為UI,GPU,IO,Platform Runner建立各自新的執行緒。

自定義配置執行緒可行方案

我們注意到Mobile平臺上面,Platform Runner和Thread是共享的。
引擎原始碼如下:

    Shell::Shell(fxl::CommandLine command_line)
    : command_line_(std::move(command_line)) {
  FXL_DCHECK(!g_shell);

  gpu_thread_.reset(new fml::Thread("gpu_thread"));
  ui_thread_.reset(new fml::Thread("ui_thread"));
  io_thread_.reset(new fml::Thread("io_thread"));

  // Since we are not using fml::Thread, we need to initialize the message loop
  // manually.
  fml::MessageLoop::EnsureInitializedForCurrentThread();

  blink::Threads threads(fml::MessageLoop::GetCurrent().GetTaskRunner(),
                         gpu_thread_->GetTaskRunner(),
                         ui_thread_->GetTaskRunner(),
                         io_thread_->GetTaskRunner());
  blink::Threads::Set(threads);

  blink::Threads::Gpu()->PostTask([this]() { InitGpuThread(); });
  blink::Threads::UI()->PostTask([this]() { InitUIThread(); });

  blink::SetRegisterNativeServiceProtocolExtensionHook(
      PlatformViewServiceProtocol::RegisterHook);
}

這裡我們可以進行改動,讓引擎每個例項初始化獨自的執行緒:


    gpu_thread_.reset(new fml::Thread("gpu_thread"));
    ui_thread_.reset(new fml::Thread("ui_thread"));
    io_thread_.reset(new fml::Thread("io_thread"));

    platform_thread_.reset(new fml::Thread("platform_thread"));

    blink::Threads threads(platform_thread_->GetTaskRunner(),
                            gpu_thread_->GetTaskRunner(),
                            ui_thread_->GetTaskRunner(),
                            io_thread_->GetTaskRunner());

理論上你可以配置任意執行緒供其使用,不過最好遵循最佳實踐。

具體程式碼導讀

iOS Android平臺可以參考Flutter Engine原始碼:

flutter/common/threads.cc
flutter/shell/common/shell.cc

Dart isolate機制

An isolated Dart execution context. 這是文件對isolate的定義。

isolate定義

isolate是Dart對actor併發模式的實現。執行中的Dart程式由一個或多個actor組成,這些actor也就是Dart概念裡面的isolate。isolate是有自己的記憶體和單執行緒控制的執行實體。isolate本身的意思是“隔離”,因為isolate之間的記憶體在邏輯上是隔離的。isolate中的程式碼是按順序執行的,任何Dart程式的併發都是執行多個isolate的結果。因為Dart沒有共享記憶體的併發,沒有競爭的可能性所以不需要鎖,也就不用擔心死鎖的問題。

isolate之間的通訊

由於isolate之間沒有共享記憶體,所以他們之間的通訊唯一方式只能是通過Port進行,而且Dart中的訊息傳遞總是非同步的。

isolate與普通執行緒的區別

我們可以看到isolate神似Thread,但實際上兩者有本質的區別。作業系統內內的執行緒之間是可以有共享記憶體的而isolate沒有,這是最為關鍵的區別。

isolate實現簡述

我們可以閱讀Dart原始碼裡面的isolate.cc檔案看看isolate的具體實現。
我們可以看到在isolate建立的時候有以下幾個主要步驟:

  • 初始化isolate資料結構
  • 初始化堆記憶體(Heap)
  • 進入新建立的isolate,使用跟isolate一對一的執行緒執行isolate
  • 配置Port
  • 配置訊息處理機制(Message Handler)
  • 配置Debugger,如果有必要的話
  • 將isolate註冊到全域性監控器(Monitor)

我們看看isolate開始執行的主要程式碼


Thread* Isolate::ScheduleThread(bool is_mutator, bool bypass_safepoint) {
  // Schedule the thread into the isolate by associating
  // a `Thread` structure with it (this is done while we are holding
  // the thread registry lock).
  Thread* thread = NULL;
  OSThread* os_thread = OSThread::Current();
  if (os_thread != NULL) {

    MonitorLocker ml(threads_lock(), false);

    // Check to make sure we don`t already have a mutator thread.
    if (is_mutator && scheduled_mutator_thread_ != NULL) {
      return NULL;
    }

   
    while (!bypass_safepoint && safepoint_handler()->SafepointInProgress()) {
      ml.Wait();
    }

    // Now get a free Thread structure.
    thread = thread_registry()->GetFreeThreadLocked(this, is_mutator);
    ASSERT(thread != NULL);

    // Set up other values and set the TLS value.
    thread->isolate_ = this;
    ASSERT(heap() != NULL);
    thread->heap_ = heap();
    thread->set_os_thread(os_thread);
    ASSERT(thread->execution_state() == Thread::kThreadInNative);
    thread->set_execution_state(Thread::kThreadInVM);
    thread->set_safepoint_state(0);
    thread->set_vm_tag(VMTag::kVMTagId);
    ASSERT(thread->no_safepoint_scope_depth() == 0);
    os_thread->set_thread(thread);
    if (is_mutator) {
      scheduled_mutator_thread_ = thread;
      if (this != Dart::vm_isolate()) {
        scheduled_mutator_thread_->set_top(heap()->new_space()->top());
        scheduled_mutator_thread_->set_end(heap()->new_space()->end());
      }
    }
    Thread::SetCurrent(thread);
    os_thread->EnableThreadInterrupts();

    thread->ResetHighWatermark();
  }
  return thread;
}

我們可以看到Dart本身抽象了isolate和thread,實際上底層還是使用作業系統的提供的OSThread。

(Isolate) -> (Dart Thread) -> (OS Thread)

Flutter Engine Runners與Dart Isolate

有朋友看到這裡可能會問既然Flutter Engine有自己的Runner,那為何還要Dart的Isolate呢,他們之間又是什麼關係呢?

那我們還要從Runner具體的實現說起,Runner是一個抽象概念,我們可以往Runner裡面提交任務,任務被Runner放到它所在的執行緒去執行,這跟iOS GCD的執行佇列很像。我們檢視iOS Runner的實現實際上裡面是一個loop,這個loop就是CFRunloop,在iOS平臺上Runner具體實現就是CFRunloop。被提交的任務被放到CFRunloop去執行。

Dart的Isolate是Dart虛擬機器自己管理的,Flutter Engine無法直接訪問。Root Isolate通過Dart的C++呼叫能力把UI渲染相關的任務提交到UI Runner執行這樣就可以跟Flutter Engine相關模組進行互動,Flutter UI相關的任務也被提交到UI Runner也可以相應的給Isolate一些事件通知,UI Runner同時也處理來自App方面Native Plugin的任務。

所以簡單來說Dart isolate跟Flutter Runner是相互獨立的,他們通過任務排程機制相互協作。

踩坑血淚史

理解Flutter Engine的原理以及Dart虛擬機器的非同步實現,讓我們避免採坑,更加靈活高效地進行開發。
在專案應用過程我們踩過不少坑在採坑和填坑的過程中不斷學習。這裡我簡單聊其中一個具體的案例:當時我們需要把Native載入好圖片資料註冊到Engine裡面去以便生成Texture渲染,使用完資源我們需要將其移除,看起來非常清晰的邏輯竟然造成了野指標問題。後來排查到註冊的時候在一個子執行緒進行而移除卻在Platform執行緒進行,在弄清楚執行緒結構以後問題也就迎刃而解。

結語

本文我們主要討論了Flutter層面的執行緒配置管理以及Dart本身isolate的實現。在深入瞭解Flutter執行緒機制以後,我們在開發過程當中更加得心應手,同時也啟發我們如何去設計類似應用內的執行緒結構。
目前我們在探索單個Flutter Engine以元件的方式啟動,也就是多個Flutter Engine例項同時存在通過Port來進行通訊的可能方案。感興趣或者有相關經驗的朋友歡迎交流,還請不吝賜教。

簡歷投遞:guicai.gxy@alibaba-inc.com

參考資料


相關文章