在程式設計的世界中,優化往往是一個重要但容易被忽視的環節。許多開發者在碼出功能後,往往對性能優化掉入次要考量。但當你深入了解 Flutter 這個框架時,你會發現,即使是最細微的變化,也可能會對整體性能產生顯著的影響。今天,我們就來探索一下 Flutter 中的幾個性能優化的小細節。
https://www.youtube.com/watch?v=cVAGLDuc2xE
我們都知道,在 Flutter 送到渲染引擎之前,會先被打包成 Layer tree 然後才進到實際渲染。那這一層層的 Layer 是如何分成哪些群組的,這裡先不展開討論。
想要看你的 Layer 是如何被分配的,最快的方式就是打開 repaintRainbow ,他可以讓你的 Widget 顯示渲染框,如果持續被 repaint 就會一直閃爍彩虹的光芒 🌈
可以直接從程式碼打開:
void main() {
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}
或是在 widget inspector 打開
打開後就能看到彩虹邊框的效果:
接下來就可以觀察 App 裡面有哪裡會被重複繪製,我們接下來可以看一個例子:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TestWidget(data: "Stateful Widget"),
],
),
),
);
}
}
class TestWidget extends StatefulWidget {
const TestWidget({super.key, required this.data});
final String data;
@override
State<TestWidget> createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
@override
Widget build(BuildContext context) {
return SizedBox(
width: 100,
height: 500,
child: SingleChildScrollView(
child: Column(children: [
Center(
child: CustomPaint(
painter: MyPainter(),
child: const SizedBox(
width: 100,
height: 50,
),
),
),
...List.generate(
100,
(index) => Column(
children: [
Text(widget.data),
],
),
),
// painter
]),
));
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
debugPrint('======paint : paint=========');
final paint = Paint()..color = const Color.fromARGB(255, 255, 162, 156);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 100, 100),
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
我們在 MyPainter 被重繪時,印出 paint ,接下來滑動中間的 SingleChildScrollView
,就會注意到 paint 一直被打印出來,顯示我們的 widget 一直不斷被重繪。
接下來掏出秘密武器 RepaintBoundary
,再來看看會是什麼效果:
RepaintBoundary(
child: CustomPaint(
painter: MyPainter(),
child: const SizedBox(
width: 100,
height: 50,
),
),
),
可以看到重新繪製的情形就消失了,這會大大的提升你的畫面性能
看完效果以後,結合一開始說的 Layer Tree 的概念,你大概就可以猜到,RepaintBundary
他的作用在於幫助這個 Widget 建立自己獨立的 Layer。獨立出來以後就不會受到外部的影響,這部分可以直接查看 RepaintBundary
的 RenderObject
: RenderRepaintBundary,他會把 isRepaintBoundary 設置為 true,建立出自己的 Render 屏障從而不再往上尋找需要重新繪製的對象。
class RenderRepaintBoundary extends RenderProxyBox {
/// Creates a repaint boundary around [child].
RenderRepaintBoundary({ RenderBox? child }) : super(child);
@override
bool get isRepaintBoundary => true;
...
}
RepaintBoundary
想像成一個“盾牌”或“屏障”。當你把它放在某個 Widget 前面,你基本上是在說:“只有這個 Widget 和它內部的子組件需要重繪,其他的部分都不需要。” 因此,即使它內部的某個小部件有變化,只有這部分內容會被重新繪製,而不是整個界面。還有時間就來講講另一個可能會拖慢你的執行速度,但你卻沒發現的小地方:async。如果裡面的程式碼相同,僅僅只是加上 async 這個關鍵字會有什麼區別嗎?
我們寫兩個測試的方法,差別只在於有沒有加上 async 關鍵字,
void main() {
final stopwatch = Stopwatch()..start();
testAsyncFunc();
stopwatch.stop();
print('testAsyncFunc() took ${stopwatch.elapsedMilliseconds}ms');
final stopwatch = Stopwatch()..start();
testFunc();
stopwatch.stop();
print('testFunc() took ${stopwatch.elapsedMilliseconds}ms');
}
testAsyncFunc() async {
debugPrint('======testAsync : start=========');
int i = 0;
for (var i = 0; i < 10000000000; i++) {}
debugPrint('======testAsync : end=========');
}
testFunc() {
debugPrint('======normal : start=========');
int i = 0;
for (var i = 0; i < 10000000000; i++) {}
debugPrint('======normal : end=========');
}
在運行之前不知道你有沒有猜到結果了,testAsyncFunc 的執行時間比沒有 async 的方法多了一倍!有沒有很驚訝啊?
flutter: testAsyncFunc() took 6367ms
flutter: testFunc() took 3601ms
這是為什麼呢?
當看到 Dart 中的 async
關鍵字,你可能會誤以為該功能是平行運行的或非同步的。但實際上,async
關鍵字並不會使函數的執行變得平行或非同步。相反,它只是使該函數能夠返回一個 Future 對象,並允許在該函數中使用 await
關鍵字。
testAsyncFunc
仍然會在事件循環的當前迭代中順序執行,並不會創建一個新的執行緒或非同步地運行任何代碼。
那麼,為什麼 testAsyncFunc
的執行時間會比 testFunc
長呢?有以下幾個原因:
函數包裝成Future:
當你標記一個函數為 async
時,Dart 會為該函數創建一個包裝,使它返回一個 Future。即使在沒有真正的非同步操作的情況下,這種包裝和相關的操作都會引入一定的性能開銷。
Event loop 的影響:
儘管你的 testAsyncFunc
函數中沒有真正的非同步操作,但由於它被標記為 async
,所以返回的 Future 會被丟到 event queue 才執行,所以每次 event loop 開啟後他都是最後才被執行到的,所以時間會被越拖越慢。
用一個簡單的範例來測試一下 event loop:
void main() {
print('main start');
Future(() => print('future'));
scheduleMicrotask(() => print('microtask'));
print('main end');
}
這會輸出:
main start
main end
microtask
future
從這裡就可知道 Future 確實是在裡面最後一個被執行到的。
如果你不需要非同步功能,就不應該使用 async
關鍵字,因為它可能會引入不必要的性能開銷。當你真正需要進行非同步操作時(例如,當你需要等待一些外部數據或進行耗時的計算時),async/await
是非常有用的,但在這種情況下,它可能只是多餘的。
優化程式並不僅僅是為了讓它運行得更快,更是為了提供使用者更好的體驗。從 Flutter 的 Layer Tree 到 Dart 中的使用時機,每一個細節都對我們的應用有深遠的影響。因此,我們應當對每一行程式碼持續問題和思考,確保在提供功能的同時,也確保了最佳的性能表現。從今天開始,不妨將這些優化的小細節加入到你的開發中,並體驗它帶來的驚人變化。