Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
wendux committed Jan 24, 2019
2 parents 7ccd279 + a4b9b35 commit df52fd1
Show file tree
Hide file tree
Showing 13 changed files with 39 additions and 38 deletions.
2 changes: 1 addition & 1 deletion docs/chapter1/dart.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dart语言简介

在之前我们已经介绍过Dart语言的相关特性,读者可以翻看一下,如果你熟悉Dart语法,可以跳过本节,如果你还不了解Dart,不用担心,按照笔者经验,如果你有过其他编程语言经验,尤其是Java和JavaScript的话,所以,如果你是前端或Android开发者,那么将会非常容易上手Dart。当然,如果你是iOS开发者,也不用担心,dart中也有一些与swift比较相似的特性,如命名参数等,笔者当时学习Dart时,只是花了一个小时,看完Dart官网的Language Tour,就开始动手写Fluttter了
在之前我们已经介绍过Dart语言的相关特性,读者可以翻看一下,如果你熟悉Dart语法,可以跳过本节,如果你还不了解Dart,不用担心,按照笔者经验,如果你有过其他编程语言经验,尤其是Java和JavaScript的话,所以,如果你是前端或Android开发者,那么将会非常容易上手Dart。当然,如果你是iOS开发者,也不用担心,dart中也有一些与swift比较相似的特性,如命名参数等,笔者当时学习Dart时,只是花了一个小时,看完Dart官网的Language Tour,就开始动手写Flutter了

在笔者看来,Dart的设计目标应该是既对标Java,也对标JavaScript,Dart在静态语法方面和Java非常相似,如类型定义、函数声明、泛型等,而在动态特性方面又和JavaScript很像,如函数式特性、异步支持等。除了融合Java和JavaScript语言之所长之外,Dart也具有一些其它具有表现力的语法,如可选命名参数、`..`(级联运算符)和`?.`(条件成员访问运算符)以及`??`(判空赋值运算符)。其实,对编程语言了解比较多的读者会发现,在Dart中其实看到的不仅有Java和JavaScript的影子,它还具有其它编程语言中的身影,如命名参数在Objective-C和Swift中早就很普遍,而`??`操作符在Php 7.0语法中就已经存在了,因此我们可以看到Google对Dart语言给予厚望,是想把Dart打造成一门集百家之所长的编程语言。

Expand Down
6 changes: 3 additions & 3 deletions docs/chapter1/mobile_development_intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ React中提出一个重要思想:状态改变则UI随之自动改变,而Reac

### React Native

上文已经提到React Native 是React 在原生移动应用平台的衍生产物,那两者主要的区别是什么呢?其实,主要的区别在于虚拟DOM映射的对象是什么?React中虚拟DOM最终会映射为浏览器DOM树,而RN中虚拟DOM会通过JavaScript Core 映射为原生控件树。
上文已经提到React Native 是React 在原生移动应用平台的衍生产物,那两者主要的区别是什么呢?其实,主要的区别在于虚拟DOM映射的对象是什么?React中虚拟DOM最终会映射为浏览器DOM树,而RN中虚拟DOM会通过 JavaScriptCore 映射为原生控件树。

JavaScript Core 是一个JavaScript解释器,它在React Native中主要有两个作用:
JavaScriptCore 是一个JavaScript解释器,它在React Native中主要有两个作用:

1. 为JavaScript提供运行环境。
2. 是JavaScript与原生应用之间通信的桥梁,作用和JsBridge一样,事实上,在iOS中,很多JsBridge的实现都是基于JavaScript Core
2. 是JavaScript与原生应用之间通信的桥梁,作用和JsBridge一样,事实上,在iOS中,很多JsBridge的实现都是基于 JavaScriptCore

而RN中将虚拟DOM映射为原生控件的过程中分两步:

Expand Down
2 changes: 1 addition & 1 deletion docs/chapter13/custom_paint.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class MyPainter extends CustomPainter {

绘制是比较昂贵的操作,所以我们在实现自绘控件时应该考虑到性能开销,下面是两条关于性能优化的建议:

- 尽可能的利用好`shouldRepaint`返回值;在UI树重新build时,控件在绘制前都会先调用该方法以确定是否有必要重绘;加入我们绘制的UI不依赖外部状态,那么就应该始终返回false,因为外部状态改变导致重新build时不会影响我们的UI外观;如果绘制依赖外部状态,那么我们就应该在shouldRepaint中判断依赖的状态是否改变,如果已改变则应返回`true`来重绘,反之则应返回`false`不需要重绘。
- 尽可能的利用好`shouldRepaint`返回值;在UI树重新build时,控件在绘制前都会先调用该方法以确定是否有必要重绘;假如我们绘制的UI不依赖外部状态,那么就应该始终返回false,因为外部状态改变导致重新build时不会影响我们的UI外观;如果绘制依赖外部状态,那么我们就应该在shouldRepaint中判断依赖的状态是否改变,如果已改变则应返回`true`来重绘,反之则应返回`false`不需要重绘。

- 绘制尽可能多的分层;在上面五子棋的示例中,我们将棋盘和棋子的绘制放在了一起,这样会有一个问题:由于棋盘始终是不变的,用户每次落子时变的只是棋子,但是如果按照上面的代码来实现,每次绘制棋子时都要重新绘制一次棋盘,这是没必要的。优化的方法就是将棋盘单独抽为一个Widget,并设置其`shouldRepaint`回调值为false,然后将棋盘Widget作为背景。然后将棋子的绘制放到另一个Widget中,这样落子时只需要绘制棋子。

Expand Down
2 changes: 1 addition & 1 deletion docs/chapter13/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ PaintingContext代表Widget的绘制上下文,通过`PaintingContext.canvas`

### 总结

组合是自定义组件最简单的方法,在任何需要自定义的场景下,都应该优先考虑是否能够通过组合来实现。而自绘和通过实现RenderObject的方法本质上是一样的,都需要开发者调用Canvas API手动去绘制UI,缺点时必须了解Canvas API,并且得自己去实现绘制逻辑,而优点是强大灵活,理论上可以实现任何外观的UI。
组合是自定义组件最简单的方法,在任何需要自定义的场景下,都应该优先考虑是否能够通过组合来实现。而自绘和通过实现RenderObject的方法本质上是一样的,都需要开发者调用Canvas API手动去绘制UI,缺点是必须了解Canvas API,并且得自己去实现绘制逻辑,而优点是强大灵活,理论上可以实现任何外观的UI。

在本章接下来的小节中,我们将通过一些实例来详细介绍自定义UI的过程,由于后两种方法本质是相同的,后续我们只介绍CustomPaint和Canvas的方式,读者如果对自定义RenderObject的方法好奇,可以查看RenderParagraph或RenderImage源码。
15 changes: 8 additions & 7 deletions docs/chapter14/element_buildcontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

### Element

在“Widget简介”一节,我们介绍了Widget和Element的关系,我们知道最终的UI树其实一个个独立的Element节点构成。我们也知道了组件最终的Layout、渲染都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建将相应的RenderObject关联到Element.renderObject属性,最后再通过RenderObject来完成布局排列和绘制。
在“Widget简介”一节,我们介绍了Widget和Element的关系,我们知道最终的UI树其实是由一个个独立的Element节点构成。我们也知道了组件最终的Layout、渲染都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。

Element就是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类,比如MultiChildRenderObjectElement。最终所有Element的RenderObject构成一棵树,我们称之为渲染树,即render tree。

Element的生命周期如下:

1. Framework 调用`Widget.createElement` 创建一个Element实例,记为`element`
2. Framework 调用 `element.mount(parentElement,newSlot)` ,mount方法中首先调用`elment`所对应Widget的`createRenderObject`方法创建与`element`相关联RenderObject对象,然后调用`element.attachRenderObject`方法将`element.renderObject`添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的`element`就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
2. Framework 调用 `element.mount(parentElement,newSlot)` ,mount方法中首先调用`elment`所对应Widget的`createRenderObject`方法创建与`element`相关联的RenderObject对象,然后调用`element.attachRenderObject`方法将`element.renderObject`添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的`element`就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
3.`element`父Widget的配置数据改变时,为了进行Element复用,Framework在决定重新创建Element前会先尝试复用相同位置旧的element:调用Element对应Widget的`canUpdate()`方法,如果返回`true`,则复用旧Element,旧的Element会使用新的Widget配置数据更新,反之则会创建一个新的Element,不会复用。`Widget.canUpdate()`主要是判断`newWidget``oldWidget``runtimeType``key`是否同时相等,如果同时相等就返回`true`,否则就会返回`false`。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来禁止复用。
4. 当有父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来避免复用。
5. 当有祖先Element决定要移除`element ` 时(如Widget树结构发生了变化,导致`element`对应的Widget被移除),这是该祖先Element就会调用`deactivateChild` 方法来移除它,移除后`element.renderObject`也会被从渲染树中移除,然后Framework会调用`element.deactivate ` 方法,这时`element`状态变为“inactive”状态。
5. 当有祖先Element决定要移除`element ` 时(如Widget树结构发生了变化,导致`element`对应的Widget被移除),这时该祖先Element就会调用`deactivateChild` 方法来移除它,移除后`element.renderObject`也会被从渲染树中移除,然后Framework会调用`element.deactivate ` 方法,这时`element`状态变为“inactive”状态。
6. “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成”active“状态,Framework就会调用其`unmount`方法将其彻底移除,这时element的状态为`defunct`,它将永远不会再被插入到树中。
7. 如果`element`要重新插入到Element树的其它位置,如`element``element`的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其`activate`方法,并将其`renderObject`重新attach到渲染树。

Expand Down Expand Up @@ -44,7 +44,7 @@ abstract class BuildContext {
}
```

那StatelessWidget和StatefulWidget的build方法传入的context对象是哪个实现了BuildContext的类。我们顺藤摸瓜,发现调用时发生在StatelessWidget和StatefulWidget对应的StatelessElement和StatefulElement的build犯法中,以StatelessElement为例:
那StatelessWidget和StatefulWidget的build方法传入的context对象是哪个实现了BuildContext的类。我们顺藤摸瓜,发现调用时发生在StatelessWidget和StatefulWidget对应的StatelessElement和StatefulElement的build方法中,以StatelessElement为例:

```dart
Expand Down Expand Up @@ -72,7 +72,7 @@ class Element extends DiagnosticableTree implements BuildContext {

我们可以看到Element是Flutter UI框架内部连接Widget和RenderObject的纽带,大多数时候开发者只需要关注Widget层即可,但是Widget层有时候并不能完全屏蔽Element细节,所以Framework在StatelessWidget和StatefulWidget中通过build方法参数将Element对象也传递给了开发者,这样便可以在需要时直接操作Element对象。那么现在笔者提两个问题,请读者先自己思考一下:

1. 如果没有Widget层,单靠Element层是否可以搭建起一个完成的UI框架?如果可以应该是什么样子?
1. 如果没有Widget层,单靠Element层是否可以搭建起一个可用的UI框架?如果可以应该是什么样子?
2. Flutter UI框架能不做成响应式吗?

对于问题1,答案当然是肯定的,因为我们之前说过Widget树只是Element树的映射,我们完全可以直接通过Element来搭建一个UI框架。下面举一个例子:
Expand Down Expand Up @@ -106,7 +106,7 @@ class HomeView extends ComponentElement{
- 上面build方法不接收参数,这一点和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代码中需要用到BuildContext的地方直接用`this`代替即可,如代码注释1处`Theme.of(this)`参数直接传`this`即可,因为当前对象本身就是Element实例。
-`text`发生改变时,我们调用`markNeedsBuild()`方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,`State.setState()`在内部也是调用的`markNeedsBuild()`方法。

- 上面代码中build方法返回的仍然是一个Widget,这是由于Flutter框架中已经有了Widget这一层,并且组件库都已经是以Widget的形式提供了,如果在Flutter框架中所有组件都想示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了,HomeView的build方法返回值类型就可以是Element了。
- 上面代码中build方法返回的仍然是一个Widget,这是由于Flutter框架中已经有了Widget这一层,并且组件库都已经是以Widget的形式提供了,如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了,HomeView的build方法返回值类型就可以是Element了。

如果我们需要将上面代码在现有Flutter框架中跑起来,那么还是得提供一个”适配器“Widget将HomeView结合到现有框架中,下面CustomHome就相当于”适配器“:

Expand All @@ -125,7 +125,8 @@ class CustomHome extends Widget {

点击按钮则按钮文本会随机排序。

对于问题2,答案当然也是肯定的,Flutter engine提供的dart API是原始且独立的,这个操作系统提供的类似,上层UI框架设计成什么样完全取决于设计者,完全可以将UI框架设计成Android风格或iOS风格,但这些事Google不会再去做,当然没有十足的理由我们也没必要再去搞一套,这是因为响应式的思想本身是很棒的,之所以提出这个问题,是因为笔者认为但做与不做是一回事,但知道能与不能是另一回事,这能反映出我们对知识的掌握程度。
对于问题2,答案当然也是肯定的,Flutter engine提供的dart API是原始且独立的,这个与操作系统提供的API类似,上层UI框架设计成什么样完全取决于设计者,完全可以将UI框架设计成Android风格或iOS风格,但这些事Google不会再去做,我们也没必要再去搞这一套,这是因为响应式的思想本身是很棒的,之所以提出这个问题,是因为笔者认为做与不做是一回事,但知道能不能做是另一回事,这能反映出我们对知识的掌握程度。


### 总结

Expand Down
4 changes: 2 additions & 2 deletions docs/chapter14/flutter_ui_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ CPU和GPU的任务是各有偏重的,CPU主要用于基本数学和逻辑计

### Flutter UI系统

我们可以看到,无论是Android SDK还是iOS的UIKit 的职责都是相同的,它们只是语言载体和操作系统不同而已。那么可不可以实现这么一个UI系统:可以使用同一种编程语言开发,然后针对不同操作系统API抽象一个对上接口一致对下适配不同操作系统的的中间层,然后在打包编译时再使用相应的中间层代码?如果可以做到,那么我们就可以使用同一套代码编写跨平台的应用了。而Flutter的原理正是如此,它提供了一套Dart API,然后再底层通过OpenGL这种跨平台的绘制库(内部会调用操作系统API)实现了一套代码跨多端。由于Dart API也是调用操作系统API,所以它的性能接近原生。
我们可以看到,无论是Android SDK还是iOS的UIKit 的职责都是相同的,它们只是语言载体和底层的系统不同而已。那么可不可以实现这么一个UI系统:可以使用同一种编程语言开发,然后针对不同操作系统API抽象一个对上接口一致对下适配不同操作系统的的中间层,然后在打包编译时再使用相应的中间层代码?如果可以做到,那么我们就可以使用同一套代码编写跨平台的应用了。而Flutter的原理正是如此,它提供了一套Dart API,然后在底层通过OpenGL这种跨平台的绘制库(内部会调用操作系统API)实现了一套代码跨多端。由于Dart API也是调用操作系统API,所以它的性能接近原生。

> 注意,虽然Dart是先调用了OpenGL,OpenGL才会调用操作系统API,但是这仍然是原生渲染,因为OpenGL只是操作系统API的一个封装库,它并不像WebView渲染那样需要JavaScript运行环境和CSS渲染器,所以不会有性能损失。
至此,我们已经介绍了Flutter UI系统和操作系统交互的这一部分原理,先在需要说一些他对应用开发者定义的开发标准。其实在前面的章节中,我们已经对这个标准非常熟悉了, 简单概括就是:组合和响应式。我们要开发一个UI界面,需要通过组合其它Widget来实现,Flutter中,一切都是Widget,当UI要发生变化时,我们不去直接修改DOM,而是通过更新状态,让Flutter UI系统来根据新的状态来重新构建UI。
至此,我们已经介绍了Flutter UI系统和操作系统交互的这一部分原理,现在需要说一些它对应用开发者定义的开发标准。其实在前面的章节中,我们已经对这个标准非常熟悉了, 简单概括就是:组合和响应式。我们要开发一个UI界面,需要通过组合其它Widget来实现,Flutter中,一切都是Widget,当UI要发生变化时,我们不去直接修改DOM,而是通过更新状态,让Flutter UI系统来根据新的状态来重新构建UI。

讲到这里,读者可能发现Flutter UI系统和Flutter Framework的概念是差不多的,的确如此,之所以用“UI系统”,是因为其他平台中可能不这么叫,我们只是为了概念统一,便于描述,读者不必纠结于概念本身。

Expand Down
Loading

0 comments on commit df52fd1

Please sign in to comment.