阅读 1976

我是如何学习Flutter源码的

前言

接触到Flutter以后,我第一时间尝了一下鲜。那时还没有详细的中文文档,只有Flutter的官网上的英文文档可参考。边看边学,捣鼓了些Flutter app以及插件。然后把这个过程中的一些心得体会总结出来几篇文章:

Android开发者的Flutter入门(一)

Android开发者的Flutter入门(二)

Flutter如何和Native通信-Android视角

Flutter插件(Plugin)开发 - Android视角

用Flutter的Canvas来自己绘制柱状频谱图

总的感觉就是Flutter是如此简洁却又如此的强大。在熟悉了FLutter app的开发之后,自然开始对Flutter框架如何运作产生了兴趣。于是花了一些时间研究了一下Flutter框架的源码,写了一系列Flutter源码分析的文章作为一个总结:

《Flutter框架分析(一)-- 总览和Window》

《Flutter框架分析(二)-- 初始化》

《Flutter框架分析(三)-- Widget,Element和RenderObject》

《Flutter框架分析(四)-- Flutter框架的运行》

《Flutter框架分析(五)-- 动画》

《Flutter框架分析(六)-- 布局》

《Flutter框架分析(七)-- 绘制》

近日有掘友提出能否分享一下学习Flutter源码的一些方法和技巧,我觉得这是个很好的建议,于是就有了这篇文章供大家参考。希望能帮到想研究一下框架源码的同学,也希望大家如果有自己的一些阅读框架源码的经验也能分享出来,共同学习提高。

本篇文章把学习Flutter框架源码分为3个阶段,学习前,学习中和学习后。学习前的阶段主要是介绍一些在开始看框架源码之前我自己觉得有帮助的一些准备工作;学习中阶段主要会介绍在看看框架源码的时候一些可能有用的方法或技巧;学习后阶段主要会介绍在理解了框架源码的基础上可以做一些事情来巩固提高自己对其的理解深度。

那么我们就开始吧。

学习前

在开始看框架源码之前,我会做一些准备工作,直到这些条件都具备了,再开始正式学习源码,会有事半功倍的效果。

有强烈的兴趣

要学习框架源码首先要有强烈的兴趣,也就是好奇心,说简单点也就是要多问问题。为什么会有StatefulWidgetStatelessWidget?为什么调用setState()以后Flutter会刷新你的页面?是什么在驱动Flutter框架运行?动画又是如何动起来的?等等等等。有了这些问题,你才会有动力去学习框架源码来自己寻找答案。如果你对Flutter并没有特别大的兴趣,仅仅是为了了解一下新东西,照着开发文档能写app觉得就ok了。或者是为了完成领导下达的任务,为了完成kpi而简单的学习一下,浅尝辄止。那我觉得这种情况下一个人是很难有去学习框架源码的念头的。

树立信心

除了要有兴趣,还要有信心。相信自己能征服框架源码,不会被庞大的代码给吓到。可能会有一些同学在开发Flutter app的过程中会点开StatefulWidget的源码去看看,但是跳转来跳转去很快就迷失在代码的海洋里找不着方向。又或者点开Flutter源码的目录看到满屏的.dart文件不知所措,直接劝退。这就是缺乏信心的表现。

这里我送大家一句话:“How hard can it be?” 直译过来就是“这能难到哪里去呢?” 这句话来自我最喜欢的前著名汽车评测节目“Top Gear”主持人,“大猩猩” Jeremy Clarkson。

虽然当他在节目中猥琐的说出这句话之后通常会搞砸某件事,但是这种精神是值得我们学习的。坚信自己能征服眼前的困难是非常重要的。

所以在你觉得Flutter源码很难的时候不如问问自己,How hard can it be?

掌握语言

Flutter框架的一个不同之处是使用了大家不太熟悉的dart语言。和Java,JS,OC等都有很大的区别。虽然dart能很快上手,短时间之内你就可以开发出像模像样的Flutter app。但是如果要去学习框架源码,则需要我们对这门新语言的特性非常的习惯,注意我这里说的是习惯而不是了解,也不是熟悉。如果你仅仅是知道新语言的特性,而不是习惯这些特性的话,去看框架源码是非常困难的。

特别是当你已经习惯了一种语言,比如Java,你对Java越熟悉,就越会觉得dart别扭。怎么在一个.dart文件里有好多个类?函数也能当参数?闭包是什么鬼?asyncawait是怎么个执行顺序?要克服惯性思维,特别是从自己熟悉的语言切换到不熟悉的语言带来的不适。才能顺利的开始新征程。怎么克服呢?唯有多看,多练,形成习惯。当你不再被新语言的特性所阻滞的时候,才能顺利的开启框架源码学习之旅。

阅读文档

在开始看框架源码之前,我先看了一些介绍框架的文章资料。这些文章可能良莠不齐,相互矛盾。也可能介绍的比较简单粗暴,看不懂里面在说啥,从而带来更多的问题。Widget我知道,可是Element和RenderObject又是啥?它们有啥关系?怎么会有那么多的树?Layer又是什么东西?

但是这些文章需要反复的看,特别是要以官网上的权威文档为主,相互比较,相互补充,这样慢慢的在脑海中就会建立起粗浅的框架模型。有了这个模型,你就会知道哪些地方比较重要,哪些地方需要特别关注,从大的分块上对框架有一个大体的认识。这个模型就是我们学习框架源码的指路明灯,避免我们迷失方向。

特别是对渲染流水线的介绍,这张图可以说就是整个Flutter框架的核心,从我的分析系列文章可以看出,我学习Flutter源码就是紧紧围绕这张图进行的。

rendering pipline

利用经验

最后呢,就是你要对前端,对GUI编程有个更高层次的理解会极大的帮助你去理解Flutter源码,不管是Android ,iOS,Web还是Flutter,各种各样的前端框架做的事情其实都是差不多的。所以它们面对的问题也是差不多的,它们各自框架的实现也是有共性的,互相也都在学习借鉴。所有要利用你之前以后的GUI开发经验,你会发现在各个框架中其实都可以找到相似的地方,如果能总结出一般性的规律,会极大帮助你理解新的框架。比如我们都知道为了和用户交互,GUI编程通常采用的是事件驱动模型,那么你肯定会去看Flutter的事件驱动模型是什么样的?我们都知道16ms,那么肯定有系统Vsync信号来驱动框架喽。GUI编程一般会有窗口(window)的概念,我们是不是可以有目的的去找一下新框架的窗口在哪里。用户界面编程肯定是逃不开界面元素的布局,绘制,光栅化。如果你已经有了这样的概念,那么剩下的只是去Flutter源码中去寻找具体实现了。有了这种一般性规律的指导,任何新GUI框架你都可以理解掌握。

学习中

做好以上准备以后,就可以进入源码的学习环节了。首先就是要找准学习框架源码的切入点。

找准切入点

一般来讲,框架代码的切入点有两个,一个是我们常见的是最上层的,也就是框架提供的那些API来入手,对于Flutter来讲那就是StatefulWidgetStatelessWidgetStatesetState()runApp()等等。另外一个切入点就是框架最底层和engine交互的地方,也就是window。这两个点也是框架的边界,入手的时候我们可以自上而下,也可以自下而上,或者兼而有之。这个看自己的喜好了。重要的是能找准框架的上界和下界。

专注于主流程

框架是很复杂的,需要处理很多事情,所以其代码也会非常庞杂。我们需要抓住主要流程,也就是我上面说的渲染流水线,而暂时忽略次要流程,例如点击事件的处理,辅助功能(Accessibility)等等,排除这些次要流程会使我们专注于主要流程,从而减轻我们的认知负担。在源代码中遇到touch,Accessibility等相关类,函数,代码可直接跳过,避免其带来干扰。这些次要流程可以待我们搞清楚主要流程之后再回过头来熟悉,这样会顺畅很多。

去粗取精

Flutter框架源代码中除了主要功能相关的代码之外,还存在着很多和调试,异常处理等逻辑。特别是有大量的assert断言语句。这些逻辑也是需要暂时剔除的以避免干扰我们的学习进程。往往一个函数在剔除了断言和异常处理等逻辑之后,关键代码可能只有几行甚至只有一行。

拿我们熟悉的setState()举例,完整代码如下(不必细看,只需要知道代码量就行了),大概60多行的样子。

  void setState(VoidCallback fn) {
    assert(fn != null);
    assert(() {
      if (_debugLifecycleState == _StateLifecycle.defunct) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() called after dispose(): $this'),
          ErrorDescription(
            'This error happens if you call setState() on a State object for a widget that '
            'no longer appears in the widget tree (e.g., whose parent widget no longer '
            'includes the widget in its build). This error can occur when code calls '
            'setState() from a timer or an animation callback.'
          ),
          ErrorHint(
            'The preferred solution is '
            'to cancel the timer or stop listening to the animation in the dispose() '
            'callback. Another solution is to check the "mounted" property of this '
            'object before calling setState() to ensure the object is still in the '
            'tree.'
          ),
          ErrorHint(
            'This error might indicate a memory leak if setState() is being called '
            'because another object is retaining a reference to this State object '
            'after it has been removed from the tree. To avoid memory leaks, '
            'consider breaking the reference to this object during dispose().'
          ),
        ]);
      }
      if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() called in constructor: $this'),
          ErrorHint(
            'This happens when you call setState() on a State object for a widget that '
            'hasn\'t been inserted into the widget tree yet. It is not necessary to call '
            'setState() in the constructor, since the state is already assumed to be dirty '
            'when it is initially created.'
          )
        ]);
      }
      return true;
    }());
    final dynamic result = fn() as dynamic;
    assert(() {
      if (result is Future) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() callback argument returned a Future.'),
          ErrorDescription(
            'The setState() method on $this was called with a closure or method that '
            'returned a Future. Maybe it is marked as "async".'
          ),
          ErrorHint(
            'Instead of performing asynchronous work inside a call to setState(), first '
            'execute the work (without updating the widget state), and then synchronously '
           'update the state inside a call to setState().'
          )
        ]);
      }
      // We ignore other types of return values so that you can do things like:
      //   setState(() => x = 3);
      return true;
    }());
    _element.markNeedsBuild();
  }
复制代码

在剔除断言,调试代码,异常处理之后就只剩短短两行了。

 void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }

复制代码

在看源码的时候通过这样的操作可以排除干扰,快速掌握关键逻辑。

举一反三

Flutter框架源代码中其实有一些套路在被普遍的使用,只要了解了这些套路,那么在看新的源码的时候会有似曾相识是的感觉,其大致逻辑也可以被推测出来。可以指引我们理解源码逻辑。例如,在渲染流水线的构建(build),布局(layout)和绘制(paint)阶段,都是相似的逻辑,首先把相应节点标记为脏(dirty)

  • Element.markNeedsBuild()
  • RenderObject.markNeedsLayout()
  • RenderObject.markNeedsPaint()

然后后续真正执行相应操作。如果搞清楚了构建流程的套路,那么在看布局,绘制的源码的时候就不会感觉那么的陌生了。这样的例子还有很多,就不一一列举了。

借助工具

Flutter框架也提供了很多的工具给开发者,大家在学习源码的的时候可以充分利用这些工具来帮助自己。比如Flutter inspector。可以让你能清晰的看到element tree, renderObject tree。有助于理解它们之间的关系。

element tree

其次就是对于比较复杂的逻辑,要自己动手写一些demo app打上断点跑一跑,单步跟踪一下,也是非常有帮助的。

最后就是Flutter也提供一些调试用的Flag。例如以下这些标志位

  debugPrintScheduleFrameStacks = true;
  debugPrintBeginFrameBanner = true;
  debugPrintEndFrameBanner = true;
  debugPrintRebuildDirtyWidgets = true;
  debugPrintBuildScope = true;
  debugPrintScheduleBuildForStacks = true;
  debugProfileBuildsEnabled = true;
  debugPaintLayerBordersEnabled = true;
  debugPrintMarkNeedsLayoutStacks = true;
  debugPrintMarkNeedsPaintStacks = true;
复制代码

有选择的打开某些标志位再跑一下自己的程序,查看log输出,也有助于我们理解Flutter框架。

做好笔记

俗话说好记性不如烂笔头。框架代码如此庞杂,如果我们仅仅是在IDE里跳转来,跳转去。那么很快就会迷失方向,不知道自己身处何处。所以必要的记录是必须的。特别是有多个分支的情况,如果不做记录,顺着其中一个分支深入下去,很可能就会忘记其他分支还没有照顾到。或者是看完一个分支再去看其他分支,也能把之前的分支干了些啥都给忘了。在后期有这些笔记也有助于对框架代码的整体理解。

要有耐心

最后就是一定要有耐心,不要中途放弃,也不要拖延,看了一部分剩下的过两个月再看的时候就全忘了。前功尽弃。遇到比较难以理解的复杂逻辑的时候要反复看,反复理解,强制自己去理解,强制自己去排除不正确的认知,把思路向框架开发者靠拢。没有翻不过去的山。

学习后

如果能坚持到这里,那么恭喜你,Flutter框架对你来讲已经不是个陌生人了。剩下的,就是再做一些事情使你们更加熟悉。

融会贯通

在初步掌握了框架主流程之后,此时我们可以回头再去看那些分支流程。继续丰富自己对框架的认识,同时也可以巩固相关知识。我的体会是学无止境,每次再回头看框架源码都会有新的收获,新的认识;再去看有关框架的文档,文章的时候也会有一些提升,比如那些地方说的对,比我自己的理解好,哪些地方可能有一些问题,和作者共同探讨一下都是非常好的。

更进一步,我们可以多问一些为什么,为什么这个地方的逻辑是这样的?框架作者为什么要这样设计?有没有更好,更高效的办法来实现同样的逻辑?带着这些问题去复习源码会给你带来更大的提高。

写成文章

最后的最后呢。如果能把自己的收获写成文章分享出来那是“坠吼的”。写文章的好处太多了,我就不多说了。谁写谁知道。。。。。。

总结

本文是应掘友要求写的学习Flutter源码的一点心得体会。其实里面很多方法,技巧可能并不局限于Flutter这个特定的框架,甚至不局限于代码。由于是个人的一些经验,可能有些东西说的也不是适合所有人,如果大家觉得有不对的地方或者有更好的经验,也希望你们能分享出来我们互相学习,一同成长。