目录
1. Flutter UI系统
2. Element与BuildContext
3. RenderObject和RenderBox
4. Flutter运行机制-从启动到显示
5. 图片加载原理与缓存
1. Flutter UI系统
UI系统(平台特指操作系统):基于一个平台,在此平台上实现GUI的一个系统,如Android、iOS或者Windows、macOS。
各个平台UI系统的原理是相通的,无论是Android还是iOS,它们将一个用户界面展示到屏幕的流程是相似的。
- 硬件绘图(屏幕显示图像) 的基本原理
显示器(屏幕)是由一个个物理显示单元(物理像素点)组成,每一个像素点可以发出多种颜色,显示器成相的原理就是在不同的物理像素点上显示不同的颜色,最终构成完整的图像。
一个像素点能发出的所有颜色总数是显示器的一个重要指标。比如1600万色的屏幕就是指一个像素点可以显示出1600万种颜色,而显示器颜色是有RGB三基色组成,所以1600万即2的24次方,即每个基本色(R、G、B)深度扩展至8 bit(位),颜色深度越深,所能显示的色彩更加丰富靓丽。
为了更新显示画面,显示器是以固定的频率刷新(从GPU取数据)。比如有一部手机屏幕的刷新频率是 60Hz。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(如VSync), 60Hz的屏幕就会一秒内发出 60次这样的信号。而这个信号主要是用于同步CPU、GPU和显示器的。一般地来说,计算机系统中,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照同步信号从帧缓冲区取帧数据传递给显示器显示。
CPU和GPU的任务是各有偏重的,CPU主要用于基本数学和逻辑计算,而GPU主要执行和图形处理相关的复杂的数学,如矩阵变化和几何计算,GPU的主要作用就是确定最终输送给显示器的各个像素点的色值。
- 操作系统绘制API的封装
由于最终的图形计算和绘制都是由相应的硬件来完成,而直接操作硬件的指令通常都会有操作系统屏蔽,应用开发者通常不会直接面对硬件,操作系统屏蔽了这些底层硬件操作后会提供一些封装后的API供操作系统之上的应用调用,但是对于应用开发者来说,直接调用这些操作系统提供的API是比较复杂和低效的,因为操作系统提供的API往往比较基础,直接调用需要了解API的很多细节。
正是因为这个原因,几乎所有用于开发GUI程序的编程语言都会在操作系统之上再封装一层,将操作系统原生API封装在一个编程框架和模型中,然后定义一种简单的开发规则来开发GUI应用程序,而这一层抽象,正是UI系统。如Android SDK正是封装了Android操作系统API,提供了一个“UI描述文件XML+Java操作DOM”的UI系统,而iOS的UIKit 对View的抽象也是一样的,他们都将操作系统API抽象成一个基础对象(如用于2D图形绘制的Canvas),然后再定义一套规则来描述UI,如UI树结构,UI操作的单线程原则等。
- Flutter UI系统
无论是Android SDK还是iOS的UIKit 的职责都是相同的,它们只是语言载体和底层的系统不同而已。那么可不可以实现这么一个UI系统:可以使用同一种编程语言开发,然后针对不同操作系统API抽象一个对上接口一致,对下适配不同操作系统的的中间层,然后在打包编译时再使用相应的中间层代码?如果可以做到,那么就可以使用同一套代码编写跨平台的应用了。
Flutter的原理正是如此,它提供了一套Dart API,然后在底层通过OpenGL这种跨平台的绘制库(内部会调用操作系统API)实现了一套代码跨多端,性能接近原生。虽然Dart是先调用了OpenGL,OpenGL再调用操作系统API,但是这仍然是原生渲染,因为OpenGL只是操作系统API的一个封装库,它并不像WebView渲染那样需要JavaScript运行环境和CSS渲染器,所以不会有性能损失。
Flutter UI系统对应用开发者定义的开发标准, 简单概括就是:组合(开发一个UI界面,需要通过组合其它Widget来实现,Flutter中一切都是Widget)和 响应式(当UI要发生变化时,不去直接修改DOM而是通过更新状态,让Flutter UI系统来根据新的状态来重新构建UI)。
2. Element与BuildContext
- Element
Flutter的UI系统包含三棵树:Widget树、Element树、渲染树。
Widget组件的Layout、渲染最终都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。Flutter正是通过Element这个纽带将Widget和RenderObject关联起来。
最终的UI树由一个个独立的Element节点构成。Element是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类(MultiChildRenderObjectElement等)。最终所有Element的RenderObject构成一棵渲染树(Render Tree)。
对于开发者来说,大多数情况下只需要关注Widget树,Flutter框架已经将对Widget树的操作映射到了Element树上,极大的降低了复杂度,提高了开发效率。
但有时候必须得直接使用Element对象来完成一些操作,比如获取主题Theme数据。
Element的生命周期如下:
1. Framework 调用Widget.createElement 创建一个Element实例,记为element
2. Framework 调用element.mount(parentElement,newSlot) ,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
3. 当有父Widget的配置数据改变时,同时其State.build返回的Widget结构与之前不同,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的canUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。Widget.canUpdate主要是判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,否则就会返回false。根据这个原理,当需要强制更新一个Widget时,可以通过指定不同的Key来避免复用。
4. 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为“inactive”状态。
5. “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。
6. 如果element要重新插入到Element树的其它位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。
- 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() // 查找当前或最近的一个祖先RenderObject
StatelessWidget和StatefulWidget的build方法都会传一个BuildContext对象(即Element对象):
Widget build(BuildContext context) {}
该build方法的调用发生在StatelessWidget和StatefulWidget对应的StatelessElement和StatefulElement的build方法中。
例如StatelessElement:
class StatelessElement extends ComponentElement {
...
@override
Widget build() => widget.build(this); // 这里调用了Widget的build方法
...
}
build传递的参数是this,所有这个BuildContext就是StatelessElement。但StatelessElement和StatefulElement本身并没有实现BuildContext接口。
继续跟踪代码,发现它们间接继承自Element类,然后查看Element类定义,发现Element类果然实现了BuildContext接口:
class Element extends DiagnosticableTree implements BuildContext {
...
}
至此真相大白,BuildContext就是widget对应的Element,所以可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。
获取主题数据的代码Theme.of(context)内部正是调用了Element的dependOnInheritedWidgetOfExactType()方法。
可以看到Element是Flutter UI框架内部连接widget和RenderObject的纽带,大多数时候开发者只需要关注widget层即可,但是widget层有时候并不能完全屏蔽Element细节,所以Framework在StatelessWidget和StatefulWidget中通过build方法参数又将Element对象也传递给了开发者。这样一来,开发者便可以在需要时直接操作Element对象。
完全可以直接通过Element来搭建一个UI框架,但使用Widget更方便。
例
通过纯粹的Element来模拟一个StatefulWidget的功能。
假设有一个页面,该页面有一个按钮,按钮的文本是一个9位数,点击一次按钮,则对9个数随机排一次序
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(); // 点击后将该Element标记为dirty,Element将会rebuild
},
),
),
);
}
}
说明:
1. 上面build方法不接收参数,这一点和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代码中需要用到BuildContext的地方直接用this代替即可,因为当前对象本身就是Element实例。
2. 当text发生改变时,调用markNeedsBuild()方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,State.setState()在内部也是调用的markNeedsBuild()方法。
3. 上面代码中build方法返回的仍然是一个widget。
如果需要将上面代码在现有Flutter框架中跑起来,那么还是得提供一个“适配器”widget将HomeView结合到现有框架中,下面CustomHome就相当于“适配器”:
class CustomHome extends Widget {
@override
Element createElement() {
return HomeView(this);
}
}
点击按钮则按钮文本会随机排序。
3. RenderObject和RenderBox
每个Element都对应一个RenderObject,可以通过Element.renderObject来获取。RenderObject的主要职责是Layout和绘制,所有的RenderObject会组成一棵渲染树Render Tree。
RenderObject就是渲染树中的一个对象,它拥有一个parent和一个parentData插槽(slot)。所谓插槽,就是指预留的一个接口或位置,这个接口和位置是由其它对象来接入或占据的,这个接口或位置在软件中通常用预留变量来表示,而parentData正是一个预留变量,它正是由parent来赋值的,parent通常会通过子RenderObject的parentData存储一些和子元素相关的数据,如在Stack布局中,RenderStack就会将子元素的偏移数据存储在子元素的parentData中。
RenderObject类本身实现了一套基础的layout和绘制协议,但是并没有定义子节点模型(如一个节点可以有几个子节点,没有子节点?一个?两个?或者更多?)。 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。
为此,Flutter提供了一个RenderBox类,它继承自RenderObject,布局坐标系统采用笛卡尔坐标系,这和Android和iOS原生坐标系是一致的,都是屏幕的top、left是原点,然后分宽高两个轴。大多数情况下直接使用RenderBox就可以了,除非遇到要自定义布局模型或坐标系统的情况。
如果要从头到尾实现一个RenderObject是比较麻烦的(必须去实现layout、绘制和命中测试逻辑),大多数时候可以直接在Widget层通过组合或者CustomPaint完成自定义UI。如果遇到只能定义一个新RenderObject的场景时(如要实现一个新的layout算法的布局容器),可以直接继承自RenderBox去实现。
- 布局过程
Constraints
在RenderBox 中,有个size属性用来保存控件的宽和高。RenderBox的layout是通过在组件树中从上往下传递BoxConstraints对象的实现的。BoxConstraints对象可以限制子节点的最大和最小宽高,子节点必须遵守父节点给定的限制条件。
在布局阶段,父节点会调用子节点的layout()方法。
RenderObject中layout()方法的大致实现:
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方法需要传入两个参数,第一个为constraints,即 父节点对子节点大小的限制,该值根据父节点的布局逻辑确定。另外一个参数是 parentUsesSize,该值用于确定 relayoutBoundary,该参数表示子节点布局变化是否影响父节点,如果为true,当子节点布局发生变化时父节点都会标记为需要重新布局,如果为false,则子节点布局发生变化后不会影响父节点。
relayoutBoundary
上面layout()源码中定义了一个relayoutBoundary变量,
当一个Element标记为 dirty(通过调用 markNeedsBuild() 方法)时便会重新build,这时RenderObject便会重新布局。在RenderObject中有一个类似的markNeedsLayout()方法,它会将RenderObject的布局状态标记为 dirty,这样在下一个frame中便会重新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,如果不是就继续向parent 查找,一直向上查找到是 relayoutBoundary 的 RenderObject为止,然后再将其标记为 dirty 的。这样来看它的作用就比较明显了,意思就是当一个控件的大小被改变时可能会影响到它的 parent,因此 parent 也需要被重新布局,那么到什么时候是个头呢?答案就是 relayoutBoundary,如果一个 RenderObject 是 relayoutBoundary,就表示它的大小变化不会再影响到 parent 的大小了,于是 parent 也就不用重新布局了。
performResize 和 performLayout
RenderBox实际的测量和布局逻辑是在performResize() 和 performLayout()两个方法中,RenderBox子类需要实现这两个方法来定制自身的布局逻辑。
根据layout() 源码可以看出只有 sizedByParent 为 true 时,performResize() 才会被调用,而 performLayout() 是每次布局都会被调用的。sizedByParent 意为该节点的大小是否仅通过 parent 传给它的 constraints 就可以确定了,即该节点的大小与它自身的属性和其子节点无关,比如如果一个控件永远充满 parent 的大小,那么 sizedByParent就应该返回true,此时其大小在 performResize() 中就确定了,在后面的 performLayout() 方法中将不会再被修改了,这种情况下 performLayout() 只负责布局子节点。
在 performLayout() 方法中除了完成自身布局,也必须完成子节点的布局,这是因为只有父子节点全部完成后布局流程才算真正完成。所以最终的调用栈将会变成:layout() > performResize()/performLayout() > child.layout() > ... ,如此递归完成整个UI的布局。
RenderBox子类要定制布局算法不应该重写layout()方法,因为对于任何RenderBox的子类来说,它的layout流程基本是相同的,不同之处只在具体的布局算法,而具体的布局算法子类应该通过重写performResize() 和 performLayout()两个方法来实现,他们会在layout()中被调用。
ParentData
RenderObject的parentData 只能通过父元素设置.
当layout结束后,每个节点的位置(相对于父节点的偏移)就已经确定了,RenderObject就可以根据位置信息来进行最终的绘制。但是在layout过程中,节点的位置信息怎么保存?对于大多数RenderBox子类来说如果子类只有一个子节点,那么子节点偏移一般都是Offset.zero ,如果有多个子节点,则每个子节点的偏移就可能不同。而子节点在父节点的偏移数据正是通过RenderObject的parentData属性来保存的。在RenderBox中,其parentData属性默认是一个BoxParentData对象,该属性只能通过父节点的setupParentData()方法来设置:
abstract class RenderBox extends RenderObject {
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! BoxParentData)
child.parentData = BoxParentData();
}
...
}
BoxParentData定义如下:
/// Parentdata 会被RenderBox和它的子类使用.
class BoxParentData extends ParentData {
/// offset表示在子节点在父节点坐标系中的绘制偏移
Offset offset = Offset.zero;
@override
String toString() => 'offset=$offset';
}
ParentData并不仅仅可以用来存储偏移信息,通常所有和子节点特定的数据都可以存储到子节点的ParentData中,如ContainerBox的ParentData就保存了指向兄弟节点的previousSibling和nextSibling,Element.visitChildren()方法也正是通过它们来实现对子节点的遍历。再比如KeepAlive 组件,它使用KeepAliveParentDataMixin(继承自ParentData) 来保存子节的keepAlive状态。
- 绘制过程
RenderObject可以通过paint()方法来完成具体绘制逻辑,流程和布局流程相似,子类可以实现paint()方法来完成自身的绘制逻辑,paint()签名如下:
void paint(PaintingContext context, Offset offset) { }
通过context.canvas可以取到Canvas对象,就可以调用Canvas API来实现具体的绘制逻辑
如果节点有子节点,它除了完成自身绘制逻辑之外,还要调用子节点的绘制方法。例:
@override
void paint(PaintingContext context, Offset offset) {
// 如果子元素未超出当前边界,则绘制子元素
if (_overflow <= 0.0) {
defaultPaint(context, offset);
return;
}
// 如果size为空,则无需绘制
if (size.isEmpty)
return;
// 剪裁掉溢出边界的部分
context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint);
assert(() {
final String debugOverflowHints = '...'; //溢出提示内容,省略
// 绘制溢出部分的错误提示样式
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;
}());
}
代码很简单,首先判断有无溢出,如果没有则调用defaultPaint(context, offset)来完成绘制,该方法源码如下:
void defaultPaint(PaintingContext context, Offset offset) {
ChildType child = firstChild;
while (child != null) {
final ParentDataType childParentData = child.parentData;
//绘制子节点,
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
很明显,由于Flex本身没有需要绘制的东西,所以直接遍历其子节点,然后调用paintChild()来绘制子节点,同时将子节点ParentData中在layout阶段保存的offset加上自身偏移作为第二个参数传递给paintChild()。而如果子节点还有子节点时,paintChild()方法还会调用子节点的paint()方法,如此递归完成整个节点树的绘制,最终调用栈为: paint() > paintChild() > paint() ... 。
当需要绘制的内容大小溢出当前空间时,将会执行paintOverflowIndicator() 来绘制溢出部分提示,这个就是我们经常看到的溢出提示
RepaintBoundary
与 RelayoutBoundary 相似,RepaintBoundary是用于在确定重绘边界的,与RelayoutBoundary不同的是,这个绘制边界需要由开发者通过RepaintBoundary 组件自己指定,如:
CustomPaint(
size: Size(300, 300), //指定画布大小
painter: MyPainter(),
child: RepaintBoundary(
child: Container(...),
),
),
RenderObject有一个isRepaintBoundary属性,该属性决定这个RenderObject重绘时是否独立于其父元素,如果该属性值为true ,则独立绘制,反之则一起绘制。
独立绘制是怎么实现的,答案就在paintChild()源码中:
void paintChild(RenderObject child, Offset offset) {
...
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
...
}
可以看到,在绘制子节点时,如果child.isRepaintBoundary 为 true则会调用_compositeChild()方法,_compositeChild()源码如下:
void _compositeChild(RenderObject child, Offset offset) {
// 给子节点创建一个layer ,然后再上面绘制子节点
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
...
}
assert(child._layer != null);
child._layer.offset = offset;
appendLayer(child._layer);
}
独立绘制是通过在不同的layer(层)上绘制的。所以,很明显,正确使用isRepaintBoundary属性可以提高绘制效率,避免不必要的重绘。具体原理是:和触发重新build和layout类似,RenderObject也提供了一个markNeedsPaint()方法,其源码如下:
void markNeedsPaint() {
...
//如果RenderObject.isRepaintBoundary 为true,则该RenderObject拥有layer,直接绘制
if (isRepaintBoundary) {
...
if (owner != null) {
//找到最近的layer,绘制
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {
// 没有自己的layer, 会和一个祖先节点共用一个layer
assert(_layer == null);
final RenderObject parent = this.parent;
// 向父级递归查找
parent.markNeedsPaint();
assert(parent == this.parent);
} else {
// 如果直到根节点也没找到一个Layer,那么便需要绘制自身,因为没有其它节点可以绘制根节点。
if (owner != null)
owner.requestVisualUpdate();
}
}
可以看出,当调用 markNeedsPaint() 方法时,会从当前 RenderObject 开始一直向父节点查找,直到找到 一个isRepaintBoundary 为 true的RenderObject 时,才会触发重绘,这样便可以实现局部重绘。当 有RenderObject 绘制的很频繁或很复杂时,可以通过RepaintBoundary Widget来指定isRepaintBoundary 为 true,这样在绘制时仅会重绘自身而无需重绘它的 parent,如此便可提高性能。
还有一个问题,通过RepaintBoundary 如何设置isRepaintBoundary属性呢?其实,如果使用了RepaintBoundary,其对应的RenderRepaintBoundary会自动将isRepaintBoundary设为true的:
class RenderRepaintBoundary extends RenderProxyBox {
/// Creates a repaint boundary around [child].
RenderRepaintBoundary({ RenderBox child }) : super(child);
@override
bool get isRepaintBoundary => true;
}
- 命中测试
一个对象是否可以响应事件,取决于其对命中测试的返回,当发生用户事件时,会从根节点(RenderView)开始进行命中测试
RenderView的hitTest()源码:
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(result, position: position); // 递归子RenderBox进行命中测试
result.add(HitTestEntry(this)); // 将测试结果添加到result中
return true;
}
RenderBox默认的hitTest()实现:
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;
}
默认的实现里调用了hitTestSelf()和hitTestChildren()两个方法,这两个方法默认实现如下:
@protected
bool hitTestSelf(Offset position) => false;
@protected
bool hitTestChildren(HitTestResult result, { Offset position }) => false;
hitTest 方法用来判断该RenderObject 是否在被点击的范围内,同时负责将被点击的 RenderBox 添加到 HitTestResult 列表中,参数 position 为事件触发的坐标(如果有的话),返回 true 则表示有RenderBox 通过了命中测试,需要响应事件,反之则认为当前RenderBox没有命中。在继承RenderBox时,可以直接重写hitTest()方法,也可以重写 hitTestSelf() 或 hitTestChildren(), 唯一不同的是 hitTest()中需要将通过命中测试的节点信息添加到命中测试结果列表中,而 hitTestSelf() 和 hitTestChildren()则只需要简单的返回true或false。
- Semantics语义化
语义化,主要是提供给读屏软件的接口,也是实现辅助功能的基础,通过语义化接口可以让机器理解页面上的内容,对于有视力障碍用户可以使用读屏软件来理解UI内容。
如果一个RenderObject要支持语义化接口,可以实现 describeApproximatePaintClip和 visitChildrenForSemantics方法和semanticsAnnotator getter。
4. Flutter运行机制-从启动到显示
- 启动
Flutter的入口在"lib/main.dart"的main()函数中,它是Dart应用程序的起点。
在Flutter应用中,main()函数最简单的实现如下:
void main() {
runApp(MyApp()); // main()函数只调用了一个runApp()方法
}
===================================
===================================
runApp方法源码实现:
// 参数app是一个widget,是Flutter应用启动后要展示的第一个Widget。
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
===================================
===================================
WidgetsFlutterBinding是绑定widget框架和Flutter engine的桥梁,定义如下:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
WidgetsFlutterBinding继承自BindingBase 并混入了很多Binding,查看这些 Binding的源码可以发现这些Binding中基本都是监听并处理Window对象(包含了当前设备和系统的一些信息以及Flutter Engine的一些回调)的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。
WidgetsFlutterBinding正是粘连Flutter engine与上层Framework的“胶水”。
1. GestureBinding:提供了window.onPointerDataPacket 回调,绑定Framework手势子系统,是Framework事件模型与底层事件的绑定入口。
2. ServicesBinding:提供了window.onPlatformMessage 回调, 用于绑定平台消息通道(message channel),主要处理原生和Flutter通信。
3. SchedulerBinding:提供了window.onBeginFrame和window.onDrawFrame回调,监听刷新事件,绑定Framework绘制调度子系统。
4. PaintingBinding:绑定绘制库,主要用于处理图片缓存。
5. SemanticsBinding:语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持。
6. RendererBinding: 提供了window.onMetricsChanged 、window.onTextScaleFactorChanged 等回调。它是渲染树与Flutter engine的桥梁。
7. WidgetsBinding:提供了window.onLocaleChanged、onBuildScheduled 等回调。它是Flutter widget层与engine的桥梁。
===================================
===================================
Window是Flutter Framework连接宿主操作系统的接口。Window类的部分定义:
class Window {
// 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
// DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5
double get devicePixelRatio => _devicePixelRatio;
// Flutter UI绘制区域的大小
Size get physicalSize => _physicalSize;
// 当前系统默认的语言Locale
Locale get locale;
// 当前系统字体缩放比例。
double get textScaleFactor => _textScaleFactor;
// 当绘制区域大小改变回调
VoidCallback get onMetricsChanged => _onMetricsChanged;
// Locale发生变化回调
VoidCallback get onLocaleChanged => _onLocaleChanged;
// 系统字体缩放变化回调
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
// 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
FrameCallback get onBeginFrame => _onBeginFrame;
// 绘制回调
VoidCallback get onDrawFrame => _onDrawFrame;
// 点击或指针事件回调
PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
// 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
// 此方法会直接调用Flutter engine的Window_scheduleFrame方法
void scheduleFrame() native 'Window_scheduleFrame';
// 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
void render(Scene scene) native 'Window_render';
// 发送平台消息
void sendPlatformMessage(String name,
ByteData data,
PlatformMessageResponseCallback callback) ;
// 平台通道消息处理回调
PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
... //其它属性及回调
}
可以看到Window类包含了当前设备和系统的一些信息以及Flutter Engine的一些回调。
===================================
===================================
WidgetsFlutterBinding.ensureInitialized()负责初始化一个WidgetsBinding的全局单例,紧接着会调用WidgetsBinding的attachRootWidget方法,该方法负责将根Widget添加到RenderView上,代码如下:
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
renderView变量是一个RenderObject,它是渲染树的根。renderViewElement变量是renderView对应的Element对象。可见该方法主要完成了根widget到根 RenderObject再到根Element的整个关联过程。
===================================
===================================
attachToRenderTree的源码实现:
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;
}
该方法负责创建根element,即RenderObjectToWidgetElement,并且将element与widget 进行关联,即创建出 widget树对应的element树。如果element 已经创建过了,则将根element 中关联的widget 设为新的,由此可以看出element 只会创建一次,后面会进行复用。BuildOwner是widget framework的管理类,它跟踪哪些widget需要重新构建。
- 渲染
runApp的实现中,当调用完attachRootWidget后,最后一行会调用 WidgetsFlutterBinding 实例的 scheduleWarmUpFrame() 方法,该方法的实现在SchedulerBinding 中,它被调用后会立即进行一次绘制(而不是等待"vsync" 信号),在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前Flutter将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。
下面是scheduleWarmUpFrame() 方法的部分实现(省略了无关代码):
void scheduleWarmUpFrame() {
...
Timer.run(() {
handleBeginFrame(null);
});
Timer.run(() {
handleDrawFrame();
resetEpoch();
});
// 锁定事件
lockEvents(() async {
await endOfFrame;
Timeline.finishSync();
});
...
}
该方法中主要调用了handleBeginFrame() 和 handleDrawFrame() 两个方法
查看handleBeginFrame() 和 handleDrawFrame() 两个方法的源码,可以发现前者主要是执行了transientCallbacks队列,而后者执行了 persistentCallbacks 和 postFrameCallbacks 队列。
===================================
===================================
Frame 和 FrameCallback 的概念:
1. Frame: 一次绘制过程(一帧)。Flutter engine受显示器垂直同步信号"VSync"的驱使不断的触发绘制。Flutter可以实现60fps(Frame Per-Second),就是指一秒钟可以触发60次重绘,FPS值越大,界面就越流畅。
2. FrameCallback:SchedulerBinding 类中有三个FrameCallback回调队列, 在一次绘制过程中,这三个回调队列会放在不同时机被执行:
1. transientCallbacks:用于存放一些临时回调,一般存放动画回调。
可以通过SchedulerBinding.instance.scheduleFrameCallback 添加回调。
2. persistentCallbacks:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。
SchedulerBinding.instance.addPersitentFrameCallback(),这个回调中处理了布局与绘制工作。
3. postFrameCallbacks:在Frame结束时只会被调用一次,调用后会被系统移除,可由 SchedulerBinding.instance.addPostFrameCallback() 注册。
注意,不要在此类回调中再触发新的Frame,这可以会导致循环刷新。
- 绘制
渲染和绘制逻辑在RendererBinding中实现,查看其源码,发现在其initInstances()方法中有如下代码:
void initInstances() {
... // 省略无关代码
// 监听Window对象的事件
ui.window
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction;
// 通过addPersistentFrameCallback 向persistentCallbacks队列添加了一个回调 _handlePersistentFrameCallback
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 布局
pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘
pipelineOwner.flushPaint(); // 重绘
renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
===================================
===================================
flushLayout() 方法主要任务是更新了所有被标记为“dirty”的RenderObject的布局信息。主要的动作发生在node._layoutWithoutResize()方法中,该方法中会调用performLayout()进行重新布局。
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属性,如果该属性值被标记为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() 方法进行了最终的绘制,可以看出它不是重绘了所有 RenderObject,而是只重绘了需要重绘的 RenderObject。真正的绘制是通过PaintingContext.repaintCompositedChild()来绘制的,该方法最终会调用Flutter engine提供的Canvas API来完成绘制。
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() 方法中有一个Scene对象,Scene对象是一个数据结构,保存最终渲染后的像素信息。这个方法将Canvas画好的Scene传给window.render()方法,该方法会直接将scene信息发送给Flutter engine,最终由engine将图像画在设备屏幕上。
void compositeFrame() {
...
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
ui.window.render(scene); //调用Flutter engine的渲染API
scene.dispose();
} finally {
Timeline.finishSync();
}
}
需要注意的是:由于RendererBinding只是一个mixin,而with它的是WidgetsBinding,所以需要看看WidgetsBinding中是否重写该方法,查看WidgetsBinding的drawFrame()方法源码:
@override
void drawFrame() {
...//省略无关代码
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame(); //调用RendererBinding的drawFrame()方法
buildOwner.finalizeTree();
}
}
在调用RendererBinding.drawFrame()方法前会调用 buildOwner.buildScope() (非首次绘制),该方法会将被标记为“dirty” 的 element 进行 rebuild() 。
5. 图片加载原理与缓存
Flutter框架对加载过的图片是有缓存的(内存),默认最大缓存数量是1000,最大缓存空间为100M。
ImageProvider主要负责图片数据的加载和缓存,而绘制部分逻辑主要是由RawImage来完成。
Image是连接起ImageProvider和RawImage的桥梁。
ImageProvider
Image组件的image参数是一个ImageProvider类型的必选参数。
ImageProvider是一个抽象类,定义了图片数据获取、加载、缓存的相关接口。
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); // 需子类实现
}
- load(T key)方法
加载图片数据源的接口,不同的数据源的加载方法不同,每个ImageProvider的子类必须实现它。
比如NetworkImage类和AssetImage类,它们都是ImageProvider的子类,但它们需要从不同的数据源来加载图片数据:NetworkImage是从网络来加载图片数据,而AssetImage则是从应用安装包来加载图片数据。
以NetworkImage为例,看看其load方法的实现:
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents), // 调用
chunkEvents: chunkEvents.stream,
scale: key.scale,
... // 省略无关代码
);
}
load方法的返回值类型是ImageStreamCompleter ,它是一个抽象类,定义了管理图片加载过程的一些接口,Image Widget中正是通过它来监听图片加载状态的。MultiFrameImageStreamCompleter 是 ImageStreamCompleter的一个子类,是flutter sdk预置的类,通过该类可以方便、轻松地创建出一个ImageStreamCompleter实例来做为load方法的返回值。
MultiFrameImageStreamCompleter 需要一个codec参数,该参数类型为Future<ui.Codec>。Codec 是处理图片编解码的类的一个handler,实际上它只是一个flutter engine API 的包装类,也就是说图片的编解码逻辑不是在Dart 代码部分实现,而是在flutter engine中实现的。
==========================================
==========================================
Codec类部分定义如下:
@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
// 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
@pragma('vm:entry-point')
Codec._();
/// 图片中的帧数(动态图会有多帧)
int get frameCount native 'Codec_frameCount';
/// 动画重复的次数
/// * 0 表示只执行一次
/// * -1 表示循环执行
int get repetitionCount native 'Codec_repetitionCount';
/// 获取下一个动画帧
Future<FrameInfo> getNextFrame() {
return _futurize(_getNextFrame);
}
// Codec最终的结果是一个或多个(动图)帧,而这些帧最终会绘制到屏幕上。
String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';
==========================================
==========================================
MultiFrameImageStreamCompleter 的 codec参数值为_loadAsync方法的返回值,_loadAsync方法的实现:
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(...);
// 接收图片数据
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');
// 对图片数据进行解码
return PaintingBinding.instance.instantiateImageCodec(bytes);
} finally {
chunkEvents.close();
}
}
可以看到_loadAsync方法主要做了两件事:
1. 下载图片。通过HttpClient从网上下载图片,下载请求会设置一些自定义的header,开发者可以通过NetworkImage的headers命名参数来传递。
2. 对下载的图片数据进行解码。在图片下载完成后调用了PaintingBinding.instance.instantiateImageCodec(bytes)对图片进行解码,值得注意的是instantiateImageCodec(...)也是一个Native API的包装,实际上会调用Flutter engine的instantiateImageCodec方法,源码如下:
String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight) native 'instantiateImageCodec';
- obtainKey(ImageConfiguration)方法
配合实现图片缓存,ImageProvider从数据源加载完数据后,会在全局的ImageCache中缓存图片数据,而图片数据缓存是一个Map,而Map的key便是调用此方法的返回值,不同的key代表不同的图片数据缓存。
以NetworkImage为例,看一下它的obtainKey()实现:
@override
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
该方法创建了一个同步的future,然后直接将自身做为key返回。因为Map中在判断key(此时是NetworkImage对象)是否相等时会使用“==”运算符,那么定义key的逻辑就是NetworkImage的“==”运算符:
@override
bool operator ==(dynamic other) {
... // 省略无关代码
final NetworkImage typedOther = other;
return url == typedOther.url
&& scale == typedOther.scale;
}
对于网络图片来说,会将其“url地址+scale缩放比例”作为缓存的key。也就是说如果两张图片的url或scale只要有一个不同,便会重新下载并分别缓存。
需要注意的是,图片缓存是在内存中,并没有进行本地文件持久化存储,这也是为什么网络图片在应用重启后需要重新联网下载的原因。同时也意味着在应用生命周期内,如果缓存没有超过上限,相同的图片只会被下载一次。
- resolve(ImageConfiguration) 方法
resolve方法是ImageProvider暴露给Image的主入口方法,接受一个ImageConfiguration参数,返回ImageStream图片数据流。
ImageStream resolve(ImageConfiguration configuration) {
... // 省略无关代码
final ImageStream stream = ImageStream();
T obtainedKey; //
// 定义错误处理函数
Future<void> handleError(dynamic exception, StackTrace stack) async {
... // 省略无关代码
stream.setCompleter(imageCompleter);
imageCompleter.setError(...);
}
// 创建一个新Zone,主要是为了当发生错误时不会干扰MainZone
final Zone dangerZone = Zone.current.fork(...);
dangerZone.runGuarded(() {
Future<T> key;
// 先验证是否已经有缓存
try {
// 生成缓存key,后面会根据此key来检测是否有缓存
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
obtainedKey = key;
// 处理缓存,这里的PaintingBinding.instance.imageCache 是 ImageCache的一个实例,它是PaintingBinding的一个属性,而Flutter框架中的PaintingBinding.instance是一个单例,imageCache事实上也是一个单例,也就是说图片缓存是全局的,统一由PaintingBinding.instance.imageCache 来管理。
final ImageStreamCompleter completer = PaintingBinding.instance
.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
if (completer != null) {
stream.setCompleter(completer);
}
}).catchError(handleError);
});
return stream;
}
==========================================
==========================================
ImageConfiguration包含了图片和设备的相关信息,如图片的大小、所在的AssetBundle(只有打到安装包的图片存在)以及当前的设备平台、devicePixelRatio(设备像素比)。
Flutter SDK提供了一个便捷函数createLocalImageConfiguration来创建ImageConfiguration 对象:
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,
);
}
这些信息基本都是通过Context来获取。
==========================================
==========================================
ImageCache类定义:
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache {
// 正在加载中的图片队列
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 缓存队列
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 缓存数量上限(1000)
int _maximumSize = _kDefaultSize;
// 缓存容量上限 (100 MB)
int _maximumSizeBytes = _kDefaultSizeBytes;
// 缓存上限设置的setter
set maximumSize(int value) {...}
set maximumSizeBytes(int value) {...}
... // 省略部分定义
// 清除所有缓存
void clear() {
// ...省略具体实现代码
}
// 清除指定key对应的图片缓存
bool evict(Object key) {
// ...省略具体实现代码
}
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. 先判断图片数据有没有缓存,如果有则先移除缓存后再添加(可以让最新使用过的缓存在_map中的位置更近一些),并返回ImageStream。
final _CachedImage image = _cache.remove(key);
if (image != null) {
_cache[key] = image;
return image.completer;
}
// 2. 如果没有缓存,则调用load(T key)方法从数据源加载图片数据,加载成功后先缓存,然后返回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;
}
// 当缓存数量超过最大值或缓存的大小超过最大缓存容量,会调用此方法清理到缓存上限以内
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);
}
... //省略无关代码
}
}
可以自定义缓存上限:
PaintingBinding.instance.imageCache.maximumSize=2000; // 最多2000张
PaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; // 最大200M
因为Map中相同key的值会被覆盖,也就是说key是图片缓存的一个唯一标识,只要是不同key,那么图片数据就会分别缓存(即使事实上是同一张图片)。key是ImageProvider.obtainKey()方法的返回值,此方法需要ImageProvider子类去重写,这也就意味着不同的ImageProvider对key的定义逻辑会不同。比如对于NetworkImage,将图片的url和scale作为key会很合适,而对于AssetImage则应该将“包名+路径”作为唯一的key。
Image组件原理
通过实现一个“简版的Image组件”,来大致了解Image组件原理。
代码流程如下:
1. 通过imageProvider.resolve方法可以得到一个ImageStream(图片数据流),然后监听ImageStream的变化。当图片数据源发生变化时,ImageStream会触发相应的事件,而本例中只设置了图片成功的监听器_updateImage,而_updateImage中只更新了_imageInfo。值得注意的是,如果是静态图,ImageStream只会触发一次时间,如果是动态图,则会触发多次事件,每一次都会有一个解码后的图片帧。
2. _imageInfo 更新后会rebuild,此时会创建一个RawImage Widget。RawImage最终会通过RenderImage来将图片绘制在屏幕上。如果继续跟进RenderImage类,会发现RenderImage的paint 方法中调用了paintImage方法,而paintImage方法中通过Canvas的drawImageRect(…)、drawImageNine(...)等方法来完成最终的绘制。
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();
// 依赖改变时,图片的配置信息可能会发生改变
_getImage();
}
@override
void didUpdateWidget(MyImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.imageProvider != oldWidget.imageProvider)
_getImage();
}
void _getImage() {
final ImageStream oldImageStream = _imageStream;
// 调用imageProvider.resolve方法,获得ImageStream。
_imageStream =
widget.imageProvider.resolve(createLocalImageConfiguration(context));
// 判断新旧ImageStream是否相同,如果不同,则需要调整流的监听器
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库
image: _imageInfo?.image,
scale: _imageInfo?.scale ?? 1.0,
);
}
}
测试一下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",
),
)
],
);
}
}
网友评论