https://time.geekbang.org/column/article/168710?code=LV9RoFlBhKjpI7j8O9ZIKzBXE4B4ggmC2o8Iluu8Dao%3D

你好,我是陈航。专栏上线以来,我在评论区看到了很多同学写的心得、经验和建议,当然更多的还是大家提的问题。为了能够让大家更好地理解我们专栏的核心知识点,我今天特意整理了每篇文章的课后思考题,并结合大家在留言区的回答情况做一次分析与扩展。当然 ,我也希望你能把这篇答疑文章作为对整个专栏所讲知识点的一次复习,如果你在学习或者使用 Flutter 的过程中,遇到哪些问题,欢迎继续给我留言。我们一起交流,共同进步!需要注意的是,这些课后题并不存在标准答案。就算是同一个功能、同一个界面,不同人也会有完全不一样的实现方案,只要你的解决方案的输入和输出满足题目要求,在我看来你就已经掌握了相应的知识点。因此,在这篇文章中,我会更侧重于介绍方案、实现思路、原理和关键细节,而不是讲具体实操的方方面面。接下来,我们就具体看看这些思考题的答案吧。

问题 1:直接在 build 函数里以内联的方式实现 Scaffold 页面元素的构建,好处是什么?

这个问题选自第 5 篇文章“从标准模板入手,体会 Flutter 代码是如何运行在原生系统上的”,你可以先回顾下这篇文章的相关知识点。

然后,我来说说这样做的最大好处是,各个组件之间可以直接共享页面的状态和方法,页面和组件间不再需要把状态数据传来传去、多级回调了。

不过这种方式也有缺点,一旦数据发生变更,Flutter 会重建整个大 Widget(而不是变化的那部分),所以会对性能产生些影响。

问题 2:对于集合类型 List 和 Map,如何让其内部元素支持多种类型?

这个问题来自第 6 篇文章“基础语法与类型变量:Dart 是如何表示信息的?”,你可以先回顾下这篇文章的相关知识点。

如果集合中多个类型之间存在共同的父类(比如 double 和 int),可以使用父类进行容器类型声明,从而增加类型的安全校验,在取出对象时根据 runtimeType 转换成实际类型即可。如果容器中的类型比较多,想省掉类型转换的步骤,也可以使用动态类型 dynamic 为元素添加不同类型的元素。

而在判断元素真实类型时,我们可以使用 is 关键字或 runtimeType。

问题 3:继承、接口与混入的相关问题。

这个问题来自第 7 篇文章“函数、类与运算符:Dart 是如何处理信息的?”,你可以先回顾下这篇文章的相关知识点。

第一,你是怎样理解父类继承、接口实现和混入的?

我们应该在什么场景下使用它们?父类继承、接口实现和混入都是实现代码复用的手段,我们在代码中应该根据不同的需求去使用。

其中:

  • 在父类继承中,子类复用了父类的实现,适用于两个类的整体存在逻辑层次关系的场景;
  • 在接口实现中,类复用了接口的参数、返回值和方法名,但不复用其方法实现,适用于接口和类在行为存在逻辑层次关系的场景;
  • 而混入则可以使一个类复用多个类的实现,这些类之间无需存在父子关系,适用于多个类的局部存在逻辑层次关系的场景。

第二,在父类继承的场景中,父类子类之间的构造函数执行顺序是怎样的?如果父类有多个构造函数,子类也有多个构造函数,如何从代码层面确保父类子类之间构造函数的正确调用?

默认情况下,子类的构造函数会自动调用父类的默认构造函数,如果需要显式地调用父类的构造函数,则需要在子类构造函数的函数体开头位置调用。但,如果子类提供了初始化参数列表,则初始化参数列表会在父类构造函数之前执行。

构造函数之间,有时候会有一些相同的逻辑。如果把这些逻辑分别写在各个构造函数中,会有些累赘,所以构造函数之间是可以传递的,相当于填充了某个构造函数的参数,从而实现类的初始化。因此可以传递的构造函数是没有方法体的,它们只会在初始化列表中,去调用另一个构造函数。

如果子类与父类存在多个构造函数,通常是为了简化类的初始化代码,将部分不需要的属性设置为默认值。因此,我们只要能确保每条构造函数的初始化路径都不会有属性被遗漏即可。一个好的做法是,依照构造函数的参数个数,将参数少的构造函数转发至参数多的构造函数中,由参数最多的构造函数统一调用父类参数最多的那个构造函数。

问题 4:扩展购物车案例的程序,使其支持商品数量属性,并输出商品列表信息(包括商品名称、数量及单价)。

这个问题来自第 8 篇文章“综合案例:掌握 Dart 核心特性”,你可以先回顾下这篇文章的相关知识点。

要实现这个扩展功能,如我所说,每个人都可能有完全不一样的解决方案。在这里,我给你的提示是,在 Item 类中增加数量属性,在做小票打印时,循环购物车内的商品信息即可实现。

需要注意的是,增加数量属性后,商品在做合并计算价格时,count 需要置为 1,而不能做累加。比如,五斤苹果和三盒巧克力做合并,结果是一份巧克力苹果套餐,而不是八份巧克力苹果套餐。

问题 5:Widget、Element 和 RenderObject 之间是什么关系?你能否在 Android/iOS/Web 中找到对应的概念呢?

这个问题来自第 9 篇文章“Widget,构建 Flutter 界面的基石”。

Widget 是数据配置,RenderObject 负责渲染,而 Element 是一个介于它们之间的中间类,用于渲染资源复用。

Widget 和 Element 是一一对应的,但 RenderObject 不是,只有实际需要布局和绘制的控件才会有 RenderObject。

这三个概念在 iOS、Android 和 Web 开发中,对应的概念分别是:

在 iOS 中,Xib 相当于 Widget,UIView 相当于 Element,CALayer 相当于 renderObject;
在 Android 中,XML 相当于 Widget,View 相当于 Element,Canvas 相当于 renderObject;
在 Web 中,以 Vue 为例,Vue 的模板相当于 Widget,virtual DOM 相当于 Element,DOM 相当于 RenderObject。

问题 6:State 构造函数和 initState 的差异是什么?

这个问题来自第 11 篇文章“提到生命周期,我们是在说什么?”。

State 构造函数调用时,Widget 还未完成初始化,因此仅适用于一些与 UI 无关的数据初始化,比如父类传入的参数加工。

而 initState 函数调用时,StatefulWidget 已经完成了 Widget 树的插入工作,因此与 Widget 相关的一些初始化工作,比如设置滚动监听器则必须放在 initState。

问题 7:Text、Image 以及按钮控件,真正承载其视觉功能的控件分别是什么?

这个问题来自第 12 篇文章“经典控件(一):文本、图片和按钮在 Flutter 中怎么用?”。

Text 是封装了 RichText 的 StatelessWidget,Image 是封装了 RawImage 的 StatefulWidget,而按钮则是封装了 RawMaterialButton 的 StatelessWidget。

可以看到,StatelessWidget 和 StatefulWidget 只是封装了控件的容器,并不参与实际绘制,真正负责渲染的是继承自 RenderObject 的视觉功能组件们,比如 RichText 与 RawImage。

问题 8:在 ListView 中,如何提前缓存子元素?

这个问题来自第 13 篇文章“经典控件(二):UITableView/ListView 在 Flutter 中是什么?”。ListView 构造函数中

ListView 构造函数中有一个 cacheExtent 参数,即预渲染区域长度。ListView 会在其可视区域的两边留一个 cacheExtent 长度的区域作为预渲染区域,相当于提前缓存些元素,这样当滑动时就可以迅速呈现了。

问题 9:Row 与 Column 自身的大小是如何决定的?当它们嵌套时,又会出现怎样的情况呢?

这个问题来自第 14 篇文章“经典布局:如何定义子控件在父容器中排版的位置?”。

Row 与 Column 自身的大小由父 Widget 的大小、子 Widget 的大小,以及 mainSize 共同决定。

Row 和 Column 只会在主轴方向占用尽可能大的空间(max:屏幕方向主轴大小或父 Widget 主轴方向大小;min:所有子 Widget 组合在一起的主轴方向大小),而纵轴的长度则取决于它们最大子元素的长度。

如果 Row 里面嵌套 Row,或者 Column 里面嵌套 Column,只有最外层的 Row 或 Colum 才会占用尽可能大的空间,里层 Row 或 Column 占用的空间为实际大小。

问题 10:在 UpdatedItem 控件的基础上,增加切换夜间模式的功能。

这个问题来自第 16 篇文章“从夜间模式说起,如何定制不同风格的 App 主题?”。

这是一道实践题。同样地,我在这里也只提示你实现思路:你可以在 ThemeData 中,通过增加变量来判断当前使用何种主题,然后在 State 中驱动变量更新即可。

问题 11:像素密度为 3.0 及 1.0 设备,如何根据资源图片像素进行处理?

这个问题来自第 17 篇文章“依赖管理(一):图片、配置和字体在 Flutter 中怎么用?”。

设备根据资源图片像素进行适配的原则是:调整为使用最合适的分辨率资源,即像素密度为 3.0 的设备会选择 2.0 而不是 1.0 的资源图片;而像素密度为 1.0 的设备,对于像素密度大于 1.0 的资源图片会进行压缩。

问题 12:.packages 与 pubspec.lock 是否需要做代码版本管理?

这个问题来自第 18 篇文章“依赖管理(二):第三方组件库在 Flutter 中要如何管理?”。

pubspec.lock 需要做版本管理,因为 lock 文件记录了 Dart 在计算项目依赖时,当前工程所有显式和隐私的依赖关系。我们可以直接使用这个结果去统一工程开发环境。

而. packages 不需要版本管理,因为这个文件记录了 Dart 在计算项目依赖时,当前工程所有依赖的本地缓存文件。与本地环境有关,无需统一。

问题 13:GestureDetector 内嵌 FlatButton 后,事件是如何响应的?

这个问题来自第 19 篇文章“用户交互事件该如何响应?”。

对于一个父容器中存在按钮 FlatButton 的界面,在父容器使用 GestureDetector 监听了 onTap 事件的情况下,我们点击按钮是不会被父 Widget 响应的。因为,手势竞技场只会同时响应一个(子 Widget)。

如果监听的是 onDoubleTap 事件,在按钮上双击后,父容器的双击事件会被识别。因为,子 Widget 没有处理双击事件,不需要经历手势竞技场的 PK 过程。

问题 14:请分别概括属性传值、InheritedWidget、Notification 与 EventBus 的特点。

这个问题来自第 20 篇文章“关于跨组件传递数据,你只需要记住这三招”。

属性传值适合在同一个视图树中使用,传递方向由父及子,通过构造方法将值以属性的方式传递过去,简单高效。其缺点是,涉及跨层传递时,属性可能需要跨越很多层才能传递给子组件,导致中间很多并不需要这个属性的组件,也得接收其子 Widget 的数据,繁琐且冗余。

InheritedWidget 适用于子 Widget 主动向上层拿数据的场景,传递方向由父及子,可以实现跨层的数据读共享。InheritedWidget 也可以实现写共享,需要在上层封装写数据的方法供下层调用。其优点是,数据传输方便,无代码侵入即可达到逻辑和视图解耦的效果;而其缺点是,如果层次较深,刷新范围过大会影响性能。

Notification 适用于子 Widget 向父 Widget 推送数据的场景,传递方向由子及父,可以实现跨层的数据变更共享。其优点是,多个子元素的同一事件可由父元素统一处理,多对一简单;而其缺点是,Notification 的自定义过程略繁琐。

EventBus 适用于无需存在父子关系的实体之间通信,订阅者需要显式地订阅和取消。其优点是,能够支持任意对象的传递,一对多的方式实现简单;而其缺点是,订阅管理略显繁琐。

问题 15:实现一个计算页面,这个页面可以对前一个页面传入的 2 个数值参数进行求和,并在该页面关闭时告知上一页面计算的结果。

这个问题来自第 21 篇文章“路由与导航,Flutter 是这样实现页面切换的”。

这是一个实践题,还需要你动手去实现。这里,我给你的提示是:基本路由可以通过构造函数属性传值的方式,或是在 MaterialPageRoute 中加入参数 setting,来传递页面参数。

打开页面时,我们可以使用上述机制为基本路由传递参数(2 个数值),并注册 then 回调监听页面的关闭事件;而页面需要关闭时,我们将 2 个数值参数取出,求和后调用 pop 函数即可。

问题 16:AnimatedBuilder 中,外层的 child 参数与内层 builder 函数中的 child 参数的作用分别是什么?

  1. AnimatedBuilder(
  2. animation: animation,
  3. child:FlutterLogo(),
  4. builder: (context, child) => Container(
  5. width: animation.value,
  6. height: animation.value,
  7. child: child
  8. )
  9. )

这个问题来自第 22 篇文章“如何构造炫酷的动画效果?”。

外层的 child 参数定义渲染,内层 builder 中的 child 参数定义动画,实现了动画和渲染的分离。通过 builder 函数,限定了重建 rebuild 的范围,做动画时不必每次重新构建整个 Widget。

问题 17:并发 Isolate 计算阶乘例子里给并发 Isolate 两个 SendPort 的原因?

  1. //并发计算阶乘
  2. Future<dynamic> asyncFactoriali(n) async{
  3. final response = ReceivePort();//创建管道
  4. //创建并发Isolate,并传入管道
  5. await Isolate.spawn(_isolate,response.sendPort);
  6. //等待Isolate回传管道
  7. final sendPort = await response.first as SendPort;
  8. //创建了另一个管道answer
  9. final answer = ReceivePort();
  10. //往Isolate回传的管道中发送参数,同时传入answer管道
  11. sendPort.send([n,answer.sendPort]);
  12. return answer.first;//等待Isolate通过answer管道回传执行结果
  13. }
  14. //Isolate函数体,参数是主Isolate传入的管道
  15. _isolate(initialReplyTo) async {
  16. final port = ReceivePort();//创建管道
  17. initialReplyTo.send(port.sendPort);//往主Isolate回传管道
  18. final message = await port.first as List;//等待主Isolate发送消息(参数和回传结果的管道)
  19. final data = message[0] as int;//参数
  20. final send = message[1] as SendPort;//回传结果的管道
  21. send.send(syncFactorial(data));//调用同步计算阶乘的函数回传结果
  22. }
  23. //同步计算阶乘
  24. int syncFactorial(n) => n < 2 ? n : n * syncFactorial(n-1);
  25. main() async => print(await asyncFactoriali(4));//等待并发计算阶乘结果

这个问题来自第 23 篇文章“单线程模型怎么保证 UI 运行流畅?”。

SendPort/ReceivePort 是一个单向管道,帮助我们实现并发 Isolate 往主 Isolate 回传执行结果:并发 Isolate 负责用 SendPort 发,而主 Isolate 负责用 ReceivePort 收。对于回传执行结果这个过程而言,主 Isolate 除了被动等待没有别的办法。

在这个例子中,并发 Isolate 用 SendPort 发了两次数据,意味着主 Isolate 也需要用 SendPort 对应的 ReceivePort 等待两次。如果并发 Isolate 用 SenderPort 发了三次数据,那主 Isolate 也需要用 ReceivePort 等待三次。

那么,主 Isolate 怎么知道自己需要等待几次呢,总不能一直等着吧?

所以更好的办法是,只使用 SendPort/ReceivePort 一次,发 / 收完了就不用了。但,如果下次还要发 / 收怎么办?

这时,我们就可以参考这个计算阶乘案例的做法,在发数据的时候把下一次用到的 SendPort 也当做参数传过去。

问题 18:自定义 dio 拦截器,检查并刷新 token。

这个问题来自第 24 篇文章“HTTP 网络编程与 JSON 解析”。

这也是一个实践题,我同样只提示你关键思路:在拦截器的 onRequest 方法中,检查 header 中是否存在 token,如果没有,则发起一个新的请求去获取 token,更新 header。考虑到可能会有多个 request 同时发出,token 会请求多次,我们可以通过调用拦截器的 lock/unlock 方法来锁定 / 解锁拦截器。

一旦请求 / 响应拦截器被锁定,接下来的请求 / 响应将会在进入请求 / 响应拦截器之前排队等待,直到解锁后,这些入队的请求才会继续执行(进入拦截器)。

问题 19:持久化存储的相关问题。

这个问题来自来第 25 篇文章“本地存储与数据库的使用和优化”。

首先,我们先看看文件、SharedPreferences 和数据库,这三种持久化数据存储方式的适用场景。

文件比较适合大量的、有序的数据持久化;

SharedPreferences,适用于缓存少量键值对信息;

数据库,则用来存储大量格式化后的数据,并且这些数据需要以较高频率更新。

接下来,我们看看如何做数据库跨版本升级?

数据库升级,实际上就是改表结构。如果升级过程是连续的,我们只需要在每个版本执行修改表结构的语句就可以了。如果升级过程不是连续的,比如直接从 1.0 升到 5.0,中间 2.0、3.0 和 4.0 都直接跳过的:

  1. 1.0->2.0:执行表结构修改语句A
  2. 2.0->3.0:执行表结构修改语句B
  3. 3.0->4.0:执行表结构修改语句C
  4. 4.0->5.0:执行表结构修改语句D

因此,我们在 5.0 的数据库迁移中,不能只考虑 5.0 的表结构,单独执行 4.0 的升级逻辑 D,还需要考虑 2.0、3.0、4.0 的表结构,把 1.0 升级到 4.0 之间的所有升级逻辑执行一遍:
/

  1. switch(oldVersion) {
  2. case '1.0': do A;
  3. case '2.0': do B;
  4. case '3.0': do C;
  5. case '4.0': do D;
  6. default: print('done');
  7. }

这样就万无一失了。

不过需要注意的是,在 Dart 的 switch 里,条件判断 break 语句是不能省的。关于如何在 Dart 中写出类似 C++ 的 fallthrough switch,你可以再思考一下。

问题 20:扩展 openAppMarket 的实现,使得我们可以跳转到任意一个 App 的应用市场。

这个问题来自第 26 篇文章“如何在 Dart 层兼容 Android/iOS 平台特定实现?(一)”。

对于这个问题,我给你的提示是:Dart 调用 invokeMethod 方法时,可传入 Map 类型的键值对参数(包含 iOS 的 bundleID 和 Android 包名),然后在原生代码宿主将参数取出即可。

问题 21:扩展内嵌原生视图的实现,实现动态变更原生视图颜色的需求。

这个问题来自第 27 篇文章“如何在 Dart 层兼容 Android/iOS 平台特定实现?(二)”。

对于这个问题,我给你提示与上一问题类似:Dart 调用 invokeMethod 方法时,可传入 Map 类型的键值对参数(颜色的 RGB 信息),然后在原生代码宿主将参数取出即可。

问题 22:对于有资源依赖的 Flutter 模块工程,其打包构建的产物,以及抽离组件库的过程是否有不同?

这个问题来自第 28 篇文章“如何在原生应用中混编 Flutter 工程?”。

答案是没什么不同。因为 Flutter 模块的文件本身就包含了资源文件。

如果模块工程有原生插件依赖,则其抽离过程还需要借助记录了插件本地依赖缓存地址的. flutter-plugins 文件,来实现组件依赖的原生部分的封装。

问题 23:如何确保混合工程中两种页面过渡动画在应用整体的效果一致?

这个问题来自第 29 篇文章“混合开发,该用何种方案管理导航栈?”

首先,这两种页面过渡动画分别是:原生页面之间的切换动画和 Flutter 页面之间的切换动画。

保证整体效果一致,有两种方案:

一是,分别定制原生工程(主要是 Android)的切换动画,及 Flutter 的切换动画;

二是,使用类似闲鱼的共享 FlutterView 的机制,将页面切换统一交由原生处理,FlutterView 只负责刷新界面。

问题 24:如何使用 Provider 实现 2 个同样类型的对象共享?

这个问题来自第 30 篇文章“为什么需要做状态管理,怎么做?

答案很简单,你可以封装 1 个大对象,将 2 个同样类型的对象封装为其内部属性。

问题 25:如何让 Flutter 代码能够更快地收到推送消息?

这个问题来自第 31 篇文章“如何实现原生推送能力?”。

我们需要先判断当前应用是处于前台还是后台,然后再用对应的方式去处理:

如果应用处于前台,并且已经完成初始化,则原生代码直接调用方法通道通知 Flutter;如果应用未完成初始化,则原生代码将消息存在本地,待 Flutter 应用初始化完成后,调用方法通道主动拉取。

如果应用处于后台,则原生代码将消息存在本地,唤醒 Flutter 应用,待 Flutter 应用初始化完成后,调用方法通道主动拉取。

问题 26:如何实现图片资源的国际化?

这个问题来自第 32 篇文章“适配国际化,除了多语言我们还需要注意什么?”。

其实,图片资源国际化与文本资源,本质上并无区别,只需要在 arb 文件中对不同的图片进行单独声明即可。具体的实现细节,你可以再回顾下这篇文章的相关内容。

问题 27:相邻页面的横竖屏切换如何实现?

这个问题来自第 33 篇文章“如何适配不同分辨率的手机屏幕?”。

这个实现方式很简单。你可以在 initState 中设置屏幕支持方向,在 dispose 时将屏幕方向还原即可。

问题 28:在保持生产环境代码不变的情况下,如何支持不同配置的切换?

这个问题来自第 34 篇文章“如何理解 Flutter 的编译模式?”。

与配置夜间模式类似,我们可以通过增加状态开关来判断当前使用何种配置,设置入口,然后在 State 中驱动变量更新即可。关于夜间模式的配置,你可以再回顾下第 16 篇文章 “从夜间模式说起,如何定制不同风格的 App 主题?” 中的相关内容。

问题 29:将 debugPrint 改为循环写日志。

这个问题来自第 36 篇文章“如何通过工具链优化开发调试效率?

关于这个问题,我给你的提示是,用不同的 main 文件定义 debugPrint 行为:main-dev.dart 定义为日志输出至控制台,而 main.dart 定义为输出至文件。当前操作的文件名默认为 0,写满后文件名按 5 取模递增,同步更新至 SharedPreferences 中,并将文件清空,重新写入。

问题 30:使用并发 Isolate 完成 MD5 的计算。

这个问题来自第 37 篇文章“如何检测并优化 Flutter App 的整体性能表现?”。

关于这个问题,我给你的提示是:将界面改造为 StatefulWidget,把 MD5 的计算启动放在 StatefulWidget 的初始化中,使用 compute 去启动计算。在 build 函数中,判断是否存在 MD5 数据,如果没有,展示 CircularProgressIndicator,如果有,则展示 ListView。

问题 31:如何使用 mockito 为 SharedPreferences 增加单元测试用例?

  1. Future<bool>updateSP(SharedPreferences prefs, int counter) async {
  2. bool result = await prefs.setInt('counter', counter);
  3. return result;
  4. }
  5. Future<int>increaseSPCounter(SharedPreferences prefs) async {
  6. int counter = (prefs.getInt('counter') ?? 0) + 1;
  7. await updateSP(prefs, counter);
  8. return counter;
  9. }

这个问题来自第 38 篇文章“如何通过自动化测试提高交付质量?”。

待测函数 updateSP 与 increaseSPCounter,其内部依赖了 SharedPreferences 的 setInt 方法与 getInt 方法,其中前者是异步函数,后者是同步函数。

因此,我们只需要为 setInt 与 getInt 模拟对应的数据返回即可。对于 setInt,我们只需要在参数为 1 的时候返回 true:

  1. when(prefs.setInt('counter', 1)).thenAnswer((_) async => true);

对于 getInt,我们只需要返回 2:

  1. when(prefs.getInt('counter')).thenAnswer((_) => 2);

其他部分与普通的单元测试并无不同。

问题 32:并发 Isolate 的异常如何采集?

这个问题来自第 39 篇文章“线上出现问题,该如何做好异常捕获与信息采集?”。

并发 Isolate 的异常是无法通过 try-catch 来捕获的。并发 Isolate 与主 Isolate 通信是采用 SendPort 的消息机制,而异常本质上也可以视作一种消息传递机制。所以,如果主 Isolate 想要捕获并发 Isolate 中的异常消息,可以给并发 Isolate 传入 SendPort。

而创建 Isolate 的函数 spawn 中就恰好有一个类型为 SendPort 的 onError 参数,因此并发 Isolate 可以通过往这个参数里发送消息,实现异常通知。

问题 33:依赖单个或多个网络接口数据的页面加载时长应该如何统计?

这个问题来自第 40 篇文章“衡量 Flutter App 线上质量,我们需要关注这三个指标”。

页面加载时长 = 页面完成渲染的时间 - 页面初始化的时间。所以,我们只需要在进入页面时记录启动页面初始化时间,在接口返回数据刷新界面的同时,开启单次帧绘制回调,检测到页面完成渲染后记录页面渲染完成时间,两者相减即可。如果页面的渲染涉及到多个接口也类似。

问题 34:如何设置 Travis 的 Flutter 版本?

设置方式很简单。在 before_install 字段里,克隆 Flutter SDK 时,直接指定特定的分支即可:

  1. git clone -b 'v1.5.4-hotfix.2' --depth 1 https://github.com/flutter/flutter.git

问题 35:如何通过反射快速实现插件定义的标准化?

这个问题来自第 44 篇文章“如何构建自己的 Flutter 混合开发框架(二)?”。

在 Dart 层调用不存在的接口(或未实现的接口),可以通过 noSuchMethod 方法进行统一处理。这个方法会携带一个类型为 Invocation 的参数 invocation,我们可以通过它得到调用的函数名及参数:

  1. //获取方法名
  2. String methodName = invocation.memberName.toString().substring(8, string.length - 2);
  3. //获取参数
  4. dynamic args = invocation.positionalArguments;

其中,参数 args 是一个 List 类型的变量,我们可以在原生代码宿主把相关的参数依次解析出来。有了函数名和参数,我们在插件类实例上,就可以利用反射去动态地调用原生方法了。

与传统的方法调用相比,以反射的方式执行方法调用,其步骤相对繁琐一些,我们需要依次找到并初始化反射调用过程的类示例对象、方法对象、参数列表对象,然后执行反射调用,并根据方法声明获取执行结果。不过这些步骤都是固定的,我们依葫芦画瓢就好。

Android 端的调用方式:

  1. public void onMethodCall(MethodCall call, Result result) {
  2. ...
  3. String method = call.argument("method"); //获取函数名
  4. ArrayList params = call.argument("params"); //获取参数列表
  5. Class<?> c = FlutterPluginDemoPlugin.class; //反射施加对象
  6. Method m = c.getMethod(method, ArrayList.class); //获取方法对象
  7. Object ret = m.invoke(this,params); //在插件实例上调用反射方法,获取返回值
  8. result.success(ret); //返回执行结果
  9. ...
  10. }

iOS 端的调用方式:

  1. - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  2. ...
  3. NSArray *arguments = call.arguments[@"params"]; //获取函数名
  4. NSString *methodName = call.arguments[@"method"]; //获取参数列表
  5. SEL selector = NSSelectorFromString([NSString stringWithFormat:@"%@:",methodName]); //获取函数对应的Slector
  6. NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector]; //在插件实例上获取方法签名
  7. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; //通过方法签名生成反射的invocation对象
  8. invocation.target = self; //设置invocation的执行对象
  9. invocation.selector = selector; //设置invocation的selector
  10. [invocation setArgument:&arguments atIndex:2]; //设置invocation的参数
  11. [invocation invoke]; //执行反射
  12. NSObject *ret = nil;
  13. if (signature.methodReturnLength) {
  14. void *returnValue = nil;
  15. [invocation getReturnValue:&returnValue];
  16. ret = (__bridge NSObject *)returnValue; //获取反射调用结果
  17. }
  18. result(ret); //返回执行结果
  19. ...
  20. }