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 เครื่องนี้ ไม่เคยยืนยันจากความจำ:
- เกิดอะไรขึ้นจริง ๆ ระหว่าง widget rebuild กับ pixel เปลี่ยน? (The Three Trees, From the Source —
Widget.canUpdate, ตาราง 4 กรณีจริงของElement.updateChild,RenderObjectWidgetที่เขียนเองพิสูจน์ว่าcreateRenderObject/updateRenderObject/performLayout/paintคือ hook สี่ตัวที่กระตุ้นแยกกันได้ และการ reparent ด้วยGlobalKeyพิสูจน์ด้วยidentical()) - อะไรที่เปลี่ยน “widget เรียก
setState” ให้เป็น “เฟรมหนึ่งรันจริง” และตามลำดับไหน? (Build Scheduling and Frames —SchedulerPhaseenum จริง, doc comment 8 ขั้นของWidgetsBinding.drawFrameเอง และ test จริงที่พิสูจน์ว่าsetStateแบบซิงโครนัสสองครั้งรวมกันเป็น build เดียว) - สายการเรียก
markNeedsLayoutหยุดไต่ tree ตรงไหนแน่ ๆ? (Layout and Paint Internals — กฎ relayout-boundary ที่แน่นอนอ้างจาก source พิสูจน์ด้วยคู่RenderBoxที่เขียนเองและdebugDumpRenderTreeจริงที่แสดง annotation ของ boundary ปรากฏและหายไป) - Impeller แทนที่อะไรจริง ๆ และ “UI+raster รวมกันแล้ว” จริงไหม? (Impeller and the GPU — README ของ engine เองและ source ของ
Entity/Contents/RenderPassและการแก้ไขจริง: การรวม thread ตั้งแต่ 3.29+ ที่มีเอกสารรองรับคือ UI+platform ไม่ใช่ UI+raster) flutter buildรันอะไรจริง ๆ และโมเดลหน่วยความจำของ Dart isolate หน้าตาเป็นแบบไหนจริง ๆ? (The Dart VM and Isolates — output แบบ verbose จริงของflutter build apk/flutter build web, ELFlibapp.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 Source | Widgets and the Tree | ทำไม rebuild แบบ type/key เดิมถึง reuse Element และ Widget.canUpdate เปรียบเทียบอะไรจริง ๆ? |
| Build Scheduling and Frames | The Render Pipeline, Async in Dart | สายโซ่จริงจากการเรียก setState() หนึ่งครั้งไปสู่เฟรมที่ถูก schedule หนึ่งครั้งคืออะไร และทำไม setState ที่ซ้ำซ้อนถึงรวมกัน? |
| Layout and Paint Internals | Constraints & Layout, The Render Pipeline | การเรียก markNeedsLayout() หยุดแพร่ขึ้น tree ตรงไหนแน่ ๆ? |
| Impeller and the GPU | The Render Pipeline | สถาปัตยกรรมจริงของ Impeller คืออะไร และ UI thread กับ raster thread รวมกันจริงไหม? |
| The Dart VM and Isolates | Isolates & compute, Build & Release | flutter build เรียกอะไรจริง ๆ และโมเดลหน่วยความจำของ isolate หน้าตาเป็นแบบไหนจริง ๆ? |
แต่ละบท Phase 3 ที่ลิงก์ไว้ข้างบนแสดงมุมมองภายนอกให้คุณแล้ว (widget test ที่นับ rebuild, ขนาดที่รายงานจริงของ build จริง, ข้อความ error จริง) โมดูลนี้แสดงกลไกเบื้องหลังมุมมองนั้น — มันไม่ได้สอนมุมมองภายนอกซ้ำ แต่ลิงก์ไปหามันแล้วลงลึกไปอีกชั้น
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกบทในโมดูลนี้คือแนวคิดเดียวกันที่นำไปใช้กับคนละชั้น: ภายในของ 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
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “widget rebuild หมายความว่ามี
RenderObjectใหม่เสมอ” The Three Trees พิสูจน์ตรงข้ามด้วย counter จริง:createRenderObjectรันครั้งเดียว; rebuild ที่ key เดิมจ่ายแค่ค่าupdateRenderObjectเท่านั้น - “เรียก
setStateสองครั้งเสมอทำให้เกิด rebuild สองครั้ง” Build Scheduling and Frames พิสูจน์ว่า guardif (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 ไม่ว่าจะอย่างไร
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”ตัวอย่างของรูปแบบที่ทุกบทในโมดูลนี้พิสูจน์อย่างน้อยหนึ่งครั้ง: ข้อกล่าวอ้างเกี่ยวกับภายในของ 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 {@overridebool get sizedByParent => true;
double _height = 40;set targetHeight(double v) { _height = v; markNeedsLayout();}
@overrideSize 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 สังเกตเห็นค้างอยู่ที่ค่าเก่า; ด้วยการเรียกที่ถูกต้อง มันอัปเดต