flutter pub gets error 开始定位Dart SDK问题
当您使用Flutter开发应用程序时,有时需要使用pub工具来获取依赖包。然而国内开发者经常会遇到下载失败的问题。现象是pub进程卡住了。堆栈如下:
Running "flutter packages get" in startup_namer...
The setter 'readEventsEnabled=' was called on null.
Receiver: null
Tried calling: readEventsEnabled=false
package:pub/src/source/hosted.dart 344 BoundHostedSource._throwFriendlyError
package:pub/src/source/hosted.dart 144 BoundHostedSource.doGetVersions
复制代码
长文警告:TLDR版本如下。如果只是想解决下载问题,解决方法如下:
禁用代理,设置环境变量,使用国内镜像下载。
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
复制代码
GitHub上已经有几个issue,比如/flutter/issues/25068
Dart Team官方解答的解决方案就是上面的方法。
如果您对问题的原因感兴趣,请阅读以下内容。
作为程序员,我有追求,我对Dart团队的回答并不满意^_^。
因为我有以下问题:
- 我的机器上使用的是某台灯,访问fluter官网和飞镖官网没有任何障碍。为什么我无法度过青春期?
- pub需要下载文件,如https://pub.dartlang.org/packages/bsdiff/versions/0.1.0.tar.gz,可以通过浏览器快速下载。 Pub只能从中文镜像下载,而且速度很慢。在项目初始化期间这是浪费时间。
- 即使无法建立连接,pub也不会崩溃。从日志来看,代码逻辑肯定有问题。
基于以上三点,我怀疑pub没有使用代理,仍然使用原来的网络链接,所以我决定追查这个问题的原因。
STEP 1:分析输入:flutter pub get处理流(flutter_tools)
要解决问题,首先需要找到该项目。在Android Studio项目中,使用命令行命令来更新包:
flutter pub get
复制代码
flutter 是FLUTTER SDK中提供的一个脚本,包含了几个工具。作为一般进入SDK,下面的语句才是真正生效的:
"$DART" --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
复制代码
运行时变量如下:
dart --packages=~/flutter/packages/flutter_tools/.packages /~/flutter/bin/cache/flutter_tools.snapshot pub get
复制代码
说明:
- flutter 作为shell脚本,最终通过dart命令调用flutter_tools来执行酒吧负责;
- --packages是命令运行时依赖的包路径,本场景不使用;
- .snapshot 文件是由 DART 程序预编译并可执行的快照文件。它可以很容易地与JAVA中的.jar文件进行比较。
- $@ 将以下命令原封不动的转发给Future_tools进行处理。
flutter_tools代码路径位于FLUTTER SDK文件夹中。它是一个用 DART 语言编写的 CLI 命令行工具:
~/flutter/packages/flutter_tools
复制代码
您可以在 IDE 中创建 DART 命令行工具项目来查看和编译该工具。详情参见:/flutter/wiki/The-flutter-tool
flutter_tool代码逻辑简析:
- 项目入口:./bin/flutter_tools.dart;在配置运行时文件中的 IDE 中指定它,您就可以在 IDE 中运行它。
void main(List<String> args) {
executable.main(args);
}
复制代码
- 命令处理流程:与JAVA、C中一般的CLI程序结构类似,对命令行输入的字符串进行分析,并发送给相应的模块进行处理。比如常用的flutter doctor命令,在commands/doctor.dart中处理:
class DoctorCommand extends FlutterCommand {......}
复制代码
- pub命令:pub命令很特殊。 Flutter_tools 通过系统的命令行接口调用外部命令:
main() ->
Executable.main->
FlutterCommandRunner.runCommand ->
PackageGetCommand._runPubGet ->
pubGet() (lib/src/dart/pub.dart)
复制代码
该命令最终是通过 SDK 的 pub 组件执行的
/// The command used for running pub.
List<String> _pubCommand(List<String> arguments) {
return <String>[ sdkBinaryName('pub') ]..addAll(arguments);
}
复制代码
也就是说,代理连接失败,问题不在于 flutter_tools,你应该继续分析酒吧进程。
STEP 2:缩小范围:pub get(pub)处理流程
- pub二进制路径位于**~/flutter/bin/cache/dart-sdk/bin/pub**,同样,这个是最终执行的shell脚本。 ./flutter/bin/cache/dart-sdk/bin/snapshots/pub.dart.snapshot
- 要解决这个问题,我们需要pub的源码。 pub 是 dart sdk 提供的工具,所以源码在 dart-lang 中,./dart-lang/pub
- 还配置了 Android Studio 中的 Dart Comman Line 项目,不再赘述。pug get -v 可以打印详细日志。
- 因篇幅限制这里省略Pub流程分析。根据崩溃堆栈分析和代码逻辑,pub使用了dart:io中的HttpClient
STEP 3:问题位置:DEMO复现,编译SDK,遵循SDK逻辑
因为问题出在dart:io ,我单独写了一个DEMO,并在dart:io中用HttpClient进行了测试。我发现这个问题可以很容易地重现。我很热情,解决了问题。绕了一圈终于找到负责人了:
import "dart:io";
import 'dart:convert';
main() async {
var google = "https://www.google.com/";
var httpClient = HttpClient();
// // Lantern proxy, cause crash
httpClient.findProxy = (uri) {
return "PROXY 127.0.0.1:45653";
};
HttpClientRequest request = await httpClient.getUrl(Uri.parse(baidu));
HttpClientResponse response = await request.close();
var responseBody = await response.transform(Utf8Decoder()).join();
print(responseBody);
}
复制代码
说明:
- 测试操作系统:Ubuntu 18.04
- 打开某盏灯:HttpClient使用某盏灯作为代理(设置HttpClient.findProxy()) 。运行,崩溃日志如下。您可以看到 setter 'readEventsEnabled=' 是在 null 上调用的,并且与 pub 崩溃日志类似。我们认为这是由同一问题引起的。
Unhandled exception: NoSuchMethodError:
The setter 'readEventsEnabled=' was called on null.
Receiver: null Tried calling: readEventsEnabled=false
#0 _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1112:29)
#1 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
#2 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)
#3 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:116:13)
#4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:173:5)
复制代码
这是对 Dart 的 StackTrace 的抱怨。崩溃日志根本没有打印场景,T_T,但很明显问题一定出在 Dart SDK 的 dart:io 库中。所以,要定位问题,先下载SDK代码,编译并按照:
- 下载代码github.com/dart-lang/s…
- 编译说明github.com/dart-lang/s…
- 编译调试版本
cd dart-sdk/sdk
./tools/build.py --mode debug --arch x64 create_sdk
复制代码
在这里我也想抱怨一下Dart。 SDK编译完成后,调试时不能中断点位跟踪。等以后有时间我再分析一下原因。这里省略定位过程(稍后会添加HttClient源码分析,敬请关注)。按照代码逻辑,最终找到了崩溃地址如下:
static Future<RawSecureSocket> secure(RawSocket socket,
{StreamSubscription<RawSocketEvent> subscription,
host,
SecurityContext context,
bool onBadCertificate(X509Certificate certificate),
List<String> supportedProtocols}) {
**//crashed at the following line. socket == null**
socket.readEventsEnabled = false;
**//crashed at the following line. socket == null**
socket.writeEventsEnabled = false;
......
}
复制代码
HttpClient设置了代理,这里socket == null;但为什么套接字为零是未知的;
STEP 4 根本原因定位:
问题已经找到,但根本原因还不清楚:为什么socket为空,正常的代理进程应该是什么。这就是为什么我考虑使用合适的场景日志作为参考:这台机器设置了两个代理服务器,一个是tinyproxy,另一个是lantern。根据 HTTP 协议,客户端首先向代理服务器发送 Connect 请求:
CONNECT www.google.com:443 HTTP/1.1
user-agent: Dart2.5(dart:io)
接受-编码:gzip
内容长度:0
主机:www.google.com:443
tinyproxy答案:程序正常工作
HTTP/1.0 200连接已建立
代理-代理:tinyproxy/1.8。 4
lantern 回复:
HTTP/1.1 200 OK
日期:2019 年 8 月 14 日星期三下午 4:13:22 CST
保持活动:超时=58
内容长度: 0
崩溃!
所以当我跟踪HttpResponse解析过程时,我发现http_parser.dart、_HttpParser._onData中对Http响应的处理存在差异。 _HttpParser收到Lantern的响应后,由于“Content-Length: 0”关闭了socket,导致上面的socket == null。 Tinyproxy走另一个分支并且socket被保留,所以没有问题。
http_parser.dart
bool _headersEnd() {
......
if (_transferLength == 0 ||
(_messageType == _MessageType.RESPONSE && _noMessageBody)) {
_reset();
var tmp = _incoming;
*****socket will closed here as "Content-Length: 0"
_closeIncoming();
_controller.add(tmp);
return false;
} else if (_chunked) {
_state = _State.CHUNK_SIZE;
_remainingContent = 0;
} else if (_transferLength > 0) {
_remainingContent = _transferLength;
_state = _State.BODY;
} else {
*****tinyproxy will go to this branch. not closing socket
// Neither chunked nor content length. End of body
// indicated by close.
_state = _State.BODY;
}
复制代码
这就是为什么改变计划非常简单。添加 _keepAlive 标志。如果Http响应中有Keep-Alive字段,则采取tinyproxy分支,并且不要关闭套接字。
void _doParse() {
...
if (headerField == "keep-alive") {
_keepAlive = true;
}
...
if ((_transferLength == 0 && !_keepAlive) // 不走这个分支,走else
...
复制代码
本地测试,问题解决。
最后
是一篇长文,以流水账的形式记录了Dart SDK的排查过程。回过头来看,pub 使用了代理,但是 dart:io 在使用代理时存在兼容性问题。目前此问题已在 issues/37808 中得到解决,因为它涉及解析 HttpResponse 字段,并且需要对 HTTP 协议进行详细分析才能更改。因此,pub bug问题只能在最终修复发布到数据库后通过更新SDK来解决。但对于我的本地用户来说,使用本地SDK构建的pub已经可以正常工作了。
写下学习DART的一些心得:
- Dart的优点:现代编程语言,拥有最流行的语言特性(async-await、stream、future),单线程模型减少了编码问题,提高了gc-效率 。最重要的是,基于Dart的flutter框架真正支持跨平台,Android、iOS和Fuchsia一统天下,未来无限光明。
- Dart的缺陷:太年轻,不够成熟。例如,本文中的这个问题可以归类为兼容性问题。目前,DART 还很年轻,仍然可能会出现许多类似的兼容性问题。我用JAVA的APACHE HttpClient写了一个测试程序,没有出现这样的问题。 JAVA的环境成熟度比Dart高很多
- DART也需要做出更好的可用性调整。比如目前遇到的StackTrace不太友好以及无法一步追踪SDK中的文件等等,这对于开发来说是非常困难的。人们制造了一些障碍。
- 一个小建议:对于 dart:io lib,仍然有大量的代码是用 Future.then 编写的。如果异步重写的话会更好理解。
简而言之,DART 存在风险。如果有陷阱的话,就得小心了。道路可能会曲折,但未来是光明的。
作者:TonyBuilder
来源:掘金
版权归作者所有。商业转载请联系作者获得许可。非商业转载请注明出处。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。