Flutter hoạt động như nào? — Phần 1

               

The Vinh Luong

Phần lớn chúng ta khi phát triển ứng dụng bằng Flutter sẽ chỉ sử dụng đến Widget nên rất ít khi quan tâm đến những khái niệm như Element, BuildContext, RenderObject và Binding. Đã bao giờ bạn thắc mắc Flutter hoạt động như thế nào chưa?

Nếu bạn tò mò thì đây chính là bài viết dành cho bạn.

Thiết bị hiển thị hoạt động như nào

Khi chúng ta quan sát bề mặt hiển thị của một thiết bị (smartphone, TV,…) thì thưc chất chúng ta đang quan sát một tập các pixel được tập hợp với nhau nhằm tạo ra một ảnh phẳng 2 chiều.

Việc dựng ảnh để hiển thị trên màn hình được chịu trách bởi thiết bị hiển thị (TBHT). Các TBHT thường có một thông số gọi là tần số làm tươi (refresh rate) thể hiện số khung hình được làm tươi trên màn hình (thường là 60 khung hình/s).

TBHT nhận data để hiển thị từ GPU (bộ xử lý đồ họa). Số lần mà GPU có thể generate ra một hình ảnh (frame buffer) để gửi cho TBHT được gọi là frame rate, nó được đo lường bằng đơn vị fps (số khung hình/giây).

Lý do mình đề cập đến cách ảnh 2D được render là bởi mục tiêu chính của Flutter là giúp chúng ta dựng giao diện bằng các ảnh phẳng 2D có thể tương tác được (chạm, vuốt). Không chỉ có vậy, Flutter cũng rất quan tâm đến việc TBHT sẽ có thể refresh nhanh và đúng thời điểm để đảm bảo app chạy mượt mà không giật lag.

Interface giữa code và thiết bị vật lý

Phía dưới là hình ảnh kiến trúc high-level của Flutter

Flutter high level architecture

Khi chúng ta dùng Dart để viết một app Flutter thì tức là chúng ta đang dùng đến tầng Framework của kiến trúc (màu xanh lá).

Tầng Framework sẽ giao tiếp với tầng Engine (xanh dương) thông qua một lớp abstract gọi là Window. Lớp abstract này sẽ cung cấp một tập các API để giao tiếp, một cách không trực tiếp, với thiết bị.

Cũng thông qua lớp abstract này mà tầng Engine sẽ có hành động notify tầng Framework khi có các sự kiện sau:

  • Cấu hình thiết bị thi thay đổi (orientation, setting, trạng thái chạy của ứng dụng…)
  • Có tương tác của user với màn hình (gesture).
  • Platform channel truyền dữ liệu lên tầng Framework.
  • Và phần lớn là khi tầng Engine sẵn sàng để render frame mới.

Tầng Framework thực chất được điều khiển bởi tầng Engine

Thật khó tin đúng không? Nhưng đó là sự thật.

Ngoại trừ một số ngoại lệ (liệt kê bên dưới) thì không có trường hợp nào mà code ở tầng Framework được execute mà không phải là do được trigger bởi hành động render frame ở tầng Engine.

Các ngoại lệ đó là:

  • Gesture: cử chỉ tay của user trên mặt kính của thiết bị.
  • Platform message: Message được emit từ platform của thiết bị, ví dụ như GPS
  • Device message: Các message liên quan đến các state của thiết bị, như orientation, app bị đưa vào background, setting của thiết bị…
  • Các response của http hay Future.

Tầng Framework sẽ ko apply sự thay đổi UI nào nếu đó không phải là request đến từ việc render frame của Engine. Thực tế vẫn có cách tạo sự thay đổi trên UI mà ko cần phải là điều được request từ Engine, nhưng cách đó không được khuyến nghị.

Một số bạn sẽ thắc mắc là giả sử nếu sự thay đổi UI đến từ gesture hoặc từ kết quả của một timer task nào đó thì rốt cuộc điều gì đã diễn ra?

Ảnh dưới mô tả cách mọi thứ diễn ra under the hood như nào:

Giải thích một cách ngắn gọn thì:

  • Một số external event (gesture, http response, Future response) có thể launch một số task dẫn đến việc cần cập nhật việc render. Một message sẽ được gửi đến Engine với mục đích thông báo việc này (Schedule Frame).
  • Khi Engine đã sẵn sàng cho việc update rendering thì nó sẽ emit một request là Begin Frame.
  • Request Begin Frame sẽ bị intercept bởi Framework để chạy một số task chủ yếu liên quan đến Tickers như Animations.
  • Các task được chạy có thể sẽ lại emit request render frame khác. Ví dụ như khi có một animation đang chạy thì tầng Framework sẽ phải gửi một message schedule frame cho Engine để nó render frame tiếp theo của animation.
  • Sau đấy thì Flutter Engine sẽ emit message Draw Frame.
  • Message này lại bị intercept bởi tầng Framework nhằm chạy những task liên quan đến việc cập nhật lại cấu trúc và size của layout.
  • Sau đấy thì các task liên quan đến việc cập nhật paint của layout sẽ được thực hiện.
  • Nếu có thứ cần được vẽ lên screen thì tầng Framework sẽ gửi một Scene mới cần được render cho Engine để nó thực hiện việc cập nhật lại screen.
  • Sau đấy thì tầng Framework sẽ thực hiện các task cần được chạy sau sự kiện render (thông qua PostFrame callback) và các task sau đó mà không liên quan đến việc render.
  • Và mọi việc cứ thế lặp lại từ đầu.

RenderView và RenderObject

Khi làm UI thì từ những dòng code mà chúng ta viết sẽ cho output là một tập các pixel được compose. Hay nói cách khác là từ các Widget mà chúng ta sử dụng sẽ cho output là các “visual part” (một phần hiển thị trên màn hình tương ưng với Widget đó).

Các visual part được render trên screen này tương ứng với các object được gọi là RenderObject, được sử dụng để:

  • Định nghĩa một khu vực trên screen để chứa nội dung được render. Để xác định khu vực thì nó sẽ sử dụng đến các thông tin như kích thước, vị trí, hình dạng hình học.
  • Xác định các khu vực trên screen có thể bắt các sự kiện gesture.

Một set các RenderObject sẽ hình thành một tree, được gọi là RenderTree. Ở gốc của tree ấy sẽ luôn là một RenderView.

RenderView đại diện cho toàn bộ bề mặt của screen mà RenderTree sẽ dùng để hiển thị UI. Có thể coi RenderView là một phiên bản đặc biệt của RenderObject.

Để dễ hình dung các bạn có thể xem ảnh dưới

Về mối quan hệ giữa RenderObject và Widget sẽ được đề cập sau.

Binding

Khi một app Flutter được start thì hệ thống sẽ invoke hàm main(), kéo theo việc hàm runApp(Widget app) được gọi theo.

Trong quá trình gọi đến hàm runApp() thì tầng Framework sẽ khởi tạo các interface giữa tầng Framework và tầng Engine. Các interface này được gọi là binding.

Giới thiệu về binding

Các binding được sinh ra như là một chất keo kết dính giữa tầng Engine và Framework. Hai tầng này chỉ có thể giao tiếp với nhau thông qua các binding này.

Mỗi một binding sẽ chịu trách nhiệm cho một số task, action và event nhất định, được nhóm lại dựa trên một tiêu chí cụ thể.

Tại thời điểm của bài viết thì đang có 8 loại binding.

Chúng ta sẽ chỉ đi sâu hơn về các loại binding chính sau:

  • SchedulerBinding
  • GestureBinding
  • RendererBinding
  • WidgetBinding

Các binding còn lại là:

  • ServicesBinding: Chịu trách nhiệm xử lý các message được gửi bởi platform channel.
  • PaintingBinding: Chịu trách nhiệm cho việc cache ảnh.
  • SemanticsBinding: Chịu trách nhiệm cho tất cả các task liên quan đến Semantics.
  • TestWidgetsFlutterBinding: được sử dụng bởi lib widget test.

Ngoài ra còn có cả WidgetsFlutterBinding, tuy nhiên nó giống một binding initializer hơn là một binding.

Hình bên dưới mô tả tương tác giữa một số các binding:

Chúng ta hãy cùng tìm hiểu kỹ hơn về các binding chính nhé

SchedulerBinding

Binding này có 2 nhiệm vụ chính:

  • Nhiệm vụ đầu tiên là để nói với tầng Engine rằng: “Trong lần rảnh tiếp theo của mày thì nhớ đánh thức tao dậy để tao xem mày có phải render cái gì không nhé, hoặc nếu tao cần mày gọi lại sau”.
  • Nhiệm vụ thứ hai là lắng nghe và xử lý những lời “đánh thức” ấy.

Vậy khi nào thì SchedulerBinding sẽ yêu cầu được “đánh thức”?

  • Là khi mà Ticker cần được tick. Ví dụ như là khi có một Animation và chúng ta start nó. Lúc này animation khi chạy sẽ được điều khiển bởi một Ticker, là cái mà cứ sau một khoảng thời gian (tick) thì nó sẽ được gọi để chạy một callback. Để chạy callback này thì chúng ta sẽ phải bảo Flutter Engine đánh thức tại lần refresh màn hình tiếp theo (BeginFrame). Việc đánh thức này sẽ invoke callback ticker để nó thực hiện task của mình. Sau khi hoàn thành task, nếu ticker cần được gọi tiếp thì nó sẽ bảo SchedulerBinding schedule frame tiếp theo.
  • Khi có sự thay đổi ở layout: Ví dụ như là khi có một event nào đó xảy ra thì chúng ta cần thay đổi UI (thay đổi màu, scroll, thêm hoặc xóa cài gì đấy trên màn hình). Trong trường hợp này thì tầng Framework sẽ gọi SchedulerBinding để nhờ nó schedule frame khác với tầng Engine.

GestureBinding

Binding này sẽ lắng nghe sự tương tác của gesture (cử chỉ tay) với tầng Engine. Cụ thể thì nó sẽ chịu trách nhiệm trong việc tiếp nhận data liên quan đến gesture và xác định phần nào của screen sẽ bị tác động bởi gesture đấy. Sau đó nó sẽ gửi thông báo đến các thành phần bị ảnh hưởng.

RendererBinding

Binding này kết nối tầng Engine với Render Tree. Nó có 2 trách nhiệm sau:

  • Đầu tiên là lắng nghe các sự kiện được emit bởi tầng Engine để thông báo về các thay đổi liên quan đến setting của thiết bị mà có thể tác động đến UI hoặc semantic.
  • Thứ hai là cung cấp những sự thay đổi về hiển thị cần thực hiện cho tầng Engine.

Để cho những sự thay đổi này có thể được render lên screen thì RendererBinding sẽ phải chịu trách nhiệm điều khiển PipelineOwner và khởi tạo RenderView.

PipelineOwner có thể coi như là một “nhạc trưởng” biết RenderObject nào cần làm gì liên quan đến layout và điều phối các hành động đó của các RenderObject.

WIdgetsBinding

Binding này cũng lắng nghe sự thay đổi về setting của device được tạo bởi người dùng mà ảnh hưởng tới ngôn ngữ (locale) và semantics. Ngoài ra thì nó cũng là thành phần kết nối Widget và Flutter Engine. Nó có 2 trách nhiệm sau:

  • Đầu tiên là điều khiển tiến trình liên quan đến việc xử lý khi có sự kiện cấu trúc của Widget bị thay đổi.
  • Thứ hai là trigger việc render của tầng Engine.

Việc xử lý khi cấu trúc của Widget bị thay đổi được thực hiện thông qua BuildOwnerBuildOwner sẽ theo dõi xem WIdget nào cần rebuild và đảm nhiệm các task có tầm ảnh hưởng đến toàn bộ cấu trúc của Widget.

Kết

Trong phần 1 này thì mình đã giới thiệu sơ lược cơ chế hoạt động bên trong của Flutter. Trong phần sau chúng ta sẽ tìm hiểu kỹ hơn về Widget, Element, RenderObject và mối quan hệ giữa chúng nhé.

Happy coding!

Nguồn tham khảo:

  1. https://medium.com/saugo360/flutters-rendering-engine-a-tutorial-part-1-e9eff68b825d
  2. https://api.flutter.dev/flutter/widgets/BuildOwner-class.html
  3. https://www.didierboelens.com/2019/09/flutter-internals/
  4. https://www.youtube.com/watch?v=UUfXWzp0-DU
  5. https://api.flutter.dev/flutter/rendering/PipelineOwner-class.html