Flutter Logging
1. Install package logging
Add logging
to pubspec.yaml
file:
1 | dependencies: |
2. Config Logger
Initialize Logger
before runApp()
in main.dart
file:
1 | import 'logging.dart'; |
1 | import 'package:logging/logging.dart'; |
3. Transmit log records from Logger to dart:developer
Logger is a producer
, but it will not post any log records if no one is listening.
Print to console:
1 | Logger.root.onRecord.listen((event) { |
Save to file:
1 | File logFile = ... |
It’s strongly recommended to use dart:developer
for logging:
1 | import 'dart:developer' as developer; |
Flutter HttpClient Overview
Flutter 上应该怎么请求http?很简单,直接用 dart:io
包下的 HttpClient,如下代码:
1 | HttpClient client = HttpClient(); |
但你肯定也发现了 dart:io
的 HttpClient
所提供的API太过底层了,所以一般不会直接用,而是用Dart官方提供的 http 包(package:http),如下代码:
1 | import 'package:http/http.dart'; |
或者有很多人喜欢的 dio。
但无论怎么封装API,底层都还是 dart:io
里的HttpClient
。
你可能一直有疑问,不是说 Flutter 是单线程的,那 http 请求难道不会卡住 UI 线程,导致 UI 无响应吗?
Flutter 同步系统的 HTTP 代理设置
一般的,在 Flutter APP 里请求 HTTP 使用的是官方提供的 http 包。
1 | import 'package:http/http.dart' as http; |
但是,有一个问题,在 Android 或者 iOS 上运行 Flutter APP,系统里配置的 HTTP 代理并不生效?
比如在使用 Charles 这种工具通过 HTTP 代理调试 API 请求时候,会发现 Flutter 的 http 请求没有按预期走代理,无论是 Http 还是 Https。
那些初学者实践 Flutter 最常出现的错误
哔哩哔哩漫画APP实践Flutter也有大半年时间了,我针对线上收集到的错误进行分析,挑选出了一些有一般代表性的错误,列在本文,可供实践 Flutter 的初学者们作为一点参考。
典型错误一:无法掌握的Future
典型错误信息:NoSuchMethodError: The method 'markNeedsBuild' was called on null.
这个错误常出现在异步任务(Future)处理,比如某个页面请求一个网络API数据,根据数据刷新 Widget State。
异步任务结束在页面被pop之后,但没有检查State 是否还是 mounted
,继续调用 setState
就会出现这个错误。
示例代码
一段很常见的获取网络数据的代码,调用 requestApi()
,等待Future从中获取response
,进而setState
刷新 Widget:
1 | class AWidgetState extends State<AWidget> { |
原因分析
response
的获取为async-await
异步任务,完全有可能在AWidgetState
被 dispose
之后才等到返回,那时候和该State
绑定的 Element
已经不在了。故而在setState
时需要容错。
解决办法: setState
之前检查是否 mounted
1 | class AWidgetState extends State { |
这个mounted
检查很重要,其实只要涉及到异步还有各种回调(callback),都不要忘了检查该值。
比如,在 FrameCallback
里执行一个动画(AnimationController):
1 |
|
AnimationController
有可能随着 State 一起 dispose
了,但是FrameCallback
仍然会被执行,进而导致异常。
又比如,在动画监听的回调里搞点事:
1 |
|
同样的在_handleAnimationTick
被回调前,State 也有可能已经被dispose
了。
如果你还不理解为什么,请仔细回味一下Event loop
还有复习一下 Dart 的线程模型。
相亲困境
最近 2019-nCoV 疫情漫延,天天闲在家也没事干,突然觉得非常孤单寂寞冷,今天回顾了一下我相亲这么多回还是孑然一身的原因。以下为正文。。
每逢假期,回老家相亲是不可回避的乡亲们茶余饭后喜闻乐见的节目。
素未谋面的两人抽出时间,凑在一起碰过面之后,如果一方觉得可以进一步发展试试,但却不知对方的态度,接下来就会纠结:是主动表态还是被动等待。
一旦开始了选择,这其实可以将理性的双方看作处于一场非零和博弈中了。
双方组合起来有如下四种策略:
🧑主动 | 🧑等待 | |
---|---|---|
👨主动 | (2,2) | (0,3) |
👨等待 | (3,0) | (1,1) |
- 如果🧑👨双方都主动表达好感👩❤️💋👨,意味着可以顺利发展下一步,这时可认为双方获得奖励性回报,双赢,各得2分。
- 如果🧑主动表达好感,而👨没有,那么认为👨得到“渣男卡”回报3分,而🧑得0分。
- 反过来,👨主动表态,热脸贴冷屁股,👨获得“好人卡”回报0分,🧑3分。
- 如果🧑👨都等待对方表态,那多半意味着相亲失败,不过多认识了朋友而已,各得1分。
在实际“博弈”中,理性的🧑👨双方都会站在自己的角度,只考虑到自己的最佳回报,不约而同地选择了等待(“纳什均衡”),亦即这场相亲最后不了了之,bad end。
哔哩哔哩漫画 Flutter 混合开发实践
哔哩哔哩的漫画业务是在18年末正式官宣成立的,之后便有了收(jie)购(pan)网易漫画的事。虽说是“新业务”,其实内部立项之早或可追溯到2017年,APP 的代码饱含着历史沧桑感,流淌来自B站内部多个部门程序员的心血,杂糅了多种技术栈和代码风格,令现维护者头疼到头秃。
2019年初适逢 Flutter 声名鹊起,经过一段时间技术调研和在网易漫画 APP 上试验后,便开始在哔哩哔哩漫画 APP (后文简称B漫)上紧锣密鼓一步一个脚印跟着版本迭代与用户见面。我给 Flutter 在B漫的落地计划起了个名叫“胡辣汤计划”。
胡辣汤
一晃时间到了19年底,不过区区 6 人的小组将“胡辣汤计划”已经从试验阶段实施到成熟阶段,开始直接对接新页面和业务功能。iOS 和 Android 双端 APP 已由 Flutter 覆盖了多个主路径页面,重写的页面和原来页面相比用户几乎没有感知到差别,日均超七成活跃用户使用过 Flutter 所呈现的页面功能,并且维持着这些页面功能 0.01% 的低错误率、崩溃率,这不能说不是得益于 Flutter 的优秀设计。
网易漫画和哔哩哔哩漫画
记一例 Android 无障碍服务(Accessibility)引发的崩溃
Logs
来自线上用户的一个神奇崩溃,日志如下:
java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0 at android.text.SpannableStringBuilder.checkRange(SpannableStringBuilder.java:1330) at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:684) at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:676) at android.view.accessibility.AccessibilityNodeInfo.setText(AccessibilityNodeInfo.java:2645) at android.widget.TextView.onInitializeAccessibilityNodeInfoInternal(TextView.java:11652) at android.view.View.onInitializeAccessibilityNodeInfo(View.java:8257) at android.view.View.createAccessibilityNodeInfoInternal(View.java:8216) at android.view.View.createAccessibilityNodeInfo(View.java:8201) at android.view.AccessibilityInteractionController$AccessibilityNodePrefetcher.prefetchDescendantsOfRealNode(AccessibilityInteractionController.java:1204) at android.view.AccessibilityInteractionController$AccessibilityNodePrefetcher.prefetchAccessibilityNodeInfos(AccessibilityInteractionController.java:1029) at android.view.AccessibilityInteractionController.findAccessibilityNodeInfoByAccessibilityIdUiThread(AccessibilityInteractionController.java:341) at android.view.AccessibilityInteractionController.access$400(AccessibilityInteractionController.java:75) at android.view.AccessibilityInteractionController$PrivateHandler.handleMessage(AccessibilityInteractionController.java:1393) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7356) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
当用户触发无障碍服务 (Accessibility) 时,会遍历可点击的 TextView。TextView 再去创建 AccessibilityNodeInfo 传递给无障碍服务,但 AccessibilityNodeInfo 在获取文本用于构造 SpannableStringBuilder 时却发生了异常—— java.lang.IndexOutOfBoundsException
Why?
下面这段摘抄自 AccessibilityNodeInfo.java 的代码告诉了我们原因:
1 | public void setText(CharSequence text) { |
上述代码关键是在替换 text
中的 ClickableSpan
对象为 AccessibilityURLSpan
或者AccessibilityClickableSpan
:
- 首先,从原始的
text
中获取的ClickableSpan
对象数组spans
。 - 其次,遍历获取每个
ClickableSpan
在原始text
中的位置。 - 最后,替换掉
Spannable
对应位置的ClickableSpan
。
崩溃就发生最最后一步 spannable.setSpan(...)
。程序执行到这里的时候, spanToReplaceStart
和spanToReplaceEnd
都是 -1
,就是说对应的 ClickableSpan
在经过 SpannableStringBuilder
拷贝后不见了 !!
why ???
三十而立
站在三十岁的第一天回头看,
生活就是一个七天接着一个七天。
人生短暂,
须臾模糊了发际线。
时间教会我的:
现在过的每一天,
都是余生中最年轻的一天。
。
。
。
没错,句子是东拼西凑抄来的╮( ̄▽ ̄)╭
如何代码获取 Flutter APP 的 FPS
众所周知,官方提供了好几个办法来让我们在开发 Flutter app 的过程中可以使用查看 fps等性能数据,如devtools,具体见文档 Debugging Flutter apps 、Flutter performance profiling 等。
但是这些工具统计到的数据充其量只能算开发过程中的“试验室”数据,假如需要统计app 在线上在用户手机上的运行情况,该如何在 flutter 端代码里自己计算性能数据,比如 fps
这个值呢?
经过阅读源码,发现其实很简单,给window
对象注册 onReportTimings
即可,去看api文档。
1 | void main() { |
注意:当升级到Flutter 1.12.x 之后,onReportTimings
应该改成SchedulerBinding
的addTimingsCallback
1 | // 需监听fps时注册 |
代码给你看
考虑计算fps
,只需要保留最近 N
个FrameTiming
来计算即可,最好用类似stack的数据结构存起来,参考了文档,我们选用 Queue ,N 指定为 100
1 | const maxframes = 100; // 100 帧足够了,对于 60 fps 来说 |
1 | +---------------------------------------------------------------+ |
lastFrames
的头就是最后一帧,尾是队伍里最开始的一帧,现在你可以计算 FPS 了:
1 | double get fps { |
但,你会发现,这样算出来和官方工具算的对不上,而且错的离谱。
why ??