├── .DS_Store ├── flutter.pdf ├── imgs ├── .DS_Store ├── dartvm.png ├── hybrid.png ├── onError.png ├── render.png ├── dartbridge.png ├── device_ob.png ├── channeltypes.png ├── debug_break.png ├── eventlooper.webp ├── plugin_depen.png ├── device-obv-url.png ├── fluttercreate.png ├── frameworkarch.png ├── plugin_depen1.png ├── settings-gradle.png └── errorwidgetsource.png └── flutter.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/.DS_Store -------------------------------------------------------------------------------- /flutter.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/flutter.pdf -------------------------------------------------------------------------------- /imgs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/.DS_Store -------------------------------------------------------------------------------- /imgs/dartvm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/dartvm.png -------------------------------------------------------------------------------- /imgs/hybrid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/hybrid.png -------------------------------------------------------------------------------- /imgs/onError.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/onError.png -------------------------------------------------------------------------------- /imgs/render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/render.png -------------------------------------------------------------------------------- /imgs/dartbridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/dartbridge.png -------------------------------------------------------------------------------- /imgs/device_ob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/device_ob.png -------------------------------------------------------------------------------- /imgs/channeltypes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/channeltypes.png -------------------------------------------------------------------------------- /imgs/debug_break.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/debug_break.png -------------------------------------------------------------------------------- /imgs/eventlooper.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/eventlooper.webp -------------------------------------------------------------------------------- /imgs/plugin_depen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/plugin_depen.png -------------------------------------------------------------------------------- /imgs/device-obv-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/device-obv-url.png -------------------------------------------------------------------------------- /imgs/fluttercreate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/fluttercreate.png -------------------------------------------------------------------------------- /imgs/frameworkarch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/frameworkarch.png -------------------------------------------------------------------------------- /imgs/plugin_depen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/plugin_depen1.png -------------------------------------------------------------------------------- /imgs/settings-gradle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/settings-gradle.png -------------------------------------------------------------------------------- /imgs/errorwidgetsource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaomaicheng/feblog/HEAD/imgs/errorwidgetsource.png -------------------------------------------------------------------------------- /flutter.md: -------------------------------------------------------------------------------- 1 | # Flutter 在铭师堂的实践 2 | 3 | ## 简介 4 | 5 | Flutter 是 Google 的一套跨平台 UI 框架。目前已经是 1.7 的 Release 版本。在移动端双端投入人力较大,短期紧急需求的背景下。跨端技术会成为越来越多的移动端技术栈选择。铭师堂移动端团队在过去几个月,对 Flutter 技术做了一些尝试和工作。这篇文章将会对 Flutter 的基本原理和我们在 `升学e网通 APP` 的工程实践做一个简单的分享。 6 | 7 | ## Flutter 的架构和原理 8 | 9 | Flutter framework 层的架构图如下: 10 | 11 | 12 | ![](https://user-gold-cdn.xitu.io/2019/5/31/16b0d208ec0130fc?w=1692&h=626&f=png&s=35353) 13 | 14 | s 15 | **Foundation**: foundation 提供了 framework 经常使用的一些基础类,包括但不限于: 16 | 17 | * BindBase: 提供了提供单例服务的对象基类,提供了 Widgets、Render、Gestures等能力 18 | 19 | * Key: 提供了 Flutter 常用的 Key 的基类 20 | 21 | * AbstractNode:表示了控件树的节点 22 | 23 | 在 foundation 之上,Flutter 提供了 动画、绘图、手势、渲染和部件,其中部件就包括我们比较熟悉的 Material 和 Cupertino 风格 24 | 25 | 我们从 dart 的入口处关注 Flutter 的渲染原理 26 | 27 | ```dart 28 | void runApp(Widget app) { 29 | WidgetsFlutterBinding.ensureInitialized() 30 | ..attachRootWidget(app) 31 | ..scheduleWarmUpFrame(); 32 | } 33 | ``` 34 | 35 | 我们直接使用了 Widgets 层的能力 36 | 37 | ### widgets 38 | 负责根据我们 dart 代码提供的 Widget 树,来构造实际的虚拟节点树 39 | 40 | 在 FLutter 的渲染机制中,有 3 个比较关键的概念: 41 | 42 | * Widget: 我们在 dart 中直接编写的 Widget,表示控件 43 | * Element:实际构建的虚拟节点,所有的节点构造出实际的控件树,概念是类似前端经常提到的 vitrual dom 44 | * RenderObject: 实际负责控件的视图工作。包括布局、渲染和图层合成 45 | 46 | 47 | 根据 `attachRootWidget` 的流程,我们可以了解到布局树的构造流程 48 | 49 | 1. `attachRootWidget` 创建根节点 50 | 2. `attachToRenderTree` 创建 root Element 51 | 3. Element 使用 `mount` 方法把自己挂载到父 Element。这里因为自己是根节点,所以可以忽略挂载过程 52 | 4. `mount` 会通过 `createRenderObject` 创建 root Element 的 RenderObject 53 | 54 | 到这里,整颗 tree 的 root 节点就构造出来了,在 `mount` 中,会通过 `BuildOwner#buildScope` 执行子节点的创建和挂载, 这里需要注意的是 child 的 RenderObject 也会被 attach 到 parent 的 RenderObejct 上去 55 | 56 | 整个过程我们可以通过下图表示 57 | 58 | ![](https://user-gold-cdn.xitu.io/2019/5/31/16b0d2112ee56297?w=828&h=278&f=png&s=55732) 59 | 60 | 感兴趣可以参考 `Element`、`RenderObjectElement`、`RenderObject` 的源码 61 | 62 | 63 | ### 渲染 64 | 负责实际整个控件树 RenderObject 的布局和绘制 65 | 66 | runApp 后会执行 `scheduleWarmUpFrame` 方法,这里就会开始调度渲染任务,进行每一帧的渲染 67 | 68 | 从 `handleBeginFrame` 和 `handleDrawFrame` 会走到 binding 的 `drawFrame` 函数,依次会调用 `WidgetsBinding` 和 `RendererBinding` 的 `drawFrame`。 69 | 70 | 这里会通过 Element 的 `BuildOwner`,去重新塑造我们的控件树。 71 | 72 | 大致原理如图 73 | 74 | ![](https://user-gold-cdn.xitu.io/2019/5/31/16b0d212987ebef6?w=1006&h=718&f=png&s=163796) 75 | 76 | 在构造或者刷新一颗控件树的时候,我们会把有改动部分的 Widget 标记为 dirty,并针对这部分执行 rebuild,但是 Flutter 会有判断来保证尽量复用 Element,从而避免了反复创建 Element 对象带来的性能问题。 77 | 78 | 在对 dirty elements 进行处理的时候,会对它进行一次排序,排序规则参考了 element 的深度: 79 | 80 | ```dart 81 | static int _sort(Element a, Element b) { 82 | if (a.depth < b.depth) 83 | return -1; 84 | if (b.depth < a.depth) 85 | return 1; 86 | if (b.dirty && !a.dirty) 87 | return -1; 88 | if (a.dirty && !b.dirty) 89 | return 1; 90 | return 0; 91 | } 92 | ``` 93 | 94 | 根据 depth 排序的目的,则是为了保证子控件一定排在父控件的左侧, 这样在 build 的时候,可以避免对子 widget 进行重复的 build。 95 | 96 | 在实际渲染过程中,Flutter 会利用 **Relayout Boundary**机制 97 | 98 | ```dart 99 | void markNeedsLayout() { 100 | // ... 101 | if (_relayoutBoundary != this) { 102 | markParentNeedsLayout(); 103 | } else { 104 | _needsLayout = true; 105 | if (owner != null) { 106 | owner._nodesNeedingLayout.add(this); 107 | owner.requestVisualUpdate(); 108 | } 109 | } 110 | //... 111 | } 112 | ``` 113 | 114 | 在设置了 relayout boundary 的控件中,只有子控件会被标记为 needsLayout,可以保证,刷新子控件的状态后,控件树的处理范围都在子树,不会去重新创建父控件,完全隔离开。 115 | 116 | 在每一个 RendererBinding 中,存在一个 `PipelineOwner` 对象,类似 WidgetsBinding 中的 `BuildOwner`. `BuilderOwner` 负责控件的build 流程,`PipelineOwner` 负责 render tree 的渲染。 117 | 118 | ```dart 119 | @protected 120 | void drawFrame() { 121 | assert(renderView != null); 122 | pipelineOwner.flushLayout(); 123 | pipelineOwner.flushCompositingBits(); 124 | pipelineOwner.flushPaint(); 125 | renderView.compositeFrame(); // this sends the bits to the GPU 126 | pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. 127 | } 128 | ``` 129 | 130 | RenderBinding 的 `drawFrame` 实际阐明了 render obejct 的渲染流程。即 布局(layout)、绘制(paint)、合成(compositeFrame) 131 | 132 | 133 | 134 | ### 调度(scheduler和线程模型) 135 | 136 | 在布局和渲染中,我们会观察到 Flutter 拥有一个 `SchedulerBinding`,在 frame 变化的时候,提供 callback 进行处理。不仅提供了帧变化的调度,在 `SchedulerBinding` 中,也提供了 task 的调度函数。这里我们就需要了解一下 dart 的异步任务和线程模型。 137 | 138 | dart 的单线程模型,所以在 dart 中,没有所谓的主线程和子线程说法。dart 的异步操作采取了 event-looper 模型。 139 | 140 | 141 | dart 没有线程的概念,但是有一个概念,叫做 isolate, 每个 isolate 是互相隔离的,不会进行内存的共享。在 main isolate 的 main 函数结束之后,会开始一个个处理 event queue 中的 event。也就是,dart 是先执行完同步代码后,再进行异步代码的执行。所以如果存在非常耗时的任务,我们可以创建自己的 isolate 去执行。 142 | 143 | 144 | 每一个 isolate 中,存在 2 个 event queue 145 | * Event Queue 146 | * Microtask Queue 147 | 148 | event-looper 执行任务的顺序是 149 | 150 | 1. 优先执行 Microtask Queue 中的task 151 | 2. Microtask Queue 为空后,才会执行 Event Queue 中的事件 152 | 153 | flutter 的异步模型如下图 154 | 155 | ![](https://user-gold-cdn.xitu.io/2019/5/31/16b0d21377fe5c4c?w=471&h=506&f=webp&s=10902) 156 | 157 | ### Gesture 158 | 159 | 每一个 GUI 都离不开手势/指针的相关事件处理。 160 | 161 | 在 GestureBiding 中,在 `_handlePointerEvent` 函数中,`PointerDownEvent` 事件每处理一次,就会创建一个 `HintTest` 对象。在 `HintTest` 中,会存有每次经过的控件节点的 path。 162 | 163 | 最终我们也会看到一个 `dispatchEvent` 函数,进行事件的分发以及 `handleEvent`,对事件进行处理。 164 | 165 | 在根节点的 renderview 中,事件会开始从 `hitTest` 处理,因为我们添加了事件的传递路径,所以,时间在经过每个节点的时候,都会被”处理“。 166 | 167 | ```dart 168 | @override // from HitTestDispatcher 169 | void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) { 170 | if (hitTestResult == null) { 171 | assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent); 172 | try { 173 | pointerRouter.route(event); 174 | } catch (exception, stack) { 175 | } 176 | return; 177 | } 178 | for (HitTestEntry entry in hitTestResult.path) { 179 | try { 180 | entry.target.handleEvent(event, entry); 181 | } catch (exception, stack) { 182 | } 183 | } 184 | } 185 | ``` 186 | 187 | 这里我们就可以看出来 Flutter 的时间顺序,从根节点开始分发,一直到子节点。同理,时间处理完后,会沿着子节点传到父节点,最终回到 `GestureBinding`。 188 | 这个顺序其实和 Android 的 View 事件分发 和 浏览器的事件冒泡 是一样的。 189 | 190 | 通过 `GestureDector` 这个 Widget, 我们可以触发和处理各种这样的事件和手势。具体的可以参考 Flutter 文档。 191 | 192 | ### Material、Cupertino 193 | 194 | Flutter 在 Widgets 之上,实现了兼容 Andorid/iOS 风格的设计。让APP 在 ui/ue 上有类原生的体验。 195 | 196 | 197 | ## Flutter 的工程实践 198 | 199 | 根据我们自己的实践,我从 **混合开发**、**基础库建设**和**日常的采坑**的角度,分享一些我们的心得体会。 200 | 201 | ### 混合工程 202 | 203 | 我们的 APP 主题大部分是 native 开发完成的。为了实践 Flutter,我们就需要把 Flutter 接入到原生的 APP 里面去。并且能满足如下需求: 204 | 205 | * 对不参与 Flutter 实践的原生开发同学不产生影响。不需要他们去安装 Flutter 开发环境 206 | * 对于参与 FLutter 的同学来说,我们要共享一份dart 代码,即共享一个代码仓库 207 | 208 | 我们的原生架构是多 module 组件化,每个 module 是一个 git 仓库,使用 google git repo 进行管理。以 Android 工程为例,为了对原生开发没有影响。最顺理成章的思路就是,提供一个 aar 包。对于 Android 的视角来说,flutter 其实只是一个 flutterview,那么我们按照 flutter 的工程结构自己创建一个相应的 module 就好了。 209 | 210 | 我们查看 `flutter create` 创建的flutter project的Andorid的 `build.gradle`,可以找到几个关键的地方 211 | 212 | app的`build.gradle` 213 | 214 | ```grovvy 215 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 216 | 217 | flutter { 218 | source '../..' 219 | } 220 | ``` 221 | 222 | 这里制定了 flutter 的gradle,并且制定了 flutter 的source 文件目录。 223 | 224 | 我们可以猜测出来,flutter相关的构建和依赖,都是 flutter 的gradle 文件里面帮我们做的。那么在我们自己创建的原生 module 内部,也用同样的方式去组织。就可以了。 225 | 226 | 同时,我们可以根据自己的实际去制定 flutter 的 source 路径。也通过 repo 将原生的module 和 dart 的lib目录,分成2个git仓库。就完美实现了代码的隔离。对于原生开发来说,后面的构建打包等持续集成都不会收到 flutter 的影响。 227 | 228 | 混合工程的架构如下: 229 | 230 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/hybrid.png) 231 | 232 | ### 混合工程启动和调试 233 | 在一个 flutter 工程中,我们一般是使用 `flutter run` 命令启动一个 flutter 应用。这时候我们就会有关注到:混合工程中,我们进入app会先进入原生页面,如何再进入 flutter 页面。那么我们如何使用热重载和调试功能呢。 234 | 235 | **热重载** 236 | 237 | 以 Andorid 为例,我们可以先给 app 进行 `./gradlew assembleDebug` 打出一个 apk 包。 238 | 239 | 然后使用 240 | ```shell 241 | flutter run --use-application-binary {debug apk path} 242 | ``` 243 | 命令。会启动我们的原生 app, 进入特定的 flutter 入口页面,命令行会自动出现 flutter 的 hot reload。 244 | 245 | **混合工程调试** 246 | 247 | 那么我们如何进行 flutter 工程的调试呢?我们可以通过给原生的端口和移动设备的 `Observatory` 端口进行映射。其实这个方法也同样适用于我们运行了一个纯 flutter 应用,想通过类似 attach 原生进程的方式里面开始断点。 248 | 249 | 命令行启动app, 出现flutter 的hotreload 后,我们可以看到 250 | 251 | ``` 252 | An Observatory debugger and profiler on Android SDK built for x86 is available at: 253 | http://127.0.0.1:54946/ 254 | ``` 255 | 256 | 这端。这个地址,我们可以打开一个关于 dart 的性能和运行情况的展示页面。 257 | 258 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/dartvm.png) 259 | 260 | 我们记录下这个端口 xxxx 261 | 262 | 然后通过 `adb logcat | grep Observatory` 查看手机的端口,可以看到如下输出 263 | 264 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/device_ob.png) 265 | 266 | 我们把最后一个地址输入到手机的浏览器,可以发现手机上也可以打开这个页面 267 | 268 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/device-obv-url.png) 269 | 270 | 我们可以理解成这里是做了一次端口映射,设备上的端口记录为 yyyy 271 | 272 | 在 Android Studio 中,我们在 run -> Edit Configurations 里面,新建一个 `dart remote debug`, 填写 xxxx 端口。 273 | 274 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/debug_break.png) 275 | 276 | 如果不成功,可以手动 forward 一下 277 | 278 | ``` 279 | adb forward tcp:xxxx tcp:yyyy 280 | ``` 281 | 282 | 然后启动这个调试器,就可以进行 dart 的断点调试了。 283 | 284 | ### 原生能力和插件开发 285 | 286 | 在 flutter 开发中,我们需要经常使用原生的功能,具体的可以参考 [官方文档](https://flutter-io.cn/docs/development/platform-integration/platform-channels), native 和 flutter 通过传递消息,来实现互相调用。 287 | 288 | 架构图如下 289 | ![](https://flutter-io.cn/images/PlatformChannels.png) 290 | 291 | 查看源码,可以看到 flutter 包括 4 中 Channel 类型。 292 | 293 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/channeltypes.png) 294 | 295 | * `BasicMessageChannel` 是发送基本的信息内容的通道 296 | * `MethodChannel`和 `OptionalMethodChannel`是发送方法调用的通道 297 | * `EventChannel` 是发送事件流 `stream` 的通道。 298 | 299 | 在 Flutter 的封装中,官方对纯 Flutter 的 library 定义为 `Package`, 对调用了原生能力的 libraray 定义为 `Plugin`。 300 | 301 | 官方同时也提供了 `Plugin` 工程的脚手架。通过 `flutter create --org {pkgname} --template=plugin xx` 创建一个 `Plugin` 工程。内部包括三端的 library 代码,也包括了一个 `example` 目录。里面是一个依赖了此插件的 flutter 应用工程。具体可以参考[插件文档](https://flutter-io.cn/docs/development/packages-and-plugins/using-packages) 302 | 303 | 在实践中,我们可以发现 Plugin 的依赖关系如下。 304 | 例如我们的 Flutter 应用叫 `MyApp`, 里面依赖了一个 `Plugin` 叫做 `MyPlugin`。那么,在 Andorid APP 中,库依关系如下图 305 | 306 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/plugin_depen.png) 307 | 308 | 但是如果我们在创建插件工程的时候,原生部分代码,不能依赖到插件的原生 aar。这样每次编译的时候就会在 `GeneratedPluginRegistrant` 这个类中报错,依赖关系就变成了下图 309 | 310 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/plugin_depen1.png) 311 | 312 | 我们会发现红色虚线部分的依赖在插件工程中是不存在的。 313 | 314 | 仔细思考一下会发现,其实我们在 Flutter 应用工程中使用 `Plugin` 的时候,只是在 `pubspec.yaml` 中添加了插件的依赖。原生部分是怎么依赖到插件的呢? 315 | 316 | 通过比较 `flutter create xx`(应用工程) 和 `flutter create --template=plugin` (插件工程) ,我们会发现在`settings.gradle` 中有一些不一样。应用工程中,有如下一段自动生成的 gradle 代码 317 | 318 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/settings-gradle.png) 319 | 320 | gradle 会去读取一个 `.flutter-plugins` 文件。从这里面读取到插件的原生工程地址,include 进来并制定了 path。 321 | 322 | 我们查看一个 `.flutter-plugins` 文件: 323 | 324 | ```shell 325 | path_provider=/Users/chenglei/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.1.0/ 326 | 327 | ``` 328 | 329 | 我们也可以大致猜测到,flutter的 gradle 脚本里面会把自己include进来的插件工程全部依赖一遍。 330 | 331 | 从这个角度,我们发现插件工程开发还是有一些规则上的限制的。 332 | 从开发的角度看,必须遵循脚手架的规范编写代码。如果依赖其他的插件,必须自己写脚本解决上面的依赖问题。 333 | 从维护的角度看,插件工程仍然需要至少一个android 同学 加一个 iOS 同学进行维护。 334 | 335 | 所以我们在涉及原生的 Flutter 基础库开发中,没有采用原生工程的方式。而是通过独立的 fluter package、独立的android ios module打二进制包的形式。 336 | 337 | ### flutter基础设施之路 338 | 339 | 基于上一小节的结论,我们开发了自己的一套 flutter 基础设置。我们的基建大致从下面几个角度出发 340 | 341 | * 利用现有能力:基于 Channel 调用原生的能力,例如网络、日志上报。可以收拢 APP 中这些基础操作 342 | * 质量和稳定性:Flutter 是新技术,我们如何在它上线的时候做到心中有底 343 | * 开发规范:从早期就定下第一版的代码结构、技术栈选择,对于后面的演进益大于弊 344 | 345 | #### 利用现有能力 346 | 347 | 我们封装了 `Channel`,开发了一个 `DartBridge` 框架。负责原生和 Dart 的互相调用。在此之上,我们开发了网络库、统一跳转库等基础设施 348 | 349 | ##### DartBridge 350 | 351 | 反观 `e网通` APP 在 webview 的通信,是在消息到达另一端后,通过统一的路由调用格式进行路由调用。对于路由提供方来说,只识别路由协议,不关心调用端是哪一段。在一定程度上,我们也可以把统一的路由协议理解为“跨平台”。我们内部协议的格式是如下形式: 352 | 353 | `scheme://{"domain":"", "action":"", "params":""}` 354 | 355 | 所以在 Flutter 和原生的通信中,结合实际业务场景,我们没有使用 `MethodChannel`,而是使用了 `BasicMessageChannel`, 通过这一个 channel,发送最基本的路由协议。被调用方收到后,调用各自的路由库,返回调用结果给通道。我们封装了一套 **DartBridge** 来进行消息的传递。 356 | 357 | 通过阅读源码我们可以发现,Channel 的设计非常的完美。它解耦了消息的编解码方式,在 `Codec` 对象中,我们可以进行我们的自定义编码,例如序列化为 json 对象的 `JsonMessageCodec`。 358 | 359 | ```dart 360 | var _dartBridgeChannel = BasicMessageChannel(DART_BRIDGE_CHANNEL,JSONMessageCodec()); 361 | ``` 362 | 363 | 在实际开发中,我们可能想要查询消息内容。如果消息的内容是获取原生的内容,例如一个学生的作业总数,我们希望在原生提供服务前,不阻塞自己的开发。并且在不修改业务代码的情况下获取到路由的mock数据。所以我们在路由的内部增加了拦截器和mock服务的功能。在sdk初始化的时候,我们可以通过对象配置的方式,配置一些对应 domain、action的mock数据。 364 | 365 | 整个 DartBridge 的架构如下 366 | 367 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/dartbridge.png) 368 | 369 | 基于这个架构模型,我们收到消息后,通过原生路由(例如 ARouter)方案,去进行相应的跳转或者服务调用。 370 | 371 | ##### 网络库 EIO 372 | 373 | Flutter 提供了自己的http 包。但是集成到原生app的时候,我们仍然希望网络这个基础操作的口子可以被统一管理。包括统一的https支持,统一的网络拦截操作,以及可能进行的统一网络监控和调优。所以在Android中,网络库我们选择调用 OKHttp。 374 | 375 | 但是考虑到如果有新的业务需求,我们开发了一个全新的flutter app,也希望在不更改框架层的代码,就可以直接移植过去,并且脱离原生的请求。 376 | 377 | 这就意味着网络架构需要把 `网络配置` 和 `网络引擎` 解耦开。本着不重复造轮子的原则,我们发现了一个非常优秀的框架:`DIO` 378 | 379 | DIO 留下了一个 `HttpClientAdapter` 类,进行网络请求的自定义。 380 | 381 | 我们实现了这个类,在 `fetch()` 函数中,通过 `DartBridge`,对原生的网络请求模块进行调用。返回的数据是一个包括: 382 | 383 | * nativeBytes List 网络数据的字节流 384 | * statusCode 网络请求的 http code 385 | * headers Map 网络的 response headers 386 | 387 | 这些数据,通过 Okhttp 请求可以获取。这里有一个细节问题。在 OkHttp 中,请求到的 bytes是一个 byte[], 直接给到dart 这边,被我强转成了一个List, 因为java 中 byte的范围是 -126 - 127 ,所以这时候,就出现了乱码。 388 | 389 | 通过对比实际的dart dio请求到的相同的字节流,我发现,byte中的一些数据转换成int的时候发生了溢出,变成了负数,产生了乱码。正好是做一次补码运算,就成了正确的。所以。我在 dart 端,对数据做了一次统一的转化: 390 | 391 | ```dart 392 | nativeBytes = nativeBytes.map((it) { 393 | if (it < 0) { 394 | return it + 256; 395 | } else { 396 | return it; 397 | } 398 | }).toList(); 399 | ``` 400 | 401 | 关于 utf8 和 byte 具体的编解码过程,我们不做赘述。感兴趣的同学可以参考一下[这篇文章](https://blog.csdn.net/sinat_38816924/article/details/78438070) 402 | 403 | #### 统一路由跳转 404 | 405 | 在 `DartBridge` 框架的基础上,我们对接原生的路由框架封装了我们自己的统一跳转。目前我们的架构还比较简单,采用了还是多容器的架构,在业务上去规避这点。我们的容器页面其实就是一个 `FlutterActivity`,我们给容器也设置了一个 path,原生在跳转flutter的时候,其实是跳转到了这个容器页。在容器页中,拿到我们实际的 Flutter path 和 参数。伪代码如下: 406 | 407 | ```kotlin 408 | val extra = intent?.extras 409 | extra?.let { 410 | val path = it.getString("flutterPath") ?: "" 411 | val params = HashMap() 412 | extra.keySet().forEach { key -> 413 | extra[key]?.let { value -> 414 | params[key] = value.toString() 415 | } 416 | } 417 | path.isNotEmpty().let { 418 | // 参数通过 bridge 告诉flutter的第一个 widget 419 | // 在flutter页面内实现真正的跳转 420 | DartBridge.sendMessage("app", "gotoFlutter",HashMap().apply { 421 | put("path", path) 422 | put("params", params) 423 | }, {success-> 424 | Log.e("native跳转flutter成功", success.toString()) 425 | }, { code, msg-> 426 | Log.e("native跳转flutter出错", "code:$code;msg:$msg") 427 | }) 428 | } 429 | } 430 | ``` 431 | 432 | 那么,业务在原生跳往 Flutter 页面的时候,我们每次都需要知道容器页面的path吗,很明显是不能这样的。 433 | 所以我们在上面叙述的基础上,抽象了一个 flutter 子路由表。进行单独维护。 434 | 业务只需要跳往自己的子路由表内的 path,在 SDK内部,会把实际的path 替换成容器的 path,把路由表 path 和跳转参数整体作为实际的参数。 435 | 436 | 在 Andorid 中,我提供了一个 `pretreatment` 函数,在 `ARouter` 的 `PretreatmentService` 中调用进行处理。返回最终的路由 path 和 参数。 437 | 438 | ### 质量和稳定性 439 | 440 | **线上开关** 441 | 442 | 为了保证新技术的稳定,在 Flutter 基础 SDK 中,我们提供了一个全局开关的配置。这个开关目前还是高粒度的,控制在进入 Flutter 页面的时候是否跳转容器页。 443 | 在开关处理的初始化中,需要提供 2 个参数 444 | * 是否允许线上打开 Flutter 页面 445 | * 在不能打开 Flutter 页面的时候,提供一个 Flutter 和 native 页面的路由映射表。跳转到对应的原生页面或者报错页。 446 | 447 | 线上开关可以和 APP 现有的无线配置中心对接。如果线上出现 Flutter 的质量问题。我们可以下发配置来控制页面跳转实现降级。 448 | 449 | **异常收集** 450 | 451 | 在原生开发中,我们会使用例如 `bugly` 之类的工具查看线上收集的 crash 异常堆栈。Flutter 我们应该怎么做呢?在开发阶段,我们经常会发现 Flutter 出现一个报错页面。 452 | 阅读源码,我们可以发现其实这个错误的显示是一个 Widget: 453 | 454 | 在 `ComponentElement` 的 `performRebuild` 函数中有如下调用 455 | 456 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/errorwidgetsource.png) 457 | 458 | 在调用 build 方法 ctach 到异常的时候,会返回显示一个 `ErrorWidget`。进一步查看会发现,它的 builder 是一个 static 的函数表达式。 459 | 460 | `(FlutterErrorDetails details) => ErrorWidget(details.exception)` 461 | 462 | 它的参数最终也返回了一个私有的函数表达式 `_debugReportException` 463 | 464 | ![](https://github.com/shaomaicheng/feblog/raw/master/imgs/onError.png) 465 | 466 | 最终这里会调用 onError 函数,可以发现它也是一个 static 的函数表达式 467 | 468 | 那么对于异常捕获,我们只需要重写下面 2 个函数就可以进行 build 方法中的视图报错 469 | 470 | * `ErrorWidget.builder` 471 | ```dart 472 | ErrorWidget.builder = (details) { 473 | return YourErrorWidget(); 474 | }; 475 | ``` 476 | * `FlutterError.onError` 477 | ```dart 478 | FlutterError.onError = (FlutterErrorDetails details) { 479 | // your log report 480 | }; 481 | ``` 482 | 483 | 到这一步,我们进行了视图的异常捕获。在 dart 的异步操作中抛出的异常又该如何捕获呢。查询资料我们得到如下结论: 484 | 485 | 在 Flutter 中有一个 `Zone` 的概念,它代表了当前代码的异步操作的一个独立的环境。Zone 是可以捕获、拦截或修改一些代码行为的 486 | 487 | 最终,我们的异常收集代码如下 488 | 489 | ```dart 490 | 491 | void main() { 492 | runMyApp(); 493 | } 494 | 495 | runMyApp() { 496 | ErrorHandler.flutterErrorInit(); // 设置同步的异常处理需要的内容 497 | runZoned(() => runApp(MyApp()), // 在 zone 中执行 MyApp 498 | zoneSpecification: null, 499 | onError: (Object obj, StackTrace stack) { 500 | // Zone 中的统一异常捕获 501 | ErrorHandler.reportError(obj, stack); 502 | }); 503 | } 504 | ``` 505 | 506 | ### 开发规范 507 | 508 | 在开发初期,我们就内部商议定下了我们的 Flutter 开发规范。重点在代码的组织结构和状态管理库。 509 | 开发结构我们考虑到未来有新增多数 Flutter 代码的可能,我们选择按照业务分模块管理各自的目录。 510 | 511 | ``` 512 | . 513 | +-- lib 514 | | +-- main.dart 515 | | +-- README.md 516 | | +-- business 517 | | +-- business1 518 | | +-- module1 519 | | +-- business1.dart 520 | | +-- store 521 | | +-- models 522 | | +-- pages 523 | | +-- widgets 524 | | +-- repositories 525 | | +-- common 526 | | +-- ui 527 | | +-- utils 528 | | +--comlib 529 | | +-- router 530 | | +-- network 531 | ``` 532 | 533 | 在每个业务中,根据页面和具体的视图模块,分为了 `page` 和 `widgets` 的概念。`store` 中,我们会存放相关的状态管理。`repositories` 中我们要求业务把各自的逻辑和纯异步操作抽象为独立的一层。每个业务早期可以维护一个自己的 common, 可以在迭代中不停的抽象自己的 pakcage,并沉淀到最终面向每个人的 comlib。这样,基本可以保证在迭代中避免大家重复造轮子导致的代码冗余混乱。 534 | 535 | 在状态管理的技术选型上,我们调研了包括 `Bloc`、'redux` 和 `mobx`。我们的结论是 536 | 537 | * `flutter-redux` 的概念和设计非常的优秀,但是适合统一的全局状态管理,其实和组件的分割又有很大的矛盾。在开源方案中,我们发现 `fish-redux` 很好的解决了这个问题。 538 | * `Bloc` 的大致思路其实和 redux 有很高的相似度。但是功能还是不如 redux 多。 539 | * `mobx`,代码简单,上手快。基本上搞清楚 `Observables`、`Actions`和`Reactions`几个概念就可以愉快的开发。 540 | * 541 | 最终处于上手成本和代码复杂度的考虑,我们选择了 mobx 作为我们的状态管理组件。 542 | 543 | ## 总结 544 | 到这里,我分享了一些 Flutter 的原理和我们的一些实践。希望能和一些正在研究 Flutter 的同学进行交流和学习。我们的 Flutter 在基础设施开发的同时,还剥离编写了一些 `升学e网通` APP 上的页面和一些基础的 ui 组件库。在未来我们会尝试在一些老的页面中,上线 Flutter 版本。并且研究更好的基础库、异常收集平台、工具链优化和单容器相关的内容。 --------------------------------------------------------------------------------