13

Flutter中的Tree Shaking机制初探(科普文)

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzU4MDUxOTI5NA%3D%3D&%3Bmid=2247486125&%3Bidx=1&%3Bsn=bf9dcb96d114a6696788cd383853ae88
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

背景

在闲鱼技术探究Flutter工程一体化的过程中,为了做到最好的开发体验,需要无缝衔接FaaS端代码与业务Flutter代码,一份代码既可以在FaaS部署,也可以直接引入在业务代码主工程中,使之真正做到工程一体。

为了实现这一目标我们对两部分代码通过RPC调用的方式实现了代码解耦,而工程解耦依赖于Flutter/Dart在编译过程中的Tree-Shaking机制。为了避免踩坑,我们需要了解,整个Tree-Shaking是怎么起作用的。本篇文章结合Flutter Engine源码对这一过程进行了简单的探究。

前置知识

Tree Shaking是一种死代码消除(Dead Code Elimination)技术,这一想法起源于20世纪90年代的LISP。其思想是:一个程序所有可能的执行流程都可以用函数调用的树来表示,这样就可以消除那些从未被调用的函数。该算法最先被应用到Google Closure Tools中的JavaScript中,然后被应用到同样由Google编写的dart2js编译器中。在Flutter中,同样有这样的Tree Shaking机制来减小最终产出的包大小。Flutter提供了三种构建模式,针对每个不同的模式,Flutter编译器对产出的二进制文件有不同优化,Tree-Shaking机制并不会在debug模式中触发。在Profile/Release模式下编译的AOT产物中,有几个比较重要的产物可以让我们更直观地看到Tree-Shaking机制在发挥作用:

  • app.dill : 这就是dart代码通过build的产物,为二进制的字节码,可以通过  strings 看到里面的内容,其实就是我们dart代码的源码。

  • snapshot_blob.bin.d : 这个文件里面是所有参与编译的dart文件的集合,包括我们自己的业务代码、  pubspec.yaml 中定义的三方库的代码、以及我们业务代码中import进来的所有flutter或者dart原生  package 的代码。

Tree Shaking机制探究

最小化Demo初探

我们写一个最简单的例子,代码如下:

640?wx_fmt=png代码非常简单,里面包含了一个没有被使用的 _unused  方法。下面我们在Profile模式下进行编译,通过DevTools来查看最终编译的产物,如下图所示

640?wx_fmt=png可以看到,在Funtions中,并没有 _unused 方法,说明在编译过程中,这段无用的代码被“摇”掉了。实际上除了Function之外,Flutter编译过程中对于引入的lib,import的dart文件都有相似的Tree-Shaking处理。下面深入代码来看看,这究竟是怎么做到。

代码解析

这里借用Gityuan前辈的 flutter run  命令执行的时序图,整个编译流程会比较长,在 GenSnapshot.run()  方法会调用gen snapshot这个二进制可执行文件(对应的源码在目录third party/dart/runtime/bin/gen snapshot.cc),生成机器码。

640?wx_fmt=png 用放大镜来看看gen snapshot内部的执行过程:

640?wx_fmt=pngtree-shaking机制就发生在其中的编译阶段,即 CompileAll()  方法。下面我们深入到代码去一步一步探究,Flutter编译器是怎么对代码做裁剪的。

源代码路径是third_party/dart/runtime/vm/compiler/aot/precompiler.cc,读者也可以自行对照查询。

编译阶段

首先是必备的准备工作,需要将对象池保留到AOT编译结束,因此这里必须使用能存活那么久的句柄,使用了StackZone。

640?wx_fmt=png为了使用类层次结构分析 (CHA),在编译前需要确保类的层次结构稳定,同时确保查找入口点时不会因为函数的类还没有最终确定而漏掉函数。CHA是一种编译器优化,可根据对类层次结构的分析结果,将虚拟调用去虚拟化为直接调用。

640?wx_fmt=png预编译构造函数,计算优化指令数等信息,可以用于内联函数。

640?wx_fmt=png下一步生成桩代码,通过 StubCode::InterpretCall 得到的code来获取它的对象池,再利用 StubCode::Build 等一系列方法系列方法获取的结果保存在object_store。收集动态函数的方法名,之后通过 AddRoots() 方法,从C++发生的分配和调用的起点添加为根, 同时通过 AddAnnotatedRoots() 方法将所有以@pragma(’vm:entry-point’)为标注的也添加为根。

640?wx_fmt=png之后,代码开始编译, Iterate() 是编译最为核心的地方。在这里会以上面找到的根作为目标,遍历添加该目标的调用者。

640?wx_fmt=png在该方法内部,主要的调用链如下:

ProcessFunction
==> CompileFunction
==> PrecompileFunctionHelper
==> PrecompileParsedFunctionHelper.Compile

至此,编译完成之后开始进入Tree-Shaking阶段,对无用代码进行简化。

Tree shaking阶段

在上面的编译过程中,函数/类等调用信息已经进行了输出,根据这些信息,让编译器可以知道,具体哪一些是不必要的代码。这里以对Function的处理为例进行讲解:

  • TraceForRetainedFunctions();

在这个方法中,取得Library、Class等句柄之后,以Library为单位,对每个包内的代码进行处理,会遍历所有类中的Functions进行处理。

640?wx_fmt=png通过 AddTypesOf(constFunction&function) 方法,将调用到的函数添加到 functions_to_retain_ 池中,同时对Function中的类型参数做了读取,通过 AddType 方法,将这些类型参数添加到对应的 typeargs_to_retain_ 池和 types_to_retain_ 池中,用于类型信息的TreeShaking(分别对应DropTypeArguments和DropTypeParameters)。

640?wx_fmt=png

Class信息在同名方法 AddTypesOf(constClass&cls) 中进行处理,处理过程比较类似,这里不做赘述,感兴趣的读者可以自行查阅

  • FinalizeDispatchTable();

这个方法里面,会确保在执行Drop方法之前建立用于序列化调度表的条目,因为编译器后续可能会清除对Code对象的引用。同时删除调度表生成器,以确保在这之后不再尝试添加新条目。

  • ReplaceFunctionStaticCallEntries();

在这个方法里通过声明的匿名内部类 StaticCallTableEntryFixer  ,对静态函数调用入口做了替换。

  • Drop

接下来,会执行一系列的Drop方法。这些方法会去掉多余的方法、字段、类、库等,如下所示:

  1. DropFunctions();

  2. DropFields();

  3. DropTypes();

  4. DropTypeParameters();

  5. DropTypeArguments();

  6. DropMetadata();

  7. DropLibraryEntries();

  8. DropClasses();

  9. DropLibraries();

具体调用时序如下图所示:

640?wx_fmt=png由于这些方法的内部实现思路有很多相似之处,这里针对Function的方法 DropFunctions 为例来说明。

在该方法中,核心是通过以上提到 functions_to_retain_  池,对Function是否有根调用者进行判断, 如果池中不包含Function对象,说明这是可以舍弃的Function。之后,将剩下的Function重新写回Class,并更新Class的调用表。

在方法内部声明了drop_function函数来“摇掉”Function。

640?wx_fmt=png之后使用对所有的代码中的Function进行遍历, 使用上面声明的 drop_function  对无用的Function代码进行标记和删除。

640?wx_fmt=png将需要被保留的Funtion重新写进所属Class中:

640?wx_fmt=png重新生成类的调用表,同时对调用表中的可能存在的无用Function进行兜底删除:

640?wx_fmt=png最后是一些内联函数等边界情况的处理,这里不再赘述。在完成Drop阶段之后,可以被丢掉的代码已经进入了删除池中,后面进入编译的收尾阶段,进一步减小二进制文件大小。

收尾阶段

在Tree-Shaking结束之后,进入编译收尾工作,包括代码混淆,垃圾回收等。

640?wx_fmt=png值得注意的是Dedup这个方法,关键代码代码如下:

640?wx_fmt=png在该方法内进行很多重复数据删除工作;在AOT模式下,binder是在Tree Shaking之后运行的,在此期间,所有的目标都已经被编译,因此binder会用对目标的直接调用代替所有的静态调用,进一步减小了编译产物二进制文件。至此所有的编译工作完成,Tree-Shaking完成了他的使命。

拓展

在Flutter 1.20版本,通过Tree-Shaking机制移除在工程中未使用到的icon fonts,进一步缩小了包大小(100KB左右),不过该方法的实现并不在以上说明的编译阶段,而是在build_system里,对assets进行了优化。相关的PR在 github.com/flutter/flutte/pull/49737 可以查看。

小结

本文主要结合Flutter Engine源代码,从编译阶段出发,探究了在过程中Tree-Shaking的运行机制。由于这样一个机制的存在,为工程解耦提供了理论基础,让工程一体化的实现更为简单,同时对我们进一步优化包大小有启发。

640?wx_fmt=png


Recommend

  • 90
    • 掘金 juejin.im 6 years ago
    • Cache

    你的Tree-Shaking并没什么卵用

    你的Tree-Shaking并没什么卵用 2018年01月10日 17:55 ·  阅读 22520 ...

  • 7
    • chipengliu.github.io 3 years ago
    • Cache

    URLSession HTTP 缓存机制初探

    URLSession HTTP 缓存机制初探 2020-05-192020-10-25iOS HTTP/HTTPS 可以通过 HTTP Response 中的 header 字段(如 Cac...

  • 3

    AnimalsSpiders eat snakes around the world, surprising study revealsNorth American widow spiders, not tropical tarantulas, have a particular taste for repti...

  • 8

    Tree-shaking 这一术语在前端社区内,起初是 Rich Harris 在 Rollup 中提出。简单概括起来,Tree-shaking 可以使得项目最终构建(Bundle)结果中只包含你实际需要的...

  • 4
    • hijiangtao.github.io 2 years ago
    • Cache

    Tree Shaking 简介

    Tree Shaking 简介 扫码或点击链接查看完整 Slides 本周分享会上,给团队同学分享了 Tree Shaking 的相关内容...

  • 6

    一、什么是 Tree ShakingTree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其它模块使用,并将其删除,以此实现打包产物的优化。Tree Shakin...

  • 5

    Tree-shaking到底有啥用?简简单单地说一下!发布于 2 月 8 日大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是...

  • 2

    作者: 神Q超人译者:前端小智来源:medium有梦想,有干货,微信搜索 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。本文 GitHub

  • 3
    • blog.saeloun.com 1 year ago
    • Cache

    Learn about tree shaking in Webpack 5

    What is tree shaking? Tree shaking is a technique used for removing the dead code from the application. It is applied for optimizing the code. Dead code is essentially code from the library which has been import...

  • 6
    • spiegelmock.com 1 year ago
    • Cache

    Mastering JavaScript Tree-Shaking

    Home

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK