目錄
1. Flutter啟動(dòng)流程 和 渲染流程
2. Element闽寡、BuildContext、RenderObject脑奠、RenderBox
4. 圖片加載原理與緩存
5. 布局過(guò)程
6. 繪制
1. Flutter啟動(dòng)流程 和 渲染流程
- 啟動(dòng)流程
Flutter程序的入口為lib目錄中main.dart文件的main函數(shù)(程序的起點(diǎn))翠肘。
main函數(shù)最簡(jiǎn)單的實(shí)現(xiàn)如下:
void main() {
runApp(MyApp()); // 只調(diào)用了一個(gè)runApp()方法
}
=============================
看一下runApp方法的實(shí)現(xiàn):
// 參數(shù)app為根widget,是Flutter應(yīng)用啟動(dòng)后要展示的第一個(gè)Widget误算。
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
=============================
看一下WidgetsFlutterBinding類的定義:
//
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
// 負(fù)責(zé)初始化一個(gè)WidgetsBinding的全局單例
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
可以看到WidgetsFlutterBinding繼承自BindingBase并混入了很多Binding。
查看這些Binding的源碼可以發(fā)現(xiàn)這些Binding中基本都是監(jiān)聽(tīng)并處理Window對(duì)象(包含了當(dāng)前設(shè)備和系統(tǒng)的一些信息以及Flutter引擎的一些回調(diào))的一些事件迷殿,然后將這些事件按照Framework的模型包裝抽象然后分發(fā)儿礼。
WidgetsFlutterBinding正是粘連Engine引擎層與Framework框架層的“膠水”(綁定Engine引擎層和Framework框架層的橋梁)。
1. GestureBinding:
提供了window.onPointerDataPacket回調(diào)庆寺,
綁定Framework框架層的手勢(shì)系統(tǒng)Gestures蚊夫。是Framework事件模型與底層事件的綁定入口。
2. ServicesBinding:
提供了window.onPlatformMessage回調(diào)懦尝,
用于綁定平臺(tái)消息通道知纷,主要處理原生和Flutter通信。
3. SchedulerBinding:
提供了window.onBeginFrame和window.onDrawFrame回調(diào)監(jiān)聽(tīng)刷新事件陵霉,
綁定Framework框架層的繪制調(diào)度系統(tǒng)琅轧。
4. PaintingBinding:
綁定Framework框架層的繪制系統(tǒng)Painting。主要用于處理圖片緩存踊挠。
5. SemanticsBinding:
綁定Framework框架層的語(yǔ)義化系統(tǒng)乍桂。主要是輔助功能的底層支持。
6. RendererBinding:
提供了window.onMetricsChanged、window.onTextScaleFactorChanged等回調(diào)睹酌。
綁定Framework框架層的渲染系統(tǒng)Rendering权谁。
7. WidgetsBinding:
提供了window.onLocaleChanged、onBuildScheduled等回調(diào)忍疾。
綁定Framework框架層的組件庫(kù)Widgets闯传。
=============================
runApp方法在獲取到WidgetsBinding單例后,先調(diào)用了該單例的attachRootWidget方法:
// 該方法主要完成了根widget到根RenderObject再到根Element的整個(gè)關(guān)聯(lián)過(guò)程
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView, // 渲染樹(shù)的根(一個(gè)RenderObject)
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement); // renderViewElement是renderView對(duì)應(yīng)的Element對(duì)象
}
=============================
attachToRenderTree方法的實(shí)現(xiàn):
//
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
該方法負(fù)責(zé)創(chuàng)建根element(RenderObjectToWidgetElement)卤妒,并且將根element與根widget進(jìn)行關(guān)聯(lián)(即創(chuàng)建出根widget樹(shù)對(duì)應(yīng)的根element樹(shù))甥绿。如果根element已經(jīng)創(chuàng)建過(guò)了,則將根element中關(guān)聯(lián)的根widget設(shè)為新的并調(diào)用markNeedsBuild更新UI则披。BuildOwner是widget framework的管理類共缕,它跟蹤哪些widget需要重新構(gòu)建。
=============================
runApp方法中接著調(diào)用WidgetsBinding單例的scheduleWarmUpFrame方法:
// 該方法的實(shí)現(xiàn)在SchedulerBinding中士复,它被調(diào)用后會(huì)立即進(jìn)行一次繪制(而不是等待"vsync" 信號(hào))图谷,在此次繪制結(jié)束前,該方法會(huì)鎖定事件分發(fā)阱洪,也就是說(shuō)在本次繪制結(jié)束完成之前 Flutter 將不會(huì)響應(yīng)各種事件便贵,這可以保證在繪制過(guò)程中不會(huì)再觸發(fā)新的重繪。
void scheduleWarmUpFrame() {
...//
Timer.run(() {
handleBeginFrame(null);
});
Timer.run(() {
handleDrawFrame();
resetEpoch();
});
// 鎖定事件
lockEvents(() async {
await endOfFrame;
Timeline.finishSync();
});
...
}
該方法中主要調(diào)用了handleBeginFrame() 和 handleDrawFrame() 兩個(gè)方法冗荸,前者主要是執(zhí)行了transientCallbacks隊(duì)列承璃,而后者執(zhí)行了 persistentCallbacks 和 postFrameCallbacks 隊(duì)列。
=============================
最后看一下上面說(shuō)到的Window類蚌本,定義如下:
// Flutter Framework連接宿主操作系統(tǒng)的接口
class Window {
// 當(dāng)前設(shè)備的DPI盔粹,即一個(gè)邏輯像素顯示多少物理像素。數(shù)字越大程癌,顯示效果就越精細(xì)保真舷嗡。
// DPI是設(shè)備屏幕的固件屬性,如Nexus 6的屏幕DPI為3.5嵌莉。
double get devicePixelRatio => _devicePixelRatio;
// Flutter UI繪制區(qū)域的大小
Size get physicalSize => _physicalSize;
// 當(dāng)前系統(tǒng)默認(rèn)的語(yǔ)言Locale
Locale get locale;
// 當(dāng)前系統(tǒng)字體縮放比例质欲。
double get textScaleFactor => _textScaleFactor;
// 當(dāng)繪制區(qū)域大小改變回調(diào)
VoidCallback get onMetricsChanged => _onMetricsChanged;
// Locale發(fā)生變化回調(diào)
VoidCallback get onLocaleChanged => _onLocaleChanged;
// 系統(tǒng)字體縮放變化回調(diào)
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
// 繪制前回調(diào)浮禾,一般會(huì)受顯示器的垂直同步信號(hào)VSync驅(qū)動(dòng)苛坚,當(dāng)屏幕刷新時(shí)就會(huì)被調(diào)用
FrameCallback get onBeginFrame => _onBeginFrame;
// 繪制回調(diào)
VoidCallback get onDrawFrame => _onDrawFrame;
// 點(diǎn)擊或指針事件回調(diào)
PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
// 調(diào)度Frame刻获,該方法執(zhí)行后,onBeginFrame和onDrawFrame將緊接著會(huì)在合適時(shí)機(jī)被調(diào)用只祠,
// 此方法會(huì)直接調(diào)用Flutter engine的Window_scheduleFrame方法
void scheduleFrame() native 'Window_scheduleFrame';
// 更新應(yīng)用在GPU上的渲染,此方法會(huì)直接調(diào)用Flutter engine的Window_render方法
void render(Scene scene) native 'Window_render';
// 發(fā)送平臺(tái)消息
void sendPlatformMessage(String name,
ByteData data,
PlatformMessageResponseCallback callback) ;
// 平臺(tái)通道消息處理回調(diào)
PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
... // 其它屬性及回調(diào)
}
可以看到Window類包含了當(dāng)前設(shè)備和系統(tǒng)的一些信息以及Flutter Engine的一些回調(diào)兜蠕。
- 渲染流程
- Frame 一次繪制(一幀)
Flutter引擎受顯示器垂直同步信號(hào)"VSync"的驅(qū)使不斷觸發(fā)繪制扰肌,可以實(shí)現(xiàn)60fps(Frame Per-Second)即一秒重繪60次抛寝,F(xiàn)PS值越大越流暢。
Flutter中的frame概念并不等同于屏幕的刷新幀F(xiàn)rame,因?yàn)镕lutter UI框架的frame并不是每次屏幕刷新都會(huì)觸發(fā)盗舰。如果UI在一段時(shí)間不變晶府,那么每次屏幕刷新都重新走一遍渲染流程是不必要的。因此钻趋,F(xiàn)lutter在第一幀渲染結(jié)束后會(huì)采取一種主動(dòng)請(qǐng)求frame的方式來(lái)實(shí)現(xiàn)只有當(dāng)UI可能會(huì)改變時(shí)才會(huì)重新走渲染流程川陆。
1. Flutter在window上注冊(cè)了一個(gè)onBeginFrame和一個(gè)onDrawFrame回調(diào),在onDrawFrame回調(diào)中最終會(huì)調(diào)用drawFrame蛮位。
2. 主動(dòng)調(diào)用window.scheduleFrame方法之后较沪,F(xiàn)lutter引擎會(huì)在合適的時(shí)機(jī)(可以認(rèn)為是在屏幕下一次刷新之前,具體取決于Flutter引擎的實(shí)現(xiàn))來(lái)調(diào)用onBeginFrame和onDrawFrame失仁。
- SchedulerPhase(Flutter應(yīng)用執(zhí)行過(guò)程的5種狀態(tài))
// Flutter將整個(gè)生命周期分為五種狀態(tài)
enum SchedulerPhase {
idle,
transientCallbacks,
midFrameMicrotasks,
persistentCallbacks,
postFrameCallbacks,
}
Flutter應(yīng)用執(zhí)行過(guò)程可簡(jiǎn)單分為2種狀態(tài):
整個(gè)Flutter應(yīng)用生命周期就是不斷在idle和frame兩種狀態(tài)間切換尸曼。
1. idle狀態(tài)
空閑狀態(tài)。
表示沒(méi)有frame在處理(即頁(yè)面未發(fā)生變化萄焦,并不需要重新渲染)控轿。
如果應(yīng)用狀態(tài)改變需要刷新UI,則需要通過(guò)調(diào)用scheduleFrame()去請(qǐng)求新的 frame拂封,當(dāng)frame到來(lái)時(shí)就進(jìn)入了frame狀態(tài)茬射。
注意:空閑狀態(tài)只是指沒(méi)有frame在處理,通常微任務(wù)冒签、定時(shí)器回調(diào)或者用戶事件回調(diào)都可能被執(zhí)行在抛。比如監(jiān)聽(tīng)了tap事件,用戶點(diǎn)擊后onTap回調(diào)就是在idle階段被執(zhí)行的镣衡。
2. frame狀態(tài)(又分為4種)
當(dāng)有新的frame到來(lái)時(shí)霜定,具體處理過(guò)程是依次執(zhí)行四個(gè)任務(wù)隊(duì)列:
1. transientCallbacks:
執(zhí)行臨時(shí)回調(diào)任務(wù)(只能被執(zhí)行一次,執(zhí)行后會(huì)被移出臨時(shí)任務(wù)隊(duì)列)廊鸥。
可存放動(dòng)畫(huà)回調(diào)望浩。
可以通過(guò)SchedulerBinding.instance.scheduleFrameCallback添加回調(diào)。
2. midFrameMicrotasks:
在執(zhí)行臨時(shí)任務(wù)時(shí)可能會(huì)產(chǎn)生一些新的微任務(wù)惰说,比如在執(zhí)行第一個(gè)臨時(shí)任務(wù)時(shí)創(chuàng)建了一個(gè)Future磨德,且這個(gè)Future在所有臨時(shí)任務(wù)執(zhí)行完畢前就已經(jīng)resolve了,此時(shí)Future的回調(diào)將在本階段執(zhí)行吆视。
3. persistentCallbacks:
執(zhí)行一些持久任務(wù)(每一個(gè)frame都要執(zhí)行的任務(wù))
處理渲染管線(構(gòu)建典挑、布局、繪制)啦吧。不能在這里觸發(fā)新的Frame您觉。
可以通過(guò)SchedulerBinding.instance.addPersitentFrameCallback添加(不能移除)。
4. postFrameCallbacks:
在當(dāng)前Frame結(jié)束之前會(huì)被調(diào)用一次授滓。
負(fù)責(zé)清理工作琳水。不能在這里觸發(fā)新的Frame(會(huì)導(dǎo)致無(wú)限循環(huán)刷新)肆糕。
可以通過(guò)SchedulerBinding.instance.addPostFrameCallback添加。
當(dāng)四個(gè)任務(wù)隊(duì)列執(zhí)行完畢后當(dāng)前frame結(jié)束在孝。
- setState執(zhí)行流程诚啃、執(zhí)行時(shí)機(jī)問(wèn)題
執(zhí)行流程
1. 調(diào)用當(dāng)前element的markNeedsBuild方法,將當(dāng)前element標(biāo)記為dirty私沮。
2. 調(diào)用scheduleBuildFor始赎,將標(biāo)記為dirty的當(dāng)前element添加到pipelineOwner的dirtyElements列表。
3. 請(qǐng)求一個(gè)新的frame仔燕,然后繪制新frame:
onBuildScheduled--(會(huì)調(diào)用)->ensureVisualUpdate--(會(huì)調(diào)用)->scheduleFrame() 造垛。
當(dāng)新的frame到來(lái)時(shí)執(zhí)行渲染管線。
=========================================
執(zhí)行時(shí)機(jī)問(wèn)題:
在transientCallbacks和midFrameMicrotasks階段晰搀,如果應(yīng)用狀態(tài)發(fā)生變化筋搏,最好的方式是只將組件標(biāo)記為dirty,而不用再去請(qǐng)求新的frame 厕隧,因?yàn)楫?dāng)前frame還沒(méi)有執(zhí)行到persistentCallbacks奔脐,因此后面執(zhí)行到后就會(huì)在當(dāng)前幀渲染管線中刷新UI。因此吁讨,setState在標(biāo)記完dirty后會(huì)先判斷一下調(diào)度狀態(tài)髓迎,如果是idle或postFrameCallbacks階段才會(huì)去請(qǐng)求新的frame :
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame(); // 請(qǐng)求新的frame
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks: // 注意這一行
return;
}
}
如果在build階段調(diào)用setState的話就又會(huì)導(dǎo)致調(diào)用build(無(wú)限循環(huán)調(diào)用),因此flutter框架發(fā)現(xiàn)在build階段調(diào)用setState的話就會(huì)報(bào)錯(cuò)建丧,如:
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, c) {
// build階段不能調(diào)用setState, 會(huì)報(bào)錯(cuò)
setState(() {
++index;
});
return Text('xx');
},
);
}
/*
注意:如果直接在build中調(diào)用setState(和上面的上下文不一樣):
@override
Widget build(BuildContext context) {
setState(() {
++index;
});
return Text('$index');
}
運(yùn)行后是不會(huì)報(bào)錯(cuò)的排龄,原因是在執(zhí)行build時(shí)當(dāng)前組件對(duì)應(yīng)的element的dirty狀態(tài)為true,只有build執(zhí)行完后才會(huì)被置為false翎朱。而setState執(zhí)行時(shí)會(huì)先判斷當(dāng)前dirty值橄维,如果為true則會(huì)直接返回,因此就不會(huì)報(bào)錯(cuò)拴曲。
【存疑】
*/
不止build階段争舞,在persistentCallbacks(build構(gòu)建、布局澈灼、繪制)階段都不能調(diào)用setState竞川,會(huì)導(dǎo)致循環(huán)調(diào)用。因此如果要在這些階段更新應(yīng)用狀態(tài)時(shí)叁熔,不能直接調(diào)用setState委乌,可使用如下方法來(lái)安全更新:
// 自定義一個(gè)update方法(可以安全更新?tīng)顟B(tài))
// 如果是persistentCallbacks階段,則調(diào)用了SchedulerBinding.instance的addPostFrameCallback荣回。
// 如果是其他階段遭贸,則直接調(diào)用setState。
void update(VoidCallback fn) {
final schedulerPhase = SchedulerBinding.instance.schedulerPhase;
if (schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(fn);
});
} else {
setState(fn);
}
}
-
渲染管線(渲染流程)
setState方法內(nèi)部會(huì)調(diào)用scheduleFrame方法去請(qǐng)求新的frame心软,當(dāng)新frame到來(lái)時(shí) 會(huì)調(diào)用WidgetsBinding的drawFrame() 方法:
//
@override
void drawFrame() {
...// 省略無(wú)關(guān)代碼
try {
buildOwner.buildScope(renderViewElement); // 1. 重新構(gòu)建widget樹(shù)
super.drawFrame(); // 調(diào)用父類的drawFrame()方法壕吹,具體實(shí)現(xiàn)見(jiàn)下方注釋
/*
super.drawFrame()的方法實(shí)現(xiàn)如下:
pipelineOwner.flushLayout(); // 2. 更新布局
pipelineOwner.flushCompositingBits(); // 3. 更新“層合成”信息
pipelineOwner.flushPaint(); // 4. 重繪
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏除秀,會(huì)將繪制出的bit數(shù)據(jù)發(fā)送給GPU
pipelineOwner.flushSemantics();
_firstFrameSent = true;
}
*/
}
}
主要作了5件事:
1. 重新構(gòu)建widget樹(shù)。
如果 dirtyElements 列表不為空算利,則遍歷該列表,調(diào)用每一個(gè)element的rebuild方法重新構(gòu)建新的widget樹(shù)泳姐。
由于新的widget樹(shù)使用新的狀態(tài)構(gòu)建效拭,所以可能導(dǎo)致widget布局信息(占用的空間和位置)發(fā)生變化,如果發(fā)生變化胖秒,則會(huì)調(diào)用其renderObject的markNeedsLayout方法缎患,該方法會(huì)從當(dāng)前節(jié)點(diǎn)向父級(jí)查找,直到找到一個(gè)relayoutBoundary的節(jié)點(diǎn)阎肝,然后會(huì)將它添加到一個(gè)全局的nodesNeedingLayout列表中挤渔;如果直到根節(jié)點(diǎn)也沒(méi)有找到relayoutBoundary,則將根節(jié)點(diǎn)添加到nodesNeedingLayout列表中风题。
2. 更新布局判导。
遍歷nodesNeedingLayout數(shù)組,對(duì)每一個(gè)renderObject重新布局(調(diào)用其layout方法)沛硅,確定新的大小和偏移眼刃。layout方法中會(huì)調(diào)用markNeedsPaint方法(和markNeedsLayout方法功能類似),也會(huì)從當(dāng)前節(jié)點(diǎn)向父級(jí)查找摇肌,直到找到一個(gè)isRepaintBoundary屬性為true的父節(jié)點(diǎn)擂红,然后將它添加到一個(gè)全局的nodesNeedingPaint列表中;由于根節(jié)點(diǎn)(RenderView)的 isRepaintBoundary 為 true围小,所以必會(huì)找到一個(gè)昵骤。查找過(guò)程結(jié)束后會(huì)調(diào)用 buildOwner.requestVisualUpdate 方法,該方法最終會(huì)調(diào)用scheduleFrame()肯适,該方法中會(huì)先判斷是否已經(jīng)請(qǐng)求過(guò)新的frame变秦,如果沒(méi)有則請(qǐng)求一個(gè)新的frame。
3. 更新“層合成”信息框舔。
4. 重繪伴栓。
遍歷nodesNeedingPaint列表,調(diào)用每一個(gè)節(jié)點(diǎn)的paint方法進(jìn)行重繪雨饺,繪制過(guò)程會(huì)生成Layer钳垮。flutter中繪制結(jié)果是保存在Layer中的,只要Layer不釋放额港,繪制的結(jié)果就會(huì)被緩存饺窿。因此,Layer可以跨frame來(lái)緩存繪制結(jié)果移斩,避免不必要的重繪開(kāi)銷肚医。
Flutter框架繪制過(guò)程中绢馍,遇到isRepaintBoundary 為 true 的節(jié)點(diǎn)時(shí),才會(huì)生成一個(gè)新的Layer肠套〗⒂浚可見(jiàn)Layer和 renderObject 不是一一對(duì)應(yīng)關(guān)系,父子節(jié)點(diǎn)可以共享layer你稚。如果是自定義組件瓷耙,我們可以在renderObject中手動(dòng)添加任意多個(gè) Layer,這通常用于只需一次繪制而隨后不會(huì)發(fā)生變化的繪制元素的緩存場(chǎng)景刁赖。
5. 上屏:將繪制的產(chǎn)物顯示在屏幕上
繪制完成后得到的是一棵Layer樹(shù)搁痛,最后需要將Layer樹(shù)中的繪制信息在屏幕上顯示。Flutter是自實(shí)現(xiàn)的渲染引擎宇弛,因此需要將繪制信息提交給Flutter engine鸡典,而renderView.compositeFrame正是完成了這個(gè)使命。
// 在上屏?xí)r會(huì)將所有的Layer添加到Scene場(chǎng)景對(duì)象后枪芒,再渲染Scene彻况。
上面5步被稱為渲染管線(rendering pipeline)或渲染流水線。
渲染繪制的具體邏輯在RendererBinding中實(shí)現(xiàn)舅踪。查看其源碼疗垛,發(fā)現(xiàn)在其initInstances()方法中有如下代碼:
void initInstances() {
... // 省略無(wú)關(guān)代碼
// 監(jiān)聽(tīng)Window對(duì)象的事件
ui.window
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction;
// 通過(guò)addPersistentFrameCallback 向persistentCallbacks隊(duì)列添加了一個(gè)回調(diào) _handlePersistentFrameCallback
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 布局
pipelineOwner.flushCompositingBits(); // 重繪之前的預(yù)處理操作,檢查RenderObject是否需要重繪
pipelineOwner.flushPaint(); // 重繪
renderView.compositeFrame(); // 上屏(將需要繪制的比特?cái)?shù)據(jù)發(fā)給GPU)
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
===================================
flushLayout() 方法主要任務(wù)是更新了所有被標(biāo)記為“dirty”的RenderObject的布局信息硫朦。主要的動(dòng)作發(fā)生在node._layoutWithoutResize()方法中贷腕,該方法中會(huì)調(diào)用performLayout()進(jìn)行重新布局举农。
void flushLayout() {
...
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (RenderObject node in
dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
}
}
===================================
flushCompositingBits()方法 檢查RenderObject是否需要重繪会宪,然后更新RenderObject.needsCompositing屬性(如果該屬性值被標(biāo)記為true則需要重繪)虐骑。
void flushCompositingBits() {
_nodesNeedingCompositingBitsUpdate.sort(
(RenderObject a, RenderObject b) => a.depth - b.depth
);
for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits(); // 更新RenderObject.needsCompositing屬性值
}
_nodesNeedingCompositingBitsUpdate.clear();
}
===================================
flushPaint() 方法進(jìn)行了最終的繪制碧注,可以看出它不是重繪了所有 RenderObject勒葱,而是只重繪了需要重繪的 RenderObject篷扩。真正的繪制是通過(guò)PaintingContext.repaintCompositedChild()來(lái)繪制的院究,該方法最終會(huì)調(diào)用Flutter engine提供的Canvas API來(lái)完成繪制掌栅。
void flushPaint() {
...
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// 反向遍歷需要重繪的RenderObject
for (RenderObject node in
dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
// 真正的繪制邏輯
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
}
}
===================================
compositeFrame() 方法中有一個(gè)Scene對(duì)象祷舀,Scene對(duì)象是一個(gè)數(shù)據(jù)結(jié)構(gòu)瀑梗,保存最終渲染后的像素信息。這個(gè)方法將Canvas畫(huà)好的Scene傳給window.render()方法裳扯,該方法會(huì)直接將scene信息發(fā)送給Flutter engine抛丽,最終由engine將圖像畫(huà)在設(shè)備屏幕上。
void compositeFrame() {
...
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
ui.window.render(scene); //調(diào)用Flutter engine的渲染API
scene.dispose();
} finally {
Timeline.finishSync();
}
}
2. Element饰豺、BuildContext亿鲜、RenderObject、RenderBox
- Element
Flutter的UI框架包含三棵樹(shù):Widget樹(shù)冤吨、Element樹(shù)蒿柳、渲染樹(shù)饶套。
最終的UI樹(shù)由一個(gè)個(gè)獨(dú)立的Element節(jié)點(diǎn)構(gòu)成。Widget組件的Layout垒探、渲染最終都是通過(guò)RenderObject來(lái)完成的妓蛮,從創(chuàng)建到渲染的大體流程是:根據(jù)Widget生成Element,然后創(chuàng)建相應(yīng)的RenderObject并關(guān)聯(lián)到Element.renderObject屬性上圾叼,最后再通過(guò)RenderObject來(lái)完成布局排列和繪制蛤克。Flutter正是通過(guò)Element這個(gè)紐帶將Widget和RenderObject關(guān)聯(lián)起來(lái)。
Element是Widget在UI樹(shù)具體位置的一個(gè)實(shí)例化對(duì)象褐奥,大多數(shù)Element只有唯一的renderObject,但還有一些Element會(huì)有多個(gè)子節(jié)點(diǎn)翘簇,如繼承自RenderObjectElement的一些類(如:MultiChildRenderObjectElement等)撬码。最終所有Element的RenderObject構(gòu)成一棵渲染樹(shù)(Render Tree)。
對(duì)于開(kāi)發(fā)者來(lái)說(shuō)版保,大多數(shù)情況下只需要關(guān)注Widget樹(shù)呜笑,F(xiàn)lutter框架已經(jīng)將對(duì)Widget樹(shù)的操作映射到了Element樹(shù)上,極大的降低了復(fù)雜度彻犁,提高了開(kāi)發(fā)效率叫胁。
但有時(shí)候必須得直接使用Element對(duì)象來(lái)完成一些操作,比如獲取主題Theme數(shù)據(jù)汞幢。
Element的生命周期如下:
1. Framework 調(diào)用Widget.createElement 創(chuàng)建一個(gè)Element實(shí)例element驼鹅。
2. Framework 調(diào)用element.mount(parentElement,newSlot) ,mount方法中首先調(diào)用element所對(duì)應(yīng)Widget的createRenderObject方法創(chuàng)建與element相關(guān)聯(lián)的RenderObject對(duì)象森篷,然后調(diào)用element.attachRenderObject方法將element.renderObject添加到渲染樹(shù)中插槽指定的位置(這一步不是必須的输钩,一般發(fā)生在Element樹(shù)結(jié)構(gòu)發(fā)生變化時(shí)才需要重新attach)。插入到渲染樹(shù)后的element就處于“active”狀態(tài)仲智,處于“active”狀態(tài)后就可以顯示在屏幕上了(可以隱藏)买乃。
3. 當(dāng)有父Widget的配置數(shù)據(jù)改變時(shí),同時(shí)其State.build返回的Widget結(jié)構(gòu)與之前不同钓辆,此時(shí)就需要重新構(gòu)建對(duì)應(yīng)的Element樹(shù)剪验。為了進(jìn)行Element復(fù)用,在Element重新構(gòu)建前會(huì)先嘗試是否可以復(fù)用舊樹(shù)上相同位置的element前联,element節(jié)點(diǎn)在更新前都會(huì)調(diào)用其對(duì)應(yīng)Widget的canUpdate方法功戚,如果返回true,則復(fù)用舊Element似嗤,舊的Element會(huì)使用新Widget配置數(shù)據(jù)更新疫铜,反之則會(huì)創(chuàng)建一個(gè)新的Element。Widget.canUpdate主要是判斷newWidget與oldWidget的runtimeType和key是否同時(shí)相等双谆,如果同時(shí)相等就返回true壳咕,否則就會(huì)返回false席揽。根據(jù)這個(gè)原理,當(dāng)需要強(qiáng)制更新一個(gè)Widget時(shí)谓厘,可以通過(guò)指定不同的Key來(lái)避免復(fù)用幌羞。
4. 當(dāng)有祖先Element決定要移除element 時(shí)(如Widget樹(shù)結(jié)構(gòu)發(fā)生了變化,導(dǎo)致element對(duì)應(yīng)的Widget被移除)竟稳,這時(shí)該祖先Element就會(huì)調(diào)用deactivateChild 方法來(lái)移除它属桦,移除后element.renderObject也會(huì)被從渲染樹(shù)中移除,然后Framework會(huì)調(diào)用element.deactivate 方法他爸,這時(shí)element狀態(tài)變?yōu)椤癷nactive”狀態(tài)聂宾。
5. “inactive”態(tài)的element將不會(huì)再顯示到屏幕。為了避免在一次動(dòng)畫(huà)執(zhí)行過(guò)程中反復(fù)創(chuàng)建诊笤、移除某個(gè)特定element系谐,“inactive”態(tài)的element在當(dāng)前動(dòng)畫(huà)最后一幀結(jié)束前都會(huì)保留,如果在動(dòng)畫(huà)執(zhí)行結(jié)束后它還未能重新變成“active”狀態(tài)讨跟,F(xiàn)ramework就會(huì)調(diào)用其unmount方法將其徹底移除纪他,這時(shí)element的狀態(tài)為defunct,它將永遠(yuǎn)不會(huì)再被插入到樹(shù)中。
6. 如果element要重新插入到Element樹(shù)的其它位置晾匠,如element或element的祖先擁有一個(gè)GlobalKey(用于全局復(fù)用元素)茶袒,那么Framework會(huì)先將element從現(xiàn)有位置移除,然后再調(diào)用其activate方法凉馆,并將其renderObject重新attach到渲染樹(shù)薪寓。
- BuildContext
abstract class BuildContext {
...
}
context可用于:
1. Theme.of(context) // 獲取主題
2. Navigator.push(context, route) // 入棧新路由
3. Localizations.of(context, type) // 獲取Local
4. context.size // 獲取大小
5. context.findRenderObject() // 查找當(dāng)前或最近的一個(gè)祖先RenderObject
StatelessWidget和StatefulWidget的build方法都會(huì)傳一個(gè)BuildContext對(duì)象(即Element對(duì)象)
Widget build(BuildContext context) {}
該build方法在StatelessWidget和StatefulWidget對(duì)應(yīng)的StatelessElement和StatefulElement的build方法中被調(diào)用。例(StatelessElement):
class StatelessElement extends ComponentElement {
@override
Widget build() => widget.build(this); // 這里調(diào)用了Widget的build方法
}
build傳遞的參數(shù)是this澜共,所有這個(gè)BuildContext就是StatelessElement预愤。但StatelessElement和StatefulElement本身并沒(méi)有實(shí)現(xiàn)BuildContext接口。繼續(xù)跟蹤代碼咳胃,發(fā)現(xiàn)它們間接繼承自Element類植康,然后查看Element類定義,發(fā)現(xiàn)Element類果然實(shí)現(xiàn)了BuildContext接口:
class Element extends DiagnosticableTree implements BuildContext {
}
至此真相大白展懈,BuildContext就是widget對(duì)應(yīng)的Element销睁,所以可以通過(guò)context在StatelessWidget和StatefulWidget的build方法中直接訪問(wèn)Element對(duì)象。獲取主題數(shù)據(jù)的代碼Theme.of(context)內(nèi)部正是調(diào)用了Element的dependOnInheritedWidgetOfExactType()方法存崖。
可以看到Element是Flutter UI框架內(nèi)部連接widget和RenderObject的紐帶冻记,大多數(shù)時(shí)候開(kāi)發(fā)者只需要關(guān)注widget層即可,但是widget層有時(shí)候并不能完全屏蔽Element細(xì)節(jié)来惧,所以Framework在StatelessWidget和StatefulWidget中通過(guò)build方法參數(shù)又將Element對(duì)象也傳遞給了開(kāi)發(fā)者冗栗。這樣一來(lái),開(kāi)發(fā)者便可以在需要時(shí)直接操作Element對(duì)象。
完全可以直接通過(guò)Element來(lái)搭建一個(gè)UI框架隅居,但使用Widget更方便钠至。
例(通過(guò)純粹的Element來(lái)模擬一個(gè)StatefulWidget的功能)
假設(shè)有一個(gè)頁(yè)面,該頁(yè)面有一個(gè)按鈕胎源,按鈕的文本是一個(gè)9位數(shù)棉钧,點(diǎn)擊一次按鈕,則對(duì)9個(gè)數(shù)隨機(jī)排一次序
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
@override
Widget build() {
Color primary=Theme.of(this).primaryColor; // 1
return GestureDetector(
child: Center(
child: FlatButton(
child: Text(text, style: TextStyle(color: primary),),
onPressed: () {
var t = text.split("")..shuffle();
text = t.join();
markNeedsBuild(); // 點(diǎn)擊后將該Element標(biāo)記為dirty涕蚤,Element將會(huì)rebuild
},
),
),
);
}
}
說(shuō)明:
1. 上面build方法不接收參數(shù)宪卿,這一點(diǎn)和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代碼中需要用到BuildContext的地方直接用this代替即可万栅,因?yàn)楫?dāng)前對(duì)象本身就是Element實(shí)例佑钾。
2. 當(dāng)text發(fā)生改變時(shí),調(diào)用markNeedsBuild()方法將當(dāng)前Element標(biāo)記為dirty即可烦粒,標(biāo)記為dirty的Element會(huì)在下一幀中重建休溶。實(shí)際上,State.setState()在內(nèi)部也是調(diào)用的markNeedsBuild()方法撒遣。
3. 上面代碼中build方法返回的仍然是一個(gè)widget邮偎,再加上適配器就可以和其他組件組合使用管跺。如果UI全是由Element組成义黎,則這里返回類型應(yīng)為Element。
如果需要將上面代碼在現(xiàn)有Flutter框架中跑起來(lái)豁跑,那么還得提供一個(gè)“適配器”widget將HomeView結(jié)合到現(xiàn)有框架中廉涕,下面CustomHome就相當(dāng)于“適配器”:
class CustomHome extends Widget {
@override
Element createElement() {
return HomeView(this);
}
}
點(diǎn)擊按鈕則按鈕文本會(huì)隨機(jī)排序。
- RenderObject和RenderBox
每個(gè)Element都對(duì)應(yīng)一個(gè)RenderObject艇拍,可以通過(guò)Element.renderObject來(lái)獲取狐蜕。
RenderObject的主要職責(zé)是Layout和繪制,所有的RenderObject會(huì)組成一棵渲染樹(shù)卸夕。
RenderObject就是渲染樹(shù)中的一個(gè)對(duì)象层释,它主要的作用是實(shí)現(xiàn)事件響應(yīng)以及渲染管線中除過(guò) build 的部分(build 部分由 element 實(shí)現(xiàn)),即包括:布局快集、繪制贡羔、層合成以及上屏。
RenderObject擁有一個(gè)parent(渲染樹(shù)中自己的父節(jié)點(diǎn))和一個(gè)parentData(slot插槽:一個(gè)預(yù)留變量)个初。
在父組件的布局過(guò)程乖寒,會(huì)確定其所有子組件布局信息(如位置信息,即相對(duì)于父組件的偏移)院溺,而這些布局信息需要在布局階段保存起來(lái)楣嘁,因?yàn)椴季中畔⒃诤罄m(xù)的繪制階段還需要被使用(用于確定組件的繪制位置),而parentData屬性的主要作用就是保存布局信息,比如在Stack 布局中逐虚,RenderStack就會(huì)將子元素的偏移數(shù)據(jù)存儲(chǔ)在子元素的parentData中聋溜。
RenderObject類本身實(shí)現(xiàn)了一套基礎(chǔ)的布局和繪制協(xié)議,但是并沒(méi)有定義子節(jié)點(diǎn)模型(如一個(gè)節(jié)點(diǎn)可以有幾個(gè)子節(jié)點(diǎn)痊班,沒(méi)有子節(jié)點(diǎn)勤婚?一個(gè)??jī)蓚€(gè)涤伐?或者更多馒胆?)。 它也沒(méi)有定義坐標(biāo)系統(tǒng)(如子節(jié)點(diǎn)定位是在笛卡爾坐標(biāo)中還是極坐標(biāo)凝果?)和具體的布局協(xié)議(是通過(guò)寬高還是通過(guò)constraint和size?祝迂,或者是否由父節(jié)點(diǎn)在子節(jié)點(diǎn)布局之前或之后設(shè)置子節(jié)點(diǎn)的大小和位置等)。
為此器净,F(xiàn)lutter提供了一個(gè)RenderBox類和一個(gè) RenderSliver類型雳,都繼承自RenderObject,布局坐標(biāo)系統(tǒng)采用笛卡爾坐標(biāo)系山害。Flutter 基于這兩個(gè)類分別實(shí)現(xiàn)了基于 RenderBox 的盒模型布局和基于 Sliver 的按需加載模型纠俭。
如果要從頭到尾實(shí)現(xiàn)一個(gè)RenderObject是比較麻煩的(必須去實(shí)現(xiàn)layout、繪制和命中測(cè)試邏輯)浪慌,大多數(shù)時(shí)候可以直接在Widget層通過(guò)組合或者CustomPaint完成自定義UI冤荆。如果遇到只能定義一個(gè)新RenderObject的場(chǎng)景時(shí)(如要實(shí)現(xiàn)一個(gè)新的layout算法的布局容器),可以直接繼承自RenderBox去實(shí)現(xiàn)权纤。
4. 圖片加載原理與緩存
Flutter框架對(duì)加載過(guò)的圖片是有緩存的(內(nèi)存)钓简,默認(rèn)最大緩存數(shù)量是1000会宪,最大緩存空間為100M伴澄。
ImageProvider主要負(fù)責(zé)圖片數(shù)據(jù)的加載和緩存,而繪制部分邏輯主要是由RawImage來(lái)完成奕短。
Image是連接起ImageProvider和RawImage的橋梁古掏。
ImageProvider
Image組件的image參數(shù)是一個(gè)ImageProvider類型的必選參數(shù)损话。
ImageProvider是一個(gè)抽象類,定義了圖片數(shù)據(jù)獲取槽唾、加載丧枪、緩存的相關(guān)接口。
abstract class ImageProvider<T> {
ImageStream resolve(ImageConfiguration configuration) {
...
}
Future<bool> evict({ ImageCache cache,
ImageConfiguration configuration = ImageConfiguration.empty }) async {
...
}1
Future<T> obtainKey(ImageConfiguration configuration);
@protected
ImageStreamCompleter load(T key); // 需子類實(shí)現(xiàn)
}
- load(T key)方法
加載圖片數(shù)據(jù)源的接口夏漱,不同的數(shù)據(jù)源的加載方法不同豪诲,每個(gè)ImageProvider的子類必須實(shí)現(xiàn)它。
比如NetworkImage類和AssetImage類挂绰,它們都是ImageProvider的子類屎篱,但它們需要從不同的數(shù)據(jù)源來(lái)加載圖片數(shù)據(jù):NetworkImage是從網(wǎng)絡(luò)來(lái)加載圖片數(shù)據(jù)服赎,而AssetImage則是從應(yīng)用安裝包來(lái)加載圖片數(shù)據(jù)。
以NetworkImage為例交播,看看其load方法的實(shí)現(xiàn):
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents), // 調(diào)用
chunkEvents: chunkEvents.stream,
scale: key.scale,
... // 省略無(wú)關(guān)代碼
);
}
load方法的返回值類型是ImageStreamCompleter 重虑,它是一個(gè)抽象類,定義了管理圖片加載過(guò)程的一些接口秦士,Image Widget中正是通過(guò)它來(lái)監(jiān)聽(tīng)圖片加載狀態(tài)的缺厉。MultiFrameImageStreamCompleter 是 ImageStreamCompleter的一個(gè)子類,是flutter sdk預(yù)置的類隧土,通過(guò)該類可以方便提针、輕松地創(chuàng)建出一個(gè)ImageStreamCompleter實(shí)例來(lái)做為load方法的返回值。
MultiFrameImageStreamCompleter 需要一個(gè)codec參數(shù)曹傀,該參數(shù)類型為Future<ui.Codec>辐脖。Codec 是處理圖片編解碼的類的一個(gè)handler,實(shí)際上它只是一個(gè)flutter engine API 的包裝類皆愉,也就是說(shuō)圖片的編解碼邏輯不是在Dart 代碼部分實(shí)現(xiàn)嗜价,而是在flutter engine中實(shí)現(xiàn)的。
==========================================
==========================================
Codec類部分定義如下:
@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
// 此類由flutter engine創(chuàng)建幕庐,不應(yīng)該手動(dòng)實(shí)例化此類或直接繼承此類久锥。
@pragma('vm:entry-point')
Codec._();
/// 圖片中的幀數(shù)(動(dòng)態(tài)圖會(huì)有多幀)
int get frameCount native 'Codec_frameCount';
/// 動(dòng)畫(huà)重復(fù)的次數(shù)
/// * 0 表示只執(zhí)行一次
/// * -1 表示循環(huán)執(zhí)行
int get repetitionCount native 'Codec_repetitionCount';
/// 獲取下一個(gè)動(dòng)畫(huà)幀
Future<FrameInfo> getNextFrame() {
return _futurize(_getNextFrame);
}
// Codec最終的結(jié)果是一個(gè)或多個(gè)(動(dòng)圖)幀,而這些幀最終會(huì)繪制到屏幕上异剥。
String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';
==========================================
==========================================
MultiFrameImageStreamCompleter 的 codec參數(shù)值為_(kāi)loadAsync方法的返回值瑟由,_loadAsync方法的實(shí)現(xiàn):
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
) async {
try {
// 下載圖片
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw Exception(...);
// 接收?qǐng)D片數(shù)據(jù)
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
// 對(duì)圖片數(shù)據(jù)進(jìn)行解碼
return PaintingBinding.instance.instantiateImageCodec(bytes);
} finally {
chunkEvents.close();
}
}
可以看到_loadAsync方法主要做了兩件事:
1. 下載圖片。通過(guò)HttpClient從網(wǎng)上下載圖片届吁,下載請(qǐng)求會(huì)設(shè)置一些自定義的header错妖,開(kāi)發(fā)者可以通過(guò)NetworkImage的headers命名參數(shù)來(lái)傳遞绿鸣。
2. 對(duì)下載的圖片數(shù)據(jù)進(jìn)行解碼疚沐。在圖片下載完成后調(diào)用了PaintingBinding.instance.instantiateImageCodec(bytes)對(duì)圖片進(jìn)行解碼,值得注意的是instantiateImageCodec(...)也是一個(gè)Native API的包裝潮模,實(shí)際上會(huì)調(diào)用Flutter engine的instantiateImageCodec方法亮蛔,源碼如下:
String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight) native 'instantiateImageCodec';
- obtainKey(ImageConfiguration)方法
配合實(shí)現(xiàn)圖片緩存,ImageProvider從數(shù)據(jù)源加載完數(shù)據(jù)后擎厢,會(huì)在全局的ImageCache中緩存圖片數(shù)據(jù)究流,而圖片數(shù)據(jù)緩存是一個(gè)Map,而Map的key便是調(diào)用此方法的返回值动遭,不同的key代表不同的圖片數(shù)據(jù)緩存芬探。
以NetworkImage為例,看一下它的obtainKey()實(shí)現(xiàn):
@override
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
該方法創(chuàng)建了一個(gè)同步的future厘惦,然后直接將自身做為key返回偷仿。因?yàn)镸ap中在判斷key(此時(shí)是NetworkImage對(duì)象)是否相等時(shí)會(huì)使用“==”運(yùn)算符,那么定義key的邏輯就是NetworkImage的“==”運(yùn)算符:
@override
bool operator ==(dynamic other) {
... // 省略無(wú)關(guān)代碼
final NetworkImage typedOther = other;
return url == typedOther.url
&& scale == typedOther.scale;
}
對(duì)于網(wǎng)絡(luò)圖片來(lái)說(shuō),會(huì)將其“url地址+scale縮放比例”作為緩存的key酝静。也就是說(shuō)如果兩張圖片的url或scale只要有一個(gè)不同节榜,便會(huì)重新下載并分別緩存。
需要注意的是别智,圖片緩存是在內(nèi)存中宗苍,并沒(méi)有進(jìn)行本地文件持久化存儲(chǔ),這也是為什么網(wǎng)絡(luò)圖片在應(yīng)用重啟后需要重新聯(lián)網(wǎng)下載的原因薄榛。同時(shí)也意味著在應(yīng)用生命周期內(nèi)讳窟,如果緩存沒(méi)有超過(guò)上限,相同的圖片只會(huì)被下載一次敞恋。
- resolve(ImageConfiguration) 方法
resolve方法是ImageProvider暴露給Image的主入口方法挪钓,接受一個(gè)ImageConfiguration參數(shù),返回ImageStream圖片數(shù)據(jù)流耳舅。
ImageStream resolve(ImageConfiguration configuration) {
... // 省略無(wú)關(guān)代碼
final ImageStream stream = ImageStream();
T obtainedKey; //
// 定義錯(cuò)誤處理函數(shù)
Future<void> handleError(dynamic exception, StackTrace stack) async {
... // 省略無(wú)關(guān)代碼
stream.setCompleter(imageCompleter);
imageCompleter.setError(...);
}
// 創(chuàng)建一個(gè)新Zone碌上,主要是為了當(dāng)發(fā)生錯(cuò)誤時(shí)不會(huì)干擾MainZone
final Zone dangerZone = Zone.current.fork(...);
dangerZone.runGuarded(() {
Future<T> key;
// 先驗(yàn)證是否已經(jīng)有緩存
try {
// 生成緩存key,后面會(huì)根據(jù)此key來(lái)檢測(cè)是否有緩存
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
obtainedKey = key;
// 處理緩存浦徊,這里的PaintingBinding.instance.imageCache 是 ImageCache的一個(gè)實(shí)例馏予,它是PaintingBinding的一個(gè)屬性,而Flutter框架中的PaintingBinding.instance是一個(gè)單例盔性,imageCache事實(shí)上也是一個(gè)單例霞丧,也就是說(shuō)圖片緩存是全局的,統(tǒng)一由PaintingBinding.instance.imageCache 來(lái)管理冕香。
final ImageStreamCompleter completer = PaintingBinding.instance
.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
if (completer != null) {
stream.setCompleter(completer);
}
}).catchError(handleError);
});
return stream;
}
==========================================
==========================================
ImageConfiguration包含了圖片和設(shè)備的相關(guān)信息蛹尝,如圖片的大小、所在的AssetBundle(只有打到安裝包的圖片存在)以及當(dāng)前的設(shè)備平臺(tái)悉尾、devicePixelRatio(設(shè)備像素比)突那。
Flutter SDK提供了一個(gè)便捷函數(shù)createLocalImageConfiguration來(lái)創(chuàng)建ImageConfiguration 對(duì)象:
ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size size }) {
return ImageConfiguration(
bundle: DefaultAssetBundle.of(context),
devicePixelRatio: MediaQuery.of(context, nullOk: true)?.devicePixelRatio ?? 1.0,
locale: Localizations.localeOf(context, nullOk: true),
textDirection: Directionality.of(context),
size: size,
platform: defaultTargetPlatform,
);
}
這些信息基本都是通過(guò)Context來(lái)獲取。
==========================================
==========================================
ImageCache類定義:
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache {
// 正在加載中的圖片隊(duì)列
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 緩存隊(duì)列
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 緩存數(shù)量上限(1000)
int _maximumSize = _kDefaultSize;
// 緩存容量上限 (100 MB)
int _maximumSizeBytes = _kDefaultSizeBytes;
// 緩存上限設(shè)置的setter
set maximumSize(int value) {...}
set maximumSizeBytes(int value) {...}
... // 省略部分定義
// 清除所有緩存
void clear() {
// ...省略具體實(shí)現(xiàn)代碼
}
// 清除指定key對(duì)應(yīng)的圖片緩存
bool evict(Object key) {
// ...省略具體實(shí)現(xiàn)代碼
}
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
assert(key != null);
assert(loader != null);
ImageStreamCompleter result = _pendingImages[key]?.completer;
// 圖片還未加載成功构眯,直接返回
if (result != null)
return result;
// 1. 先判斷圖片數(shù)據(jù)有沒(méi)有緩存愕难,如果有則先移除緩存后再添加(可以讓最新使用過(guò)的緩存在_map中的位置更近一些),并返回ImageStream惫霸。
final _CachedImage image = _cache.remove(key);
if (image != null) {
_cache[key] = image;
return image.completer;
}
// 2. 如果沒(méi)有緩存猫缭,則調(diào)用load(T key)方法從數(shù)據(jù)源加載圖片數(shù)據(jù),加載成功后先緩存壹店,然后返回ImageStream猜丹。
try {
result = loader();
} catch (error, stackTrace) {
if (onError != null) {
onError(error, stackTrace);
return null;
} else {
rethrow;
}
}
void listener(ImageInfo info, bool syncCall) {
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize);
// 下面是緩存處理的邏輯
if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
_maximumSizeBytes = imageSize + 1000;
}
_currentSizeBytes += imageSize;
final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
_cache[key] = image;
_checkCacheSize();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
}
return result;
}
// 當(dāng)緩存數(shù)量超過(guò)最大值或緩存的大小超過(guò)最大緩存容量,會(huì)調(diào)用此方法清理到緩存上限以內(nèi)
void _checkCacheSize() {
while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
final Object key = _cache.keys.first;
final _CachedImage image = _cache[key];
_currentSizeBytes -= image.sizeBytes;
_cache.remove(key);
}
... //省略無(wú)關(guān)代碼
}
}
可以自定義緩存上限:
PaintingBinding.instance.imageCache.maximumSize=2000; // 最多2000張
PaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; // 最大200M
因?yàn)镸ap中相同key的值會(huì)被覆蓋硅卢,也就是說(shuō)key是圖片緩存的一個(gè)唯一標(biāo)識(shí)射窒,只要是不同key妖混,那么圖片數(shù)據(jù)就會(huì)分別緩存(即使事實(shí)上是同一張圖片)。key是ImageProvider.obtainKey()方法的返回值轮洋,此方法需要ImageProvider子類去重寫(xiě)制市,這也就意味著不同的ImageProvider對(duì)key的定義邏輯會(huì)不同。比如對(duì)于NetworkImage弊予,將圖片的url和scale作為key會(huì)很合適祥楣,而對(duì)于AssetImage則應(yīng)該將“包名+路徑”作為唯一的key。
Image組件原理
通過(guò)實(shí)現(xiàn)一個(gè)“簡(jiǎn)版的Image組件”汉柒,來(lái)大致了解Image組件原理误褪。
代碼流程如下:
1. 通過(guò)imageProvider.resolve方法可以得到一個(gè)ImageStream(圖片數(shù)據(jù)流),然后監(jiān)聽(tīng)I(yíng)mageStream的變化碾褂。當(dāng)圖片數(shù)據(jù)源發(fā)生變化時(shí)兽间,ImageStream會(huì)觸發(fā)相應(yīng)的事件,而本例中只設(shè)置了圖片成功的監(jiān)聽(tīng)器_updateImage正塌,而_updateImage中只更新了_imageInfo嘀略。值得注意的是,如果是靜態(tài)圖乓诽,ImageStream只會(huì)觸發(fā)一次時(shí)間帜羊,如果是動(dòng)態(tài)圖,則會(huì)觸發(fā)多次事件鸠天,每一次都會(huì)有一個(gè)解碼后的圖片幀讼育。
2. _imageInfo 更新后會(huì)rebuild,此時(shí)會(huì)創(chuàng)建一個(gè)RawImage Widget稠集。RawImage最終會(huì)通過(guò)RenderImage來(lái)將圖片繪制在屏幕上奶段。如果繼續(xù)跟進(jìn)RenderImage類,會(huì)發(fā)現(xiàn)RenderImage的paint 方法中調(diào)用了paintImage方法剥纷,而paintImage方法中通過(guò)Canvas的drawImageRect(…)痹籍、drawImageNine(...)等方法來(lái)完成最終的繪制。
class MyImage extends StatefulWidget {
final ImageProvider imageProvider;
const MyImage({
Key key,
@required this.imageProvider,
})
: assert(imageProvider != null),
super(key: key);
@override
_MyImageState createState() => _MyImageState();
}
class _MyImageState extends State<MyImage> {
ImageStream _imageStream;
ImageInfo _imageInfo;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 依賴改變時(shí)筷畦,圖片的配置信息可能會(huì)發(fā)生改變
_getImage();
}
@override
void didUpdateWidget(MyImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.imageProvider != oldWidget.imageProvider)
_getImage();
}
void _getImage() {
final ImageStream oldImageStream = _imageStream;
// 調(diào)用imageProvider.resolve方法词裤,獲得ImageStream刺洒。
_imageStream =
widget.imageProvider.resolve(createLocalImageConfiguration(context));
// 判斷新舊ImageStream是否相同鳖宾,如果不同,則需要調(diào)整流的監(jiān)聽(tīng)器
if (_imageStream.key != oldImageStream?.key) {
final ImageStreamListener listener = ImageStreamListener(_updateImage);
oldImageStream?.removeListener(listener);
_imageStream.addListener(listener);
}
}
void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
});
}
@override
void dispose() {
_imageStream.removeListener(ImageStreamListener(_updateImage));
super.dispose();
}
@override
Widget build(BuildContext context) {
return RawImage( // dart:ui庫(kù)
image: _imageInfo?.image,
scale: _imageInfo?.scale ?? 1.0,
);
}
}
測(cè)試一下MyImage組件
class ImageInternalTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
MyImage(
imageProvider: NetworkImage(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
),
)
],
);
}
}
5. 布局過(guò)程
布局過(guò)程主要是確定每一個(gè)組件的布局信息(大小和位置)
Flutter 的布局過(guò)程如下:
1. 父節(jié)點(diǎn)向子節(jié)點(diǎn)傳遞約束(constraints)信息逆航,限制子節(jié)點(diǎn)的最大和最小寬高鼎文。
2. 子節(jié)點(diǎn)根據(jù)約束信息確定自己的大小(size)因俐。
3. 父節(jié)點(diǎn)根據(jù)特定布局規(guī)則(不同布局組件會(huì)有不同的布局算法)確定每一個(gè)子節(jié)點(diǎn)在父節(jié)點(diǎn)布局空間中的位置拇惋,用偏移 offset 表示周偎。
4. 遞歸整個(gè)過(guò)程,確定出每一個(gè)節(jié)點(diǎn)的大小和位置撑帖。
可以看到蓉坎,組件的大小是由自身決定的,而組件的位置是由父組件決定的胡嘿。
示例(SingleChildRenderObjectWidget)
Constraints
在RenderBox 中蛉艾,有個(gè)size屬性用來(lái)保存控件的寬和高。RenderBox的layout是通過(guò)在組件樹(shù)中從上往下傳遞BoxConstraints對(duì)象的實(shí)現(xiàn)的衷敌。BoxConstraints對(duì)象可以限制子節(jié)點(diǎn)的最大和最小寬高勿侯,子節(jié)點(diǎn)必須遵守父節(jié)點(diǎn)給定的限制條件。
在布局階段缴罗,父節(jié)點(diǎn)會(huì)調(diào)用子節(jié)點(diǎn)的layout()方法助琐。
RenderObject中l(wèi)ayout()方法的大致實(shí)現(xiàn):
void layout(Constraints constraints, { bool parentUsesSize = false }) {
...
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight
|| parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
...
if (sizedByParent) {
performResize();
}
performLayout();
...
}
layout方法需要傳入兩個(gè)參數(shù),第一個(gè)為constraints面氓,即 父節(jié)點(diǎn)對(duì)子節(jié)點(diǎn)大小的限制兵钮,該值根據(jù)父節(jié)點(diǎn)的布局邏輯確定。另外一個(gè)參數(shù)是 parentUsesSize舌界,該值用于確定 relayoutBoundary矢空,該參數(shù)表示子節(jié)點(diǎn)布局變化是否影響父節(jié)點(diǎn),如果為true禀横,當(dāng)子節(jié)點(diǎn)布局發(fā)生變化時(shí)父節(jié)點(diǎn)都會(huì)標(biāo)記為需要重新布局屁药,如果為false,則子節(jié)點(diǎn)布局發(fā)生變化后不會(huì)影響父節(jié)點(diǎn)柏锄。
relayoutBoundary
上面layout()源碼中定義了一個(gè)relayoutBoundary變量,
當(dāng)一個(gè)Element標(biāo)記為 dirty(通過(guò)調(diào)用 markNeedsBuild() 方法)時(shí)便會(huì)重新build酿箭,這時(shí)RenderObject便會(huì)重新布局。在RenderObject中有一個(gè)類似的markNeedsLayout()方法趾娃,它會(huì)將RenderObject的布局狀態(tài)標(biāo)記為 dirty缭嫡,這樣在下一個(gè)frame中便會(huì)重新layout,
RenderObject的markNeedsLayout()的部分源碼:
void markNeedsLayout() {
...
assert(_relayoutBoundary != null);
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
...
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
代碼大致邏輯是先判斷自身是不是relayoutBoundary抬闷,如果不是就繼續(xù)向parent 查找妇蛀,一直向上查找到是 relayoutBoundary 的 RenderObject為止,然后再將其標(biāo)記為 dirty 的笤成。這樣來(lái)看它的作用就比較明顯了评架,意思就是當(dāng)一個(gè)控件的大小被改變時(shí)可能會(huì)影響到它的 parent,因此 parent 也需要被重新布局炕泳,那么到什么時(shí)候是個(gè)頭呢纵诞?答案就是 relayoutBoundary,如果一個(gè) RenderObject 是 relayoutBoundary培遵,就表示它的大小變化不會(huì)再影響到 parent 的大小了,于是 parent 也就不用重新布局了。
performResize 和 performLayout
RenderBox實(shí)際的測(cè)量和布局邏輯是在performResize() 和 performLayout()兩個(gè)方法中浪漠,RenderBox子類需要實(shí)現(xiàn)這兩個(gè)方法來(lái)定制自身的布局邏輯桐愉。
根據(jù)layout() 源碼可以看出只有 sizedByParent 為 true 時(shí),performResize() 才會(huì)被調(diào)用,而 performLayout() 是每次布局都會(huì)被調(diào)用的。sizedByParent 意為該節(jié)點(diǎn)的大小是否僅通過(guò) parent 傳給它的 constraints 就可以確定了,即該節(jié)點(diǎn)的大小與它自身的屬性和其子節(jié)點(diǎn)無(wú)關(guān)掉蔬,比如如果一個(gè)控件永遠(yuǎn)充滿 parent 的大小,那么 sizedByParent就應(yīng)該返回true矾瘾,此時(shí)其大小在 performResize() 中就確定了女轿,在后面的 performLayout() 方法中將不會(huì)再被修改了,這種情況下 performLayout() 只負(fù)責(zé)布局子節(jié)點(diǎn)壕翩。
在 performLayout() 方法中除了完成自身布局蛉迹,也必須完成子節(jié)點(diǎn)的布局,這是因?yàn)橹挥懈缸庸?jié)點(diǎn)全部完成后布局流程才算真正完成放妈。所以最終的調(diào)用棧將會(huì)變成:layout() > performResize()/performLayout() > child.layout() > ... 北救,如此遞歸完成整個(gè)UI的布局。
RenderBox子類要定制布局算法不應(yīng)該重寫(xiě)layout()方法芜抒,因?yàn)閷?duì)于任何RenderBox的子類來(lái)說(shuō)珍策,它的layout流程基本是相同的,不同之處只在具體的布局算法宅倒,而具體的布局算法子類應(yīng)該通過(guò)重寫(xiě)performResize() 和 performLayout()兩個(gè)方法來(lái)實(shí)現(xiàn)攘宙,他們會(huì)在layout()中被調(diào)用。
ParentData
RenderObject的parentData 只能通過(guò)父元素設(shè)置.
當(dāng)layout結(jié)束后拐迁,每個(gè)節(jié)點(diǎn)的位置(相對(duì)于父節(jié)點(diǎn)的偏移)就已經(jīng)確定了蹭劈,RenderObject就可以根據(jù)位置信息來(lái)進(jìn)行最終的繪制。但是在layout過(guò)程中线召,節(jié)點(diǎn)的位置信息怎么保存铺韧?對(duì)于大多數(shù)RenderBox子類來(lái)說(shuō)如果子類只有一個(gè)子節(jié)點(diǎn),那么子節(jié)點(diǎn)偏移一般都是Offset.zero 缓淹,如果有多個(gè)子節(jié)點(diǎn)哈打,則每個(gè)子節(jié)點(diǎn)的偏移就可能不同。而子節(jié)點(diǎn)在父節(jié)點(diǎn)的偏移數(shù)據(jù)正是通過(guò)RenderObject的parentData屬性來(lái)保存的讯壶。在RenderBox中料仗,其parentData屬性默認(rèn)是一個(gè)BoxParentData對(duì)象,該屬性只能通過(guò)父節(jié)點(diǎn)的setupParentData()方法來(lái)設(shè)置:
abstract class RenderBox extends RenderObject {
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! BoxParentData)
child.parentData = BoxParentData();
}
...
}
BoxParentData定義如下:
/// Parentdata 會(huì)被RenderBox和它的子類使用.
class BoxParentData extends ParentData {
/// offset表示在子節(jié)點(diǎn)在父節(jié)點(diǎn)坐標(biāo)系中的繪制偏移
Offset offset = Offset.zero;
@override
String toString() => 'offset=$offset';
}
ParentData并不僅僅可以用來(lái)存儲(chǔ)偏移信息鹏溯,通常所有和子節(jié)點(diǎn)特定的數(shù)據(jù)都可以存儲(chǔ)到子節(jié)點(diǎn)的ParentData中罢维,如ContainerBox的ParentData就保存了指向兄弟節(jié)點(diǎn)的previousSibling和nextSibling,Element.visitChildren()方法也正是通過(guò)它們來(lái)實(shí)現(xiàn)對(duì)子節(jié)點(diǎn)的遍歷丙挽。再比如KeepAlive 組件肺孵,它使用KeepAliveParentDataMixin(繼承自ParentData) 來(lái)保存子節(jié)的keepAlive狀態(tài)。
- 繪制過(guò)程
RenderObject可以通過(guò)paint()方法來(lái)完成具體繪制邏輯颜阐,流程和布局流程相似平窘,子類可以實(shí)現(xiàn)paint()方法來(lái)完成自身的繪制邏輯,paint()簽名如下:
void paint(PaintingContext context, Offset offset) { }
通過(guò)context.canvas可以取到Canvas對(duì)象,就可以調(diào)用Canvas API來(lái)實(shí)現(xiàn)具體的繪制邏輯
如果節(jié)點(diǎn)有子節(jié)點(diǎn)凳怨,它除了完成自身繪制邏輯之外瑰艘,還要調(diào)用子節(jié)點(diǎn)的繪制方法。例:
@override
void paint(PaintingContext context, Offset offset) {
// 如果子元素未超出當(dāng)前邊界肤舞,則繪制子元素
if (_overflow <= 0.0) {
defaultPaint(context, offset);
return;
}
// 如果size為空紫新,則無(wú)需繪制
if (size.isEmpty)
return;
// 剪裁掉溢出邊界的部分
context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint);
assert(() {
final String debugOverflowHints = '...'; //溢出提示內(nèi)容,省略
// 繪制溢出部分的錯(cuò)誤提示樣式
Rect overflowChildRect;
switch (_direction) {
case Axis.horizontal:
overflowChildRect = Rect.fromLTWH(0.0, 0.0, size.width + _overflow, 0.0);
break;
case Axis.vertical:
overflowChildRect = Rect.fromLTWH(0.0, 0.0, 0.0, size.height + _overflow);
break;
}
paintOverflowIndicator(context, offset, Offset.zero & size,
overflowChildRect, overflowHints: debugOverflowHints);
return true;
}());
}
代碼很簡(jiǎn)單李剖,首先判斷有無(wú)溢出芒率,如果沒(méi)有則調(diào)用defaultPaint(context, offset)來(lái)完成繪制,該方法源碼如下:
void defaultPaint(PaintingContext context, Offset offset) {
ChildType child = firstChild;
while (child != null) {
final ParentDataType childParentData = child.parentData;
//繪制子節(jié)點(diǎn)篙顺,
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
很明顯偶芍,由于Flex本身沒(méi)有需要繪制的東西,所以直接遍歷其子節(jié)點(diǎn)德玫,然后調(diào)用paintChild()來(lái)繪制子節(jié)點(diǎn)匪蟀,同時(shí)將子節(jié)點(diǎn)ParentData中在layout階段保存的offset加上自身偏移作為第二個(gè)參數(shù)傳遞給paintChild()。而如果子節(jié)點(diǎn)還有子節(jié)點(diǎn)時(shí)宰僧,paintChild()方法還會(huì)調(diào)用子節(jié)點(diǎn)的paint()方法材彪,如此遞歸完成整個(gè)節(jié)點(diǎn)樹(shù)的繪制,最終調(diào)用棧為: paint() > paintChild() > paint() ... 琴儿。
當(dāng)需要繪制的內(nèi)容大小溢出當(dāng)前空間時(shí)查刻,將會(huì)執(zhí)行paintOverflowIndicator() 來(lái)繪制溢出部分提示,這個(gè)就是我們經(jīng)撤锢啵看到的溢出提示
RepaintBoundary
與 RelayoutBoundary 相似穗泵,RepaintBoundary是用于在確定重繪邊界的,與RelayoutBoundary不同的是谜疤,這個(gè)繪制邊界需要由開(kāi)發(fā)者通過(guò)RepaintBoundary 組件自己指定佃延,如:
CustomPaint(
size: Size(300, 300), //指定畫(huà)布大小
painter: MyPainter(),
child: RepaintBoundary(
child: Container(...),
),
),
RenderObject有一個(gè)isRepaintBoundary屬性,該屬性決定這個(gè)RenderObject重繪時(shí)是否獨(dú)立于其父元素夷磕,如果該屬性值為true 履肃,則獨(dú)立繪制,反之則一起繪制坐桩。
獨(dú)立繪制是怎么實(shí)現(xiàn)的,答案就在paintChild()源碼中:
void paintChild(RenderObject child, Offset offset) {
...
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
...
}
可以看到尺棋,在繪制子節(jié)點(diǎn)時(shí),如果child.isRepaintBoundary 為 true則會(huì)調(diào)用_compositeChild()方法绵跷,_compositeChild()源碼如下:
void _compositeChild(RenderObject child, Offset offset) {
// 給子節(jié)點(diǎn)創(chuàng)建一個(gè)layer 膘螟,然后再上面繪制子節(jié)點(diǎn)
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
...
}
assert(child._layer != null);
child._layer.offset = offset;
appendLayer(child._layer);
}
獨(dú)立繪制是通過(guò)在不同的layer(層)上繪制的成福。所以,很明顯荆残,正確使用isRepaintBoundary屬性可以提高繪制效率奴艾,避免不必要的重繪。具體原理是:和觸發(fā)重新build和layout類似内斯,RenderObject也提供了一個(gè)markNeedsPaint()方法蕴潦,其源碼如下:
void markNeedsPaint() {
...
//如果RenderObject.isRepaintBoundary 為true,則該RenderObject擁有l(wèi)ayer,直接繪制
if (isRepaintBoundary) {
...
if (owner != null) {
//找到最近的layer俘闯,繪制
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {
// 沒(méi)有自己的layer, 會(huì)和一個(gè)祖先節(jié)點(diǎn)共用一個(gè)layer
assert(_layer == null);
final RenderObject parent = this.parent;
// 向父級(jí)遞歸查找
parent.markNeedsPaint();
assert(parent == this.parent);
} else {
// 如果直到根節(jié)點(diǎn)也沒(méi)找到一個(gè)Layer潭苞,那么便需要繪制自身,因?yàn)闆](méi)有其它節(jié)點(diǎn)可以繪制根節(jié)點(diǎn)蜜猾。
if (owner != null)
owner.requestVisualUpdate();
}
}
可以看出,當(dāng)調(diào)用 markNeedsPaint() 方法時(shí)缕坎,會(huì)從當(dāng)前 RenderObject 開(kāi)始一直向父節(jié)點(diǎn)查找急凰,直到找到 一個(gè)isRepaintBoundary 為 true的RenderObject 時(shí)袁稽,才會(huì)觸發(fā)重繪锹杈,這樣便可以實(shí)現(xiàn)局部重繪。當(dāng) 有RenderObject 繪制的很頻繁或很復(fù)雜時(shí)掘剪,可以通過(guò)RepaintBoundary Widget來(lái)指定isRepaintBoundary 為 true岗照,這樣在繪制時(shí)僅會(huì)重繪自身而無(wú)需重繪它的 parent库菲,如此便可提高性能。
還有一個(gè)問(wèn)題志膀,通過(guò)RepaintBoundary 如何設(shè)置isRepaintBoundary屬性呢熙宇?其實(shí),如果使用了RepaintBoundary溉浙,其對(duì)應(yīng)的RenderRepaintBoundary會(huì)自動(dòng)將isRepaintBoundary設(shè)為true的:
class RenderRepaintBoundary extends RenderProxyBox {
/// Creates a repaint boundary around [child].
RenderRepaintBoundary({ RenderBox child }) : super(child);
@override
bool get isRepaintBoundary => true;
}
- 命中測(cè)試
一個(gè)對(duì)象是否可以響應(yīng)事件烫止,取決于其對(duì)命中測(cè)試的返回,當(dāng)發(fā)生用戶事件時(shí)戳稽,會(huì)從根節(jié)點(diǎn)(RenderView)開(kāi)始進(jìn)行命中測(cè)試
RenderView的hitTest()源碼:
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(result, position: position); // 遞歸子RenderBox進(jìn)行命中測(cè)試
result.add(HitTestEntry(this)); // 將測(cè)試結(jié)果添加到result中
return true;
}
RenderBox默認(rèn)的hitTest()實(shí)現(xiàn):
bool hitTest(HitTestResult result, { @required Offset position }) {
...
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
默認(rèn)的實(shí)現(xiàn)里調(diào)用了hitTestSelf()和hitTestChildren()兩個(gè)方法馆蠕,這兩個(gè)方法默認(rèn)實(shí)現(xiàn)如下:
@protected
bool hitTestSelf(Offset position) => false;
@protected
bool hitTestChildren(HitTestResult result, { Offset position }) => false;
hitTest 方法用來(lái)判斷該RenderObject 是否在被點(diǎn)擊的范圍內(nèi),同時(shí)負(fù)責(zé)將被點(diǎn)擊的 RenderBox 添加到 HitTestResult 列表中惊奇,參數(shù) position 為事件觸發(fā)的坐標(biāo)(如果有的話)互躬,返回 true 則表示有RenderBox 通過(guò)了命中測(cè)試,需要響應(yīng)事件颂郎,反之則認(rèn)為當(dāng)前RenderBox沒(méi)有命中吼渡。在繼承RenderBox時(shí),可以直接重寫(xiě)hitTest()方法祖秒,也可以重寫(xiě) hitTestSelf() 或 hitTestChildren(), 唯一不同的是 hitTest()中需要將通過(guò)命中測(cè)試的節(jié)點(diǎn)信息添加到命中測(cè)試結(jié)果列表中诞吱,而 hitTestSelf() 和 hitTestChildren()則只需要簡(jiǎn)單的返回true或false。
- Semantics語(yǔ)義化
語(yǔ)義化竭缝,主要是提供給讀屏軟件的接口房维,也是實(shí)現(xiàn)輔助功能的基礎(chǔ),通過(guò)語(yǔ)義化接口可以讓機(jī)器理解頁(yè)面上的內(nèi)容抬纸,對(duì)于有視力障礙用戶可以使用讀屏軟件來(lái)理解UI內(nèi)容咙俩。
如果一個(gè)RenderObject要支持語(yǔ)義化接口,可以實(shí)現(xiàn) describeApproximatePaintClip和 visitChildrenForSemantics方法和semanticsAnnotator getter湿故。
6. 繪制
Flutter 繪制原理
繪制相關(guān)的對(duì)象:
1. Canvas
封裝了Flutter Skia各種繪制指令阿趁,比如畫(huà)線、畫(huà)圓坛猪、畫(huà)矩形等指令脖阵。
2. Layer
分為容器類和繪制類兩種;暫時(shí)可以理解為是繪制產(chǎn)物的載體墅茉,比如調(diào)用 Canvas 的繪制 API 后命黔,相應(yīng)的繪制產(chǎn)物被保存在 PictureLayer.picture 對(duì)象中呜呐。
3. Scene
屏幕上將要要顯示的元素。在上屏前悍募,需要將Layer中保存的繪制產(chǎn)物關(guān)聯(lián)到 Scene 上蘑辑。
繪制流程:
1. 構(gòu)建一個(gè) Canvas,用于繪制坠宴;同時(shí)還需要?jiǎng)?chuàng)建一個(gè)繪制指令記錄器洋魂,因?yàn)槔L制指令最終是要傳遞給 Skia 的,而 Canvas 可能會(huì)連續(xù)發(fā)起多條繪制指令喜鼓,指令記錄器用于收集 Canvas 在一段時(shí)間內(nèi)所有的繪制指令副砍,因此Canvas 構(gòu)造函數(shù)第一個(gè)參數(shù)必須傳遞一個(gè) PictureRecorder 實(shí)例。
2. Canvas 繪制完成后颠通,通過(guò) PictureRecorder 獲取繪制產(chǎn)物硼控,然后將其保存在 Layer 中捍掺。
3. 構(gòu)建 Scene 對(duì)象禾嫉,將 layer 的繪制產(chǎn)物和 Scene 關(guān)聯(lián)起來(lái)讲竿。
4. 上屏秀仲;調(diào)用window.render API 將Scene上的繪制產(chǎn)物發(fā)送給GPU。
示例(五子棋)
void main() {
// 1.創(chuàng)建繪制記錄器和Canvas
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
// 2.在指定位置區(qū)域繪制胁赢。
var rect = Rect.fromLTWH(30, 200, 300,300 );
drawChessboard(canvas,rect); //畫(huà)棋盤(pán)
drawPieces(canvas,rect);//畫(huà)棋子
// 3.創(chuàng)建layer,將繪制的產(chǎn)物保存在layer中
var pictureLayer = PictureLayer(rect);
//recorder.endRecording()獲取繪制產(chǎn)物纵穿。
pictureLayer.picture = recorder.endRecording();
var rootLayer = OffsetLayer();
rootLayer.append(pictureLayer);
// 4.上屏拷淘,將繪制的內(nèi)容顯示在屏幕上。
final SceneBuilder builder = SceneBuilder();
final Scene scene = rootLayer.buildScene(builder);
window.render(scene);
}
Picture
PictureLayer 的繪制產(chǎn)物是 Picture恃轩。
1. Picture 實(shí)際上是一系列的圖形繪制操作指令结洼。
2. Picture 要顯示在屏幕上,必然會(huì)經(jīng)過(guò)光柵化叉跛,隨后Flutter會(huì)將光柵化后的位圖信息緩存起來(lái)松忍,也就是說(shuō)同一個(gè) Picture 對(duì)象,其繪制指令只會(huì)執(zhí)行一次筷厘,執(zhí)行完成后繪制的位圖就會(huì)被緩存起來(lái)鸣峭。
綜合以上兩點(diǎn),可以看到 PictureLayer 的“繪制產(chǎn)物”一開(kāi)始是一些列“繪圖指令”敞掘,當(dāng)?shù)谝淮卫L制完成后叽掘,位圖信息就會(huì)被緩存,繪制指令也就不會(huì)再被執(zhí)行了玖雁,所以這時(shí)“繪制產(chǎn)物”就是位圖了。Picture有一個(gè)toImage方法盖腕,可以根據(jù)指定的大小導(dǎo)出Image赫冬。
// 將圖片導(dǎo)出為Uint8List
final Image image = await pictureLayer.picture.toImage();
final ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
final Uint8List pngBytes = byteData!.buffer.asUint8List();
print(pngBytes);
Layer
思考一個(gè)問(wèn)題:Layer作為繪制產(chǎn)物的持有者有什么作用? 答案就是:
1. 可以在不同的frame之間復(fù)用繪制產(chǎn)物(如果沒(méi)有發(fā)生變化)溃列。
2. 劃分繪制邊界劲厌,縮小重繪范圍。
Layer類型
在五子棋示例中定義了兩個(gè)Layer對(duì)象:
1. OffsetLayer
根 Layer听隐,它繼承自ContainerLayer补鼻,而ContainerLayer繼承自 Layer 類,我們將直接繼承自ContainerLayer 類的 Layer 稱為容器類Layer雅任,容器類 Layer 可以添加任意多個(gè)子Layer风范。
2. PictureLayer
保存繪制產(chǎn)物的 Layer,它直接繼承自 Layer 類沪么。我們將可以直接承載(或關(guān)聯(lián))繪制結(jié)果的 Layer 稱為繪制類 Layer硼婿。
容器類 Layer
作用和具體使用場(chǎng)景:
1. 將組件樹(shù)的繪制結(jié)構(gòu)組成一棵樹(shù)。
因?yàn)?Flutter 中的 Widget 是樹(shù)狀結(jié)構(gòu)禽车,那么相應(yīng)的 RenderObject 對(duì)應(yīng)的繪制結(jié)構(gòu)也應(yīng)該是樹(shù)狀結(jié)構(gòu)寇漫,F(xiàn)lutter 會(huì)根據(jù)一些“特定的規(guī)則”為組件樹(shù)生成一棵 Layer 樹(shù)刊殉,而容器類Layer就可以組成樹(shù)狀結(jié)構(gòu)肆捕。
2. 可以對(duì)多個(gè) layer 整體應(yīng)用一些變換效果己儒。
容器類 Layer 可以對(duì)其子 Layer 整體做一些變換效果,比如剪裁效果(ClipRectLayer瘦穆、ClipRRectLayer栓撞、ClipPathLayer)亚亲、過(guò)濾效果(ColorFilterLayer、ImageFilterLayer)腐缤、矩陣變換(TransformLayer)捌归、透明變換(OpacityLayer)等。
雖然 ContainerLayer 并非抽象類岭粤,開(kāi)發(fā)者可以直接創(chuàng)建 ContainerLayer 類的示例惜索,但實(shí)際上很少會(huì)這么做,相反剃浇,在需要使用使用 ContainerLayer 時(shí)直接使用其子類即可巾兆。如果我們確實(shí)不需要任何變換效果,那么就使用 OffsetLayer虎囚,不用擔(dān)心會(huì)有額外性能開(kāi)銷角塑,它的底層(Skia 中)實(shí)現(xiàn)是非常高效的。
繪制類 Layer
PictureLayer類是Flutter中最常用的一種繪制類Layer淘讥。
最終顯示在屏幕上的是位圖信息圃伶,而位圖信息正是由 Canvas API 繪制的。實(shí)際上蒲列,Canvas 的繪制產(chǎn)物是 Picture 對(duì)象表示窒朋,而當(dāng)前版本的 Flutter 中只有 PictureLayer 才擁有 picture 對(duì)象,換句話說(shuō)蝗岖,F(xiàn)lutter 中通過(guò)Canvas 繪制自身及其子節(jié)點(diǎn)的組件的繪制結(jié)果最終會(huì)落在 PictureLayer 中侥猩。
變換效果實(shí)現(xiàn)方式的選擇
ContainerLayer 可以對(duì)其子 layer 整體進(jìn)行一些變換,實(shí)際上抵赢,在大多數(shù)UI系統(tǒng)的 Canvas API 中也都有一些變換相關(guān)的 API 欺劳,那么也就意味著一些變換效果既可以通過(guò) ContainerLayer 來(lái)實(shí)現(xiàn),也可以通過(guò) Canvas 來(lái)實(shí)現(xiàn)铅鲤。比如划提,要實(shí)現(xiàn)平移變換,我們既可以使用 OffsetLayer 彩匕,也可以直接使用 Canva.translate API腔剂。既然如此,那我們選擇實(shí)現(xiàn)方式的原則是什么呢驼仪?
容器類 Layer的變換在底層是通過(guò) Skia 來(lái)實(shí)現(xiàn)的掸犬,不需要 Canvas 來(lái)處理袜漩。實(shí)現(xiàn)變換效果的具體原理是,有變換功能的容器類 Layer 會(huì)對(duì)應(yīng)一個(gè) Skia 引擎中的 Layer湾碎,為了和Flutter framework中 Layer 區(qū)分宙攻,flutter 中將 Skia 的Layer 稱為 engine layer。而有變換功能的容器類 Layer 在添加到 Scene 之前就會(huì)構(gòu)建一個(gè) engine layer介褥,我們以 OffsetLayer 為例座掘,看看其相關(guān)實(shí)現(xiàn):
@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
// 構(gòu)建 engine layer
engineLayer = builder.pushOffset(
layerOffset.dx + offset.dx,
layerOffset.dy + offset.dy,
oldLayer: _engineLayer as ui.OffsetEngineLayer?,
);
addChildrenToScene(builder);
builder.pop();
}
OffsetLayer 對(duì)其子節(jié)點(diǎn)整體做偏移變換的功能是 Skia 中實(shí)現(xiàn)支持的。Skia 可以支持多層渲染柔滔,但并不是層越多越好溢陪,engineLayer 是會(huì)占用一定的資源,F(xiàn)lutter 自帶組件庫(kù)中涉及到變換效果的都是優(yōu)先使用 Canvas 來(lái)實(shí)現(xiàn)睛廊,如果 Canvas 實(shí)現(xiàn)起來(lái)非常困難或?qū)崿F(xiàn)不了時(shí)才會(huì)用 ContainerLayer 來(lái)實(shí)現(xiàn)形真。
那么有什么場(chǎng)景下變換效果通過(guò) Canvas 實(shí)現(xiàn)起來(lái)會(huì)非常困難,需要用 ContainerLayer 來(lái)實(shí)現(xiàn) 超全?一個(gè)典型的場(chǎng)景是咆霜,我們需要對(duì)組件樹(shù)中的某個(gè)子樹(shù)整體做變換,且子樹(shù)中的有多個(gè) PictureLayer 時(shí)嘶朱。這是因?yàn)橐粋€(gè) Canvas 往往對(duì)應(yīng)一個(gè) PictureLayer蛾坯,不同 Canvas 之間相互隔離的,只有子樹(shù)中所有組件都通過(guò)同一個(gè) Canvas 繪制時(shí)才能通過(guò)該 Canvas 對(duì)所有子節(jié)點(diǎn)進(jìn)行整體變換疏遏,否則就只能通過(guò) ContainerLayer 脉课。
/*
Canvas對(duì)象中也有名為 ...layer 相關(guān)的 API,如 Canvas.saveLayer改览,它和本節(jié)介紹的Layer 含義不同下翎。Canvas對(duì)象中的 layer 主要是提供一種在繪制過(guò)程中緩存中間繪制結(jié)果的手段,為了在繪制復(fù)雜對(duì)象時(shí)方便多個(gè)繪制元素之間分離繪制而設(shè)計(jì)的
*/
組件樹(shù)繪制流程
繪制相關(guān)實(shí)現(xiàn)在渲染對(duì)象 RenderObject宝当,RenderObject 中和繪制相關(guān)的主要屬性有:
1. layer
2. isRepaintBoundary(類型bool)
將isRepaintBoundary值為 true的RenderObject節(jié)點(diǎn)稱為繪制邊界節(jié)點(diǎn)
3. needsCompositing (類型bool)
Flutter 自帶了一個(gè) RepaintBoundary 組件,它的功能其實(shí)就是向組件樹(shù)中插入一個(gè)繪制邊界節(jié)點(diǎn)胆萧。
Flutter繪制組件樹(shù)的大致流程(暫時(shí)忽略子樹(shù)中需要“層合成”):
Flutter第一次繪制時(shí)庆揩,會(huì)從上到下開(kāi)始遞歸的繪制子節(jié)點(diǎn),每當(dāng)遇到一個(gè)邊界節(jié)點(diǎn)跌穗,則判斷如果該邊界節(jié)點(diǎn)的 layer 屬性為空(類型為ContainerLayer)订晌,就會(huì)創(chuàng)建一個(gè)新的 OffsetLayer 并賦值給它;如果不為空蚌吸,則直接使用它锈拨。然后會(huì)將邊界節(jié)點(diǎn)的 layer 傳遞給子節(jié)點(diǎn),接下來(lái)有兩種情況:
1. 如果子節(jié)點(diǎn)是非邊界節(jié)點(diǎn)羹唠,且需要繪制奕枢,則會(huì)在第一次繪制時(shí):
1. 創(chuàng)建一個(gè)Canvas 對(duì)象和一個(gè) PictureLayer娄昆,然后將它們綁定,后續(xù)調(diào)用Canvas 繪制都會(huì)落到和其綁定的PictureLayer 上缝彬。
2. 接著將這個(gè) PictureLayer 加入到邊界節(jié)點(diǎn)的 layer 中萌焰。
2. 如果不是第一次繪制,則復(fù)用已有的 PictureLayer 和 Canvas 對(duì)象 谷浅。
3. 如果子節(jié)點(diǎn)是邊界節(jié)點(diǎn)扒俯,則對(duì)子節(jié)點(diǎn)遞歸上述過(guò)程。當(dāng)子樹(shù)的遞歸完成后一疯,就要將子節(jié)點(diǎn)的layer 添加到父級(jí) Layer中撼玄。
整個(gè)流程執(zhí)行完后就生成了一棵Layer樹(shù)。
1. RenderView 是 Flutter 應(yīng)用的根節(jié)點(diǎn)墩邀,繪制會(huì)從它開(kāi)始掌猛,因?yàn)樗且粋€(gè)繪制邊界節(jié)點(diǎn),在第一次繪制時(shí)磕蒲,會(huì)為他創(chuàng)建一個(gè) OffsetLayer留潦,我們記為 OffsetLayer1,接下來(lái) OffsetLayer1會(huì)傳遞給Row.
2. 由于 Row 是一個(gè)容器類組件且不需要繪制自身辣往,那么接下來(lái)他會(huì)繪制自己的孩子兔院,它有兩個(gè)孩子,先繪制第一個(gè)孩子Column1站削,將 OffsetLayer1 傳給 Column1坊萝,而 Column1 也不需要繪制自身,那么它又會(huì)將 OffsetLayer1 傳遞給第一個(gè)子節(jié)點(diǎn)Text1许起。
3. Text1 需要繪制文本十偶,他會(huì)使用 OffsetLayer1進(jìn)行繪制,由于 OffsetLayer1 是第一次繪制园细,所以會(huì)新建一個(gè)PictureLayer1和一個(gè) Canvas1 惦积,然后將 Canvas1 和PictureLayer1 綁定,接下來(lái)文本內(nèi)容通過(guò) Canvas1 對(duì)象繪制猛频,Text1 繪制完成后狮崩,Column1 又會(huì)將 OffsetLayer1 傳給 Text2 。
4. Text2 也需要使用 OffsetLayer1 繪制文本鹿寻,但是此時(shí) OffsetLayer1 已經(jīng)不是第一次繪制睦柴,所以會(huì)復(fù)用之前的 Canvas1 和 PictureLayer1,調(diào)用 Canvas1來(lái)繪制文本毡熏。
5. Column1 的子節(jié)點(diǎn)繪制完成后坦敌,PictureLayer1 上承載的是Text1 和 Text2 的繪制產(chǎn)物。
6. 接下來(lái) Row 完成了 Column1 的繪制后,開(kāi)始繪制第二個(gè)子節(jié)點(diǎn) RepaintBoundary狱窘,Row 會(huì)將 OffsetLayer1 傳遞給 RepaintBoundary杜顺,由于它是一個(gè)繪制邊界節(jié)點(diǎn),且是第一次繪制训柴,則會(huì)為它創(chuàng)建一個(gè) OffsetLayer2哑舒,接下來(lái) RepaintBoundary 會(huì)將 OffsetLayer2 傳遞給Column2,和 Column1 不同的是幻馁,Column2 會(huì)使用 OffsetLayer2 去繪制 Text3 和 Text4洗鸵,繪制過(guò)程同Column1,在此不再贅述仗嗦。
7. 當(dāng) RepaintBoundary 的子節(jié)點(diǎn)繪制完時(shí)膘滨,要將 RepaintBoundary 的 layer( OffsetLayer2 )添加到父級(jí)Layer(OffsetLayer1)中。
至此稀拐,整棵組件樹(shù)繪制完成火邓,生成了一棵右圖所示的 Layer 樹(shù)。需要說(shuō)名的是 PictureLayer1 和 OffsetLayer2 是兄弟關(guān)系德撬,它們都是 OffsetLayer1 的孩子铲咨。通過(guò)上面的例子我們至少可以發(fā)現(xiàn)一點(diǎn):同一個(gè) Layer 是可以多個(gè)組件共享的,比如 Text1 和 Text2 共享 PictureLayer1蜓洪。
如果共享的話纤勒,會(huì)不會(huì)導(dǎo)致一個(gè)問(wèn)題,比如 Text1 文本發(fā)生變化需要重繪時(shí)隆檀,是不是也會(huì)連帶著 Text2 也必須重繪摇天?
答案是:是!這貌似有點(diǎn)不合理恐仑,既然如此那為什么要共享呢泉坐?不能每一個(gè)組件都繪制在一個(gè)單獨(dú)的 Layer 上嗎?這樣還能避免相互干擾裳仆。原因其實(shí)還是為了節(jié)省資源腕让,Layer 太多時(shí) Skia 會(huì)比較耗資源,所以這其實(shí)是一個(gè)trade-off歧斟。
再次強(qiáng)調(diào)一下记某,上面只是繪制的一般流程。一般情況下 Layer 樹(shù)中的 ContainerLayer 和 PictureLayer 的數(shù)量和結(jié)構(gòu)是和 Widget 樹(shù)中的邊界節(jié)點(diǎn)一一對(duì)應(yīng)的构捡,注意并不是和 Widget一一對(duì)應(yīng)。 當(dāng)然壳猜,如果 Widget 樹(shù)中有子組件在繪制過(guò)程中添加了新的 Layer勾徽,那么Layer 會(huì)比邊界節(jié)點(diǎn)數(shù)量多一些,這時(shí)就不是一一對(duì)應(yīng)了统扳。Flutter 中很多擁有變換喘帚、剪裁畅姊、透明等效果的組件的實(shí)現(xiàn)中都會(huì)往 Layer 樹(shù)中添加新的 Layer。
發(fā)起重繪
RenderObject 是通過(guò)調(diào)用 markNeedsRepaint 來(lái)發(fā)起重繪請(qǐng)求的吹由。
繪制過(guò)程存在Layer共享若未,所以重繪時(shí) 需要重繪所有共享同一個(gè)Layer的組件。當(dāng)一個(gè)節(jié)點(diǎn)需要重繪時(shí)倾鲫,我們得找到離它最近的第一個(gè)父級(jí)繪制邊界節(jié)點(diǎn)粗合,然后讓它重繪即可。當(dāng)一個(gè)節(jié)點(diǎn)調(diào)用了它時(shí)乌昔,markNeedsRepaint正是完成了這個(gè)過(guò)程隙疚。具體的步驟如下:
1. 會(huì)從當(dāng)前節(jié)點(diǎn)一直往父級(jí)查找,直到找到一個(gè)繪制邊界節(jié)點(diǎn)時(shí)終止查找磕道,然后會(huì)將該繪制邊界節(jié)點(diǎn)添加到其PiplineOwner的 _nodesNeedingPaint列表中(保存需要重繪的繪制邊界節(jié)點(diǎn))供屉。
2. 在查找的過(guò)程中,會(huì)將自己到繪制邊界節(jié)點(diǎn)路徑上所有節(jié)點(diǎn)的_needsPaint屬性置為true溺蕉,表示需要重新繪制伶丐。
3. 請(qǐng)求新的 frame ,執(zhí)行重繪重繪流程疯特。
markNeedsRepaint 刪減后的核心源碼如下:
void markNeedsPaint() {
if (_needsPaint) return;
_needsPaint = true;
if (isRepaintBoundary) { // 如果是當(dāng)前節(jié)點(diǎn)是邊界節(jié)點(diǎn)
owner!._nodesNeedingPaint.add(this); // 將當(dāng)前節(jié)點(diǎn)添加到需要重新繪制的列表中哗魂。
owner!.requestVisualUpdate(); // 請(qǐng)求新的frame,該方法最終會(huì)調(diào)用scheduleFrame()
} else if (parent is RenderObject) { // 若不是邊界節(jié)點(diǎn)且存在父節(jié)點(diǎn)
final RenderObject parent = this.parent! as RenderObject;
parent.markNeedsPaint(); // 遞歸調(diào)用父節(jié)點(diǎn)的markNeedsPaint
} else {
// 如果是根節(jié)點(diǎn)辙芍,直接請(qǐng)求新的 frame 即可
if (owner != null)
owner!.requestVisualUpdate();
}
}
在當(dāng)前版本的Flutter中是永遠(yuǎn)不會(huì)走到最后一個(gè)else分支的啡彬,因?yàn)楫?dāng)前版本中根節(jié)點(diǎn)是一個(gè)RenderView,而該組件的isRepaintBoundary 屬性為 true故硅,所以如果調(diào)用 renderView.markNeedsPaint()是會(huì)走到isRepaintBoundary為 true的分支的庶灿。
請(qǐng)求新的 frame后,下一個(gè) frame 到來(lái)時(shí)就會(huì)走drawFrame流程吃衅,drawFrame中和繪制相關(guān)的涉及flushCompositingBits往踢、flushPaint 和 compositeFrame 三個(gè)函數(shù),而重新繪制的流程在 flushPaint 中徘层,所以我們先重點(diǎn)看一下flushPaint的流程峻呕。
flushPaint流程
遍歷需要繪制的節(jié)點(diǎn)列表,然后逐個(gè)開(kāi)始繪制趣效。
final List<RenderObject> dirtyNodes = nodesNeedingPaint;
for (final RenderObject node in dirtyNodes){
PaintingContext.repaintCompositedChild(node);
}
組件樹(shù)中某個(gè)節(jié)點(diǎn)要更新自己時(shí)會(huì)調(diào)用markNeedsRepaint方法瘦癌,而該方法會(huì)從當(dāng)前節(jié)點(diǎn)一直往上查找,直到找到一個(gè)isRepaintBoundary為 true 的節(jié)點(diǎn)跷敬,然后會(huì)將該節(jié)點(diǎn)添加到 nodesNeedingPaint列表中讯私。因此,nodesNeedingPaint中的節(jié)點(diǎn)的isRepaintBoundary 必然為 true,換句話說(shuō)斤寇,能被添加到 nodesNeedingPaint列表中節(jié)點(diǎn)都是繪制邊界桶癣,那么這個(gè)邊界究竟是如何起作用的,我們繼續(xù)看PaintingContext.repaintCompositedChild 函數(shù)的實(shí)現(xiàn)娘锁。
static void repaintCompositedChild( RenderObject child, PaintingContext? childContext) {
assert(child.isRepaintBoundary); // 斷言:能走的這節(jié)點(diǎn)牙寞,其isRepaintBoundary必定為true.
OffsetLayer? childLayer = child.layer;
if (childLayer == null) { //如果邊界節(jié)點(diǎn)沒(méi)有l(wèi)ayer,則為其創(chuàng)建一個(gè)OffsetLayer
final OffsetLayer layer = OffsetLayer();
child.layer = childLayer = layer;
} else { //如果邊界節(jié)點(diǎn)已經(jīng)有l(wèi)ayer了(之前繪制時(shí)已經(jīng)為其創(chuàng)建過(guò)layer了)莫秆,則清空其子節(jié)點(diǎn)间雀。
childLayer.removeAllChildren();
}
//通過(guò)其layer構(gòu)建一個(gè)paintingContext,之后layer便和childContext綁定馏锡,這意味著通過(guò)同一個(gè)
//paintingContext的canvas繪制的產(chǎn)物屬于同一個(gè)layer雷蹂。
paintingContext ??= PaintingContext(childLayer, child.paintBounds);
//調(diào)用節(jié)點(diǎn)的paint方法,繪制子節(jié)點(diǎn)(樹(shù))
child.paint(paintingContext, Offset.zero);
childContext.stopRecordingIfNeeded();//這行后面解釋
}
可以看到,在繪制邊界節(jié)點(diǎn)時(shí)會(huì)首先檢查其是否有l(wèi)ayer,如果沒(méi)有就會(huì)創(chuàng)建一個(gè)新的 OffsetLayer 給它,隨后會(huì)根據(jù)該 offsetLayer 構(gòu)建一個(gè) PaintingContext 對(duì)象(記為context)滑蚯,之后子組件在獲取context的canvas對(duì)象時(shí)會(huì)創(chuàng)建一個(gè) PictureLayer,然后再創(chuàng)建一個(gè) Canvas 對(duì)象和新創(chuàng)建的 PictureLayer 關(guān)聯(lián)起來(lái)萎庭,這意味著后續(xù)通過(guò)同一個(gè)paintingContext 的 canvas 繪制的產(chǎn)物屬于同一個(gè)PictureLayer。下面我們看看相關(guān)源碼:
Canvas get canvas {
//如果canvas為空齿拂,則是第一次獲炔倒妗;
if (_canvas == null) _startRecording();
return _canvas!;
}
//創(chuàng)建PictureLayer和canvas
void _startRecording() {
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder!);
//將pictureLayer添加到_containerLayer(是繪制邊界節(jié)點(diǎn)的Layer)中
_containerLayer.append(_currentLayer!);
}
下面我們?cè)賮?lái)看看 child.paint 方法的實(shí)現(xiàn)署海,該方法需要節(jié)點(diǎn)自己實(shí)現(xiàn)吗购,用于繪制自身,節(jié)點(diǎn)類型不同砸狞,繪制算法一般也不同捻勉,不過(guò)功能是差不多的,即:如果是容器組件刀森,要繪制孩子和自身(也可能自身也可能沒(méi)有繪制邏輯踱启,只繪制孩子,比如Center組件)研底,如果不是容器類組件埠偿,則繪制自己(比如Image)。
void paint(PaintingContext context, Offset offset) {
// ...自身的繪制
if(hasChild){ //如果該組件是容器組件榜晦,繪制子節(jié)點(diǎn)冠蒋。
context.paintChild(child, offset)
}
//...自身的繪制
}
接下來(lái)我們看一下context.paintChild方法:它的主要邏輯是:如果當(dāng)前節(jié)點(diǎn)是邊界節(jié)點(diǎn)且需要重新繪制,則先調(diào)用上面解析過(guò)的repaintCompositedChild方法乾胶,該方法執(zhí)行完畢后浊服,會(huì)將當(dāng)前節(jié)點(diǎn)的layer添加到父邊界節(jié)點(diǎn)的Layer中统屈;如果當(dāng)前節(jié)點(diǎn)不是邊界節(jié)點(diǎn),則調(diào)用paint方法(上面剛說(shuō)過(guò)):
//繪制孩子
void paintChild(RenderObject child, Offset offset) {
//如果子節(jié)點(diǎn)是邊界節(jié)點(diǎn)牙躺,則遞歸調(diào)用repaintCompositedChild
if (child.isRepaintBoundary) {
if (child._needsPaint) { //需要重繪時(shí)再重繪
repaintCompositedChild(child);
}
//將孩子節(jié)點(diǎn)的layer添加到Layer樹(shù)中,
final OffsetLayer childOffsetLayer = child.layer! as OffsetLayer;
childOffsetLayer.offset = offset;
//將當(dāng)前邊界節(jié)點(diǎn)的layer添加到父邊界節(jié)點(diǎn)的layer中.
appendLayer(childOffsetLayer);
} else {
// 如果不是邊界節(jié)點(diǎn)直接繪制自己
child.paint(this, offset);
}
}
這里需要注意三點(diǎn):
1. 繪制孩子節(jié)點(diǎn)時(shí),如果遇到邊界節(jié)點(diǎn)且當(dāng)其不需要重繪(_needsPaint 為 false) 時(shí)腕扶,會(huì)直接復(fù)用該邊界節(jié)點(diǎn)的 layer孽拷,而無(wú)需重繪!這就是邊界節(jié)點(diǎn)能跨 frame 復(fù)用的原理半抱。
2. 因?yàn)檫吔绻?jié)點(diǎn)的layer類型是ContainerLayer脓恕,所以是可以給它添加子節(jié)點(diǎn)。
3. 注意是將當(dāng)前邊界節(jié)點(diǎn)的layer添加到 父邊界節(jié)點(diǎn)窿侈,而不是父節(jié)點(diǎn)炼幔。
按照上面的流程執(zhí)行完畢后,最終所有邊界節(jié)點(diǎn)的layer就會(huì)相連起來(lái)組成一棵Layer樹(shù)史简。
創(chuàng)建新的 PictureLayer
在上面示例中給Row添加第三個(gè)子節(jié)點(diǎn) Text5乃秀。
因?yàn)?Text5 是在 RepaintBoundary 繪制完成后才會(huì)繪制,上例中當(dāng) RepaintBoundary 的子節(jié)點(diǎn)繪制完時(shí)圆兵,將 RepaintBoundary 的 layer( OffsetLayer2 )添加到父級(jí)Layer(OffsetLayer1)中后發(fā)生了什么跺讯?答案在我們上面介紹的repaintCompositedChild 的最后一行:
childContext.stopRecordingIfNeeded();
我們看看其刪減后的核心代碼:
void stopRecordingIfNeeded() {
_currentLayer!.picture = _recorder!.endRecording();// 將canvas繪制產(chǎn)物保存在 PictureLayer中
_currentLayer = null;
_recorder = null;
_canvas = null;
}
當(dāng)繪制完 RepaintBoundary 走到 childContext.stopRecordingIfNeeded() 時(shí), childContext 對(duì)應(yīng)的 Layer 是 OffsetLayer1殉农,而 _currentLayer 是 PictureLayer1刀脏, _canvas 對(duì)應(yīng)的是 Canvas1。我們看到實(shí)現(xiàn)很簡(jiǎn)單超凳,先將 Canvas1 的繪制產(chǎn)物保存在 PictureLayer1 中愈污,然后將一些變量都置空。
接下來(lái)再繪制 Text5 時(shí)轮傍,要先通過(guò)context.canvas 來(lái)繪制暂雹,根據(jù) canvas getter的實(shí)現(xiàn)源碼,此時(shí)就會(huì)走到 _startRecording() 方法金麸,該方法我們上面介紹過(guò)擎析,它會(huì)重新生成一個(gè) PictureLayer 和一個(gè)新的 Canvas :
Canvas get canvas {
//如果canvas為空,則是第一次獲然酉隆揍魂;
if (_canvas == null) _startRecording();
return _canvas!;
}
之后,我們將新生成的 PictureLayer 和 Canvas 記為 PictureLayer3 和 Canvas3棚瘟,Text5 的繪制會(huì)落在 PictureLayer3 上
總結(jié)一下:
父節(jié)點(diǎn)在繪制子節(jié)點(diǎn)時(shí)现斋,如果子節(jié)點(diǎn)是繪制邊界節(jié)點(diǎn),則在繪制完子節(jié)點(diǎn)后會(huì)生成一個(gè)新的 PictureLayer偎蘸,后續(xù)其他子節(jié)點(diǎn)會(huì)在新的 PictureLayer 上繪制庄蹋。
為什么要這么做呢瞬内?直接復(fù)用之前的 PictureLayer1 有問(wèn)題嗎?答案:在當(dāng)前的示例中是不會(huì)有問(wèn)題限书,但是在層疊布局的場(chǎng)景中就會(huì)有問(wèn)題虫蝶。
左邊是一個(gè) Stack 布局,右邊是對(duì)應(yīng)的Layer樹(shù)結(jié)構(gòu)倦西;我們知道Stack布局中會(huì)根據(jù)其子組件的加入順序進(jìn)行層疊繪制能真,最先加入的孩子在最底層,最后加入的孩子在最上層扰柠》垲恚可以設(shè)想一下如果繪制 Child3 時(shí)復(fù)用了 PictureLayer1,則會(huì)導(dǎo)致 Child3 被 Child2 遮住卤档,這顯然不符合預(yù)期蝙泼,但如果新建一個(gè) PictureLayer 在添加到 OffsetLayer 最后面,則可以獲得正確的結(jié)果劝枣。
現(xiàn)在我們?cè)賮?lái)深入思考一下:如果 Child2 的父節(jié)點(diǎn)不是 RepaintBoundary汤踏,那么是否就意味著 Child3 和 Child1就可以共享同一個(gè) PictureLayer 了?
答案是否定的哨免!如果 Child2 的父組件改為一個(gè)自定義的組件茎活,在這個(gè)自定義的組件中我們希望對(duì)子節(jié)點(diǎn)在渲染時(shí)進(jìn)行一些舉證變化,為了實(shí)現(xiàn)這個(gè)功能琢唾,我們創(chuàng)建一個(gè)新的 TransformLayer 并指定變換規(guī)則载荔,然后我們把它傳遞給 Child2,Child2會(huì)繪制完成后采桃,我們需要將 TransformLayer 添加到 Layer 樹(shù)中(不添加到Layer樹(shù)中是不會(huì)顯示的)
可以發(fā)現(xiàn)這種情況本質(zhì)上和上面使用 RepaintBoudary 的情況是一樣的懒熙,Child3 仍然不應(yīng)該復(fù)用 PictureLayer1,那么現(xiàn)在我們可以總結(jié)一個(gè)一般規(guī)律了:只要一個(gè)組件需要往 Layer 樹(shù)中添加新的 Layer普办,那么就必須也要結(jié)束掉當(dāng)前 PictureLayer 的繪制工扎。這也是為什么 PaintingContext 中需要往 Layer 樹(shù)中添加新 Layer 的方法(比如pushLayer、addLayer)中都有如下兩行代碼:
stopRecordingIfNeeded(); //先結(jié)束當(dāng)前 PictureLayer 的繪制
appendLayer(layer);// 再添加到 layer樹(shù)
這是向 Layer 樹(shù)中添加Layer的標(biāo)準(zhǔn)操作衔蹲。這個(gè)結(jié)論要牢記肢娘,我們?cè)诤竺娼榻B flushCompositingBits() 的原理時(shí)會(huì)用到。
compositeFrame
創(chuàng)建好layer后舆驶,接下來(lái)就需要上屏展示了橱健,而這部分工作是由renderView.compositeFrame方法來(lái)完成的。實(shí)際上他的實(shí)現(xiàn)邏輯很簡(jiǎn)單:先通過(guò)layer構(gòu)建Scene沙廉,最后再通過(guò)window.render API 來(lái)渲染:
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer!.buildScene(builder);
window.render(scene);
值得一提的是構(gòu)建Scene的過(guò)程拘荡,我們看一下核心源碼:
ui.Scene buildScene(ui.SceneBuilder builder) {
updateSubtreeNeedsAddToScene();
addToScene(builder); //關(guān)鍵
final ui.Scene scene = builder.build();
return scene;
}
最關(guān)鍵的一行就是調(diào)用addToScene,該方法主要的功能就是將Layer樹(shù)中每一個(gè)layer傳給Skia(最終會(huì)調(diào)用native API撬陵,如果想了解詳情珊皿,建議查看 OffsetLayer 和 PictureLayer 的 addToScene 方法)网缝,這是上屏前的最后一個(gè)準(zhǔn)備動(dòng)作,最后就是調(diào)用 window.render 將繪制數(shù)據(jù)發(fā)給GPU蟋定,渲染出來(lái)了粉臊!