ข้ามไปยังเนื้อหา

How Flutter Works Internally

ทุกโมดูลก่อนหน้านี้ในคอร์สสอนให้คุณใช้Flutter: สร้าง widget, จัดการ state, navigate, จัด layout หน้าจอ, ship build ไม่มีข้อไหนต้องรู้ว่า setState ทำอะไรจริง ๆ ข้างใต้, “relayout boundary” คืออะไร, ทำไม PSO ของ Impeller ถึง “precompiled”, หรือ gen_snapshot สร้างอะไรจริง ๆ ช่องว่างนั้นแหละคือจุดที่คำอธิบาย Flutter ที่ฟังดูมั่นใจ (ไม่ว่ามนุษย์หรือ AI เขียน) ผิดพลาดในแบบที่ flutter analyze เพียงอย่างเดียวจับไม่ได้ — ข้อกล่าวอ้างที่ฟังดูสมเหตุสมผลอย่าง “UI thread กับ raster thread รวมกันแล้ว” หรือ “isolate แต่ละตัวมี heap แยกกันโดยสิ้นเชิง” อ่านดูน่าเชื่อถือจนกว่าคุณจะตรวจ source จริงหรือรันคำสั่งจริง

โมดูลนี้ตอบคำถามภายในห้าข้อ แต่ละข้อพิสูจน์กับ source จริงของ Flutter 3.47.5 / Dart 3.13.4 ที่ติดตั้งไว้ (revision 6a19cca564) หรือคำสั่งจริงที่รันบน Mac เครื่องนี้ ไม่เคยยืนยันจากความจำ:

  1. เกิดอะไรขึ้นจริง ๆ ระหว่าง widget rebuild กับ pixel เปลี่ยน? (The Three Trees, From the Source — Widget.canUpdate, ตาราง 4 กรณีจริงของ Element.updateChild, RenderObjectWidget ที่เขียนเองพิสูจน์ว่า createRenderObject/updateRenderObject/performLayout/paint คือ hook สี่ตัวที่กระตุ้นแยกกันได้ และการ reparent ด้วย GlobalKey พิสูจน์ด้วย identical())
  2. อะไรที่เปลี่ยน “widget เรียก setState” ให้เป็น “เฟรมหนึ่งรันจริง” และตามลำดับไหน? (Build Scheduling and Frames — SchedulerPhase enum จริง, doc comment 8 ขั้นของ WidgetsBinding.drawFrame เอง และ test จริงที่พิสูจน์ว่า setState แบบซิงโครนัสสองครั้งรวมกันเป็น build เดียว)
  3. สายการเรียก markNeedsLayout หยุดไต่ tree ตรงไหนแน่ ๆ? (Layout and Paint Internals — กฎ relayout-boundary ที่แน่นอนอ้างจาก source พิสูจน์ด้วยคู่ RenderBox ที่เขียนเองและ debugDumpRenderTree จริงที่แสดง annotation ของ boundary ปรากฏและหายไป)
  4. Impeller แทนที่อะไรจริง ๆ และ “UI+raster รวมกันแล้ว” จริงไหม? (Impeller and the GPU — README ของ engine เองและ source ของ Entity/Contents/RenderPass และการแก้ไขจริง: การรวม thread ตั้งแต่ 3.29+ ที่มีเอกสารรองรับคือ UI+platform ไม่ใช่ UI+raster)
  5. flutter build รันอะไรจริง ๆ และโมเดลหน่วยความจำของ Dart isolate หน้าตาเป็นแบบไหนจริง ๆ? (The Dart VM and Isolates — output แบบ verbose จริงของ flutter build apk/flutter build web, ELF libapp.so จริงที่แกะออกมาตรวจด้วย file(1) และหลักฐานสด ๆ ที่ grep เจอว่า branch ของ kDebugMode ถูกลบออกจาก release build)

ทุกข้อกล่าวอ้างระดับ source ด้านล่างอ้าง permalink จริงเข้าไปใน github.com/flutter/flutter ที่ revision ที่คอร์สนี้ตรึงไว้ 6a19cca564 (Flutter 3.47.5) หรือ github.com/dart-lang/sdk ที่ tag ที่ตรงกัน 3.13.4 — revision เดียวกับที่ flutter --version รายงานบน Mac เครื่องนี้ ดังนั้นทุกเลขบรรทัดที่อ้างจะตรงกับที่ลิงก์ไว้ แม้ SDK นี้จะอัปเกรดในอนาคต ทุกข้อกล่าวอ้างระดับ runtime คือการรัน flutter test จริงกับ probe ที่ share ของคอร์สนี้ (output ตัดทอนจริง ไม่เคยแต่งขึ้น) และทุกข้อกล่าวอ้างเกี่ยวกับ build pipeline (ข้อความ --help ของ CLI ของ Impeller and the GPU, output ของ flutter build apk/flutter build web -v ของ The Dart VM and Isolates) คือคำสั่งจริงที่รันบน Mac เครื่องนี้ ผ่าน probe หรือแอป scratch จาก flutter create แบบใช้แล้วทิ้งสำหรับสิ่งที่ probe ที่ share รันไม่ได้อย่างปลอดภัย (การ build APK/web เต็มรูปแบบ) จุดที่สภาพแวดล้อมนี้ไม่มี device และข้อกล่าวอ้างยืนยันในเครื่องจริง ๆ ไม่ได้ (screenshot ของ DevTools raster-stats, frame trace จริงของ Impeller ใน production) แต่ละบทพูดตรง ๆ แทนที่จะเดา

flowchart TD
  t["The Three Trees, From the Source\n(Widget/Element/RenderObject)"] --> f["Build Scheduling and Frames\n(setState -> a real frame)"]
  t --> l["Layout and Paint Internals\n(the relayout-boundary rule)"]
  f --> l
  l --> i["Impeller and the GPU\n(what actually runs on the raster thread)"]
  f --> v["The Dart VM and Isolates\n(the real build pipeline)"]
  i --> v
ห้าบทภายในเรียงตามลำดับที่ต่อยอดกัน
บทต่อยอดจาก (Phase 3)ตอบคำถามอะไร
The Three Trees, From the SourceWidgets and the Treeทำไม rebuild แบบ type/key เดิมถึง reuse Element และ Widget.canUpdate เปรียบเทียบอะไรจริง ๆ?
Build Scheduling and FramesThe Render Pipeline, Async in Dartสายโซ่จริงจากการเรียก setState() หนึ่งครั้งไปสู่เฟรมที่ถูก schedule หนึ่งครั้งคืออะไร และทำไม setState ที่ซ้ำซ้อนถึงรวมกัน?
Layout and Paint InternalsConstraints & Layout, The Render Pipelineการเรียก markNeedsLayout() หยุดแพร่ขึ้น tree ตรงไหนแน่ ๆ?
Impeller and the GPUThe Render Pipelineสถาปัตยกรรมจริงของ Impeller คืออะไร และ UI thread กับ raster thread รวมกันจริงไหม?
The Dart VM and IsolatesIsolates & compute, Build & Releaseflutter build เรียกอะไรจริง ๆ และโมเดลหน่วยความจำของ isolate หน้าตาเป็นแบบไหนจริง ๆ?

แต่ละบท Phase 3 ที่ลิงก์ไว้ข้างบนแสดงมุมมองภายนอกให้คุณแล้ว (widget test ที่นับ rebuild, ขนาดที่รายงานจริงของ build จริง, ข้อความ error จริง) โมดูลนี้แสดงกลไกเบื้องหลังมุมมองนั้น — มันไม่ได้สอนมุมมองภายนอกซ้ำ แต่ลิงก์ไปหามันแล้วลงลึกไปอีกชั้น

ทุกบทในโมดูลนี้คือแนวคิดเดียวกันที่นำไปใช้กับคนละชั้น: ภายในของ Flutter ไม่ใช่กล่องดำ — มันคือชุด source file และคำสั่งที่รันได้จริงที่ตายตัว ซึ่งจะแสดงกลไกจริงให้คุณเห็นตรง ๆ เสมอ “rebuild แพง” ไม่ใช่คำขวัญ มันคือการทดสอบสองฟิลด์ที่แน่นอนของ Widget.canUpdate พิสูจน์ได้ด้วย spy “relayout boundary หยุดการไต่” ไม่ใช่อุปมา มันคือบรรทัดเดียวตรงตัว _isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent == null พิสูจน์ได้ด้วย counter และ debugDumpRenderTree จริง “Impeller กำจัด shader jank” ไม่ใช่การตลาด มันคือ doc comment ของ ContentContextOptions เองที่จำกัดจำนวน PSO variant ไว้ที่ “หลายร้อย ไม่ใช่หมื่น” เมื่อคุณรู้ว่า source file หรือคำสั่งไหนพิสูจน์ข้อกล่าวอ้างไหน “เชื่อฉันสิ Flutter ทำงานแบบนี้ข้างใต้” ก็ไม่จำเป็นอีกต่อไป — สำหรับข้อกล่าวอ้างของคุณเอง และสำหรับตัดสินข้อกล่าวอ้างของ AI

  • “widget rebuild หมายความว่ามี RenderObject ใหม่เสมอ” The Three Trees พิสูจน์ตรงข้ามด้วย counter จริง: createRenderObject รันครั้งเดียว; rebuild ที่ key เดิมจ่ายแค่ค่า updateRenderObject เท่านั้น
  • “เรียก setState สองครั้งเสมอทำให้เกิด rebuild สองครั้ง” Build Scheduling and Frames พิสูจน์ว่า guard if (dirty) return; ของ Element.markNeedsBuild เองรวมการเรียกแบบซิงโครนัสสองครั้งให้เหลือ build เดียวเป๊ะ
  • “การทำเครื่องหมาย widget ว่าต้อง layout ทำให้ทุกอย่างข้างบนมัน relayout เสมอ” Layout and Paint Internals พิสูจน์ว่า relayout boundary ดูดซับ dirty flag จริง ๆ — debugDumpRenderTree จริงยืนยันจากมุมที่สองที่เป็นอิสระ
  • “Impeller รวม UI thread กับ raster thread เข้าด้วยกัน” Impeller and the GPU พิสูจน์ด้วย quote จริงจาก docs.flutter.dev ว่าการรวมจริงตั้งแต่ 3.29+ คือ UI+platform บน iOS/Android — raster thread ที่ Impeller/Skia คุยกับ GPU จริง ๆ ไม่เคยถูกรวมกับ UI thread เลย
  • “Dart isolate แต่ละตัวมี heap แยกกันโดยสิ้นเชิง” The Dart VM and Isolates พิสูจน์จาก glossary ของ dart-lang/sdk เองว่า isolate ใน group เดียวกัน share heap — แล้วพิสูจน์สด ๆ ว่าการส่ง message ของ compute() ยัง copy ไม่ใช่ alias ไม่ว่าจะอย่างไร

ตัวอย่างของรูปแบบที่ทุกบทในโมดูลนี้พิสูจน์อย่างน้อยหนึ่งครั้ง: ข้อกล่าวอ้างเกี่ยวกับภายในของ Flutter ที่ฟังดูสมเหตุสมผลแต่การอ้างอิง source จริงหรือ test จริงหักล้างได้

🐛 หา bug ในโค้ดนี้

// A comment an AI assistant added above a custom RenderBox
// "sizedByParent means the framework never needs to re-check this
// widget's layout when its internal state changes -- just call
// markNeedsLayout() like any other render object."
class _RenderFixedHeightBox extends RenderBox {
@override
bool get sizedByParent => true;
double _height = 40;
set targetHeight(double v) {
_height = v;
markNeedsLayout();
}
@override
Size computeDryLayout(BoxConstraints constraints) =>
constraints.constrain(Size(constraints.maxWidth, _height));
}
เฉลย

พิสูจน์แล้วว่าผิดใน Layout and Paint Internals ด้วย test จริงที่รันแล้ว: object ที่ sizedByParent: true เป็น relayout boundary ของตัวเองเสมอ (พิสูจน์จากกฎ source ที่แน่นอนที่บทนั้นอ้างไว้) ซึ่งหมายความว่า markNeedsLayout() เฉย ๆ ไม่เคยแจ้ง parent เลยเมื่อ size ของ object นี้เอง เปลี่ยน — parent เก็บ size เก่าไว้แบบเงียบ ๆ doc comment ของ sizedByParent เองต้องการ markNeedsLayoutForSizedByParentChange() แทนสำหรับกรณีนี้เป๊ะ และ test ของบทนั้นพิสูจน์ความต่างตรง ๆ: ด้วยการเรียกเฉย ๆ width ของ child ที่ parent สังเกตเห็นค้างอยู่ที่ค่าเก่า; ด้วยการเรียกที่ถูกต้อง มันอัปเดต

โมดูลนี้อ้าง permalink เข้าไปใน github.com/flutter/flutter และ github.com/dart-lang/sdk มันตรึงไว้ที่ revision/tag ไหน และทำไมถึงสำคัญ?
พิสูจน์แล้วใน The Three Trees ด้วย spy ระดับ render-object จริง เกิดอะไรขึ้นกับ createRenderObject เมื่อ widget type และ key เดิม rebuild โดยเปลี่ยนแค่ property ที่กระทบ paint?
พิสูจน์แล้วใน Build Scheduling and Frames ด้วย build counter จริง guard การ schedule ตัวจริงเบื้องหลัง "การเรียก setState ซ้ำซ้อนไม่มีต้นทุน" คืออะไร?
พิสูจน์แล้วใน Layout and Paint Internals อ้างตรงตัวจาก RenderObject.layout กฎ relayout-boundary ที่แน่นอนคืออะไร?
พิสูจน์แล้วใน Impeller and the GPU ด้วย quote จริงจาก docs.flutter.dev การรวม thread ตัวจริงที่เกิดขึ้นตั้งแต่ Flutter 3.29 คืออะไร?
พิสูจน์แล้วใน The Dart VM and Isolates ด้วยการแกะ APK ที่ build จริง การรันคำสั่ง Unix มาตรฐาน file(1) บน libapp.so แสดงอะไร?