You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

30 KiB

特别放送 温故而知新,与你说说专栏的那些思考题

你好,我是陈航。专栏上线以来,我在评论区看到了很多同学写的心得、经验和建议,当然更多的还是大家提的问题。

为了能够让大家更好地理解我们专栏的核心知识点,我今天特意整理了每篇文章的课后思考题,并结合大家在留言区的回答情况做一次分析与扩展。

当然 我也希望你能把这篇答疑文章作为对整个专栏所讲知识点的一次复习如果你在学习或者使用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而不能做累加。比如五斤苹果和三盒巧克力做合并结果是一份巧克力苹果套餐而不是八份巧克力苹果套餐。

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

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

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

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

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

  • 在iOS中Xib相当于WidgetUIView相当于ElementCALayer相当于renderObject
  • 在Android中XML相当于WidgetView相当于ElementCanvas相当于renderObject
  • 在Web中以Vue为例Vue的模板相当于Widgetvirtual DOM相当于ElementDOM相当于RenderObject。

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

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

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

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

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

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

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

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

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

这个问题来自第13篇文章“经典控件UITableView/ListView在Flutter中是什么”。

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

问题9Row与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在计算项目依赖时当前工程所有依赖的本地缓存文件。与本地环境有关无需统一。

问题13GestureDetector内嵌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函数即可。

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

AnimatedBuilder(
  animation: animation,
  child:FlutterLogo(),
  builder: (context, child) => Container(
    width: animation.value,
    height: animation.value,
    child: child
  )
)

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

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

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

//并发计算阶乘
Future<dynamic> asyncFactoriali(n) async{
  final response = ReceivePort();//创建管道
  //创建并发Isolate并传入管道
  await Isolate.spawn(_isolate,response.sendPort);
  //等待Isolate回传管道
  final sendPort = await response.first as SendPort;
  //创建了另一个管道answer
  final answer = ReceivePort();
  //往Isolate回传的管道中发送参数同时传入answer管道
  sendPort.send([n,answer.sendPort]);
  return answer.first;//等待Isolate通过answer管道回传执行结果
}

//Isolate函数体参数是主Isolate传入的管道
_isolate(initialReplyTo) async {
  final port = ReceivePort();//创建管道
  initialReplyTo.send(port.sendPort);//往主Isolate回传管道
  final message = await port.first as List;//等待主Isolate发送消息(参数和回传结果的管道)
  final data = message[0] as int;//参数
  final send = message[1] as SendPort;//回传结果的管道 
  send.send(syncFactorial(data));//调用同步计算阶乘的函数回传结果
}

//同步计算阶乘
int syncFactorial(n) => n < 2 ? n : n * syncFactorial(n-1);
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.0->2.0执行表结构修改语句A
2.0->3.0执行表结构修改语句B
3.0->4.0执行表结构修改语句C
4.0->5.0执行表结构修改语句D

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

switch(oldVersion) {
 case '1.0': do A;
 case '2.0': do B;
 case '3.0': do C;
 case '4.0': do D;
 default: print('done');
}

这样就万无一失了。

不过需要注意的是在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文件来实现组件依赖的原生部分的封装。具体细节你可以参考第44篇文章

问题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增加单元测试用例

Future<bool>updateSP(SharedPreferences prefs, int counter) async {
  bool result = await prefs.setInt('counter', counter);
  return result;
}

Future<int>increaseSPCounter(SharedPreferences prefs) async {
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  await updateSP(prefs, counter);
  return counter;
}

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

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

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

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

对于getInt我们只需要返回2

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版本

这个问题来自第42篇文章“如何构建高效的Flutter App打包发布环境”。

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

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

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

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

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

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

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

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

Android端的调用方式

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

iOS端的调用方式

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
 ...
  NSArray *arguments = call.arguments[@"params"]; //获取函数名
  NSString *methodName = call.arguments[@"method"]; //获取参数列表
  SEL selector = NSSelectorFromString([NSString stringWithFormat:@"%@:",methodName]); //获取函数对应的Slector
  NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector]; //在插件实例上获取方法签名
  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; //通过方法签名生成反射的invocation对象
        
  invocation.target = self; //设置invocation的执行对象
  invocation.selector = selector; //设置invocation的selector     
  [invocation setArgument:&arguments atIndex:2]; //设置invocation的参数

  [invocation invoke]; //执行反射
        
  NSObject *ret = nil;
  if (signature.methodReturnLength) {
      void *returnValue = nil;
      [invocation getReturnValue:&returnValue];
      ret = (__bridge NSObject *)returnValue; //获取反射调用结果
  }    
              
  result(ret); //返回执行结果
  ...      
}

以上就是“Flutter核心技术与实战”专栏全部思考题的答案了。你如果还有其他问题的话欢迎给我留言我们一起讨论。