天天看點

Flutter Engine線程管理與Dart Isolate機制Dart isolate機制結語參考資料

閑魚技術-福居

本文結合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線程管理與Dart Isolate機制Dart isolate機制結語參考資料

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。

Flutter Engine線程管理與Dart Isolate機制Dart isolate機制結語參考資料

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來進行通信的可能方案。感興趣或者有相關經驗的朋友歡迎交流,還請不吝賜教。

履歷投遞:[email protected]

參考資料

繼續閱讀