在使用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 team這樣的回覆的 ^_^。
因為有以下疑問:
- 我的機器上使用了某燈,fluter的官網和dart官網訪問都沒有障礙,為何pub不行;
- pub需要下載的檔案,例如https://pub.dartlang.org/packages/bsdiff/versions/0.1.0.tar.gz,從瀏覽器中是可以快速下載的。pub只能從中國映象下載,而且非常慢,專案初始化時太浪費時間。
- 即使連線不上,pub也不能崩潰處理,從Log看肯定是程式碼邏輯有問題。
從以上三點出發,我猜想pub沒有使用代理,還是走原來的網路連結,所以我決定跟蹤一下這個問題的根本原因。
STEP 1: 分析入口:flutter pub get 的處理流程 (flutter_tools)
要想解決問題,首先需要需要找到入口,Android Studio工程中,更新package使用的是命令列命令:
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執行pub get命令;
- --packages是命令執行時依賴的package路徑,這個場景沒有用到;
- .snapshot 檔案是DART程式預編譯生成的快照檔案,可執行,可以簡單類比JAVA中的.jar檔案。
- $@ 把後續命令原封不動轉發給fluter_tools處理。
flutter_tools程式碼路徑在FLUTTER SDK目錄下, 是一個DART語言編寫的CLI命令列工具:
~/flutter/packages/flutter_tools
複製程式碼
在IDE中可以建立DART Command Line Tool工程檢視,編譯這個工具,具體可以參考:/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中,需要繼續分析pub流程。
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 可以列印詳細log.
- 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);
}
複製程式碼
解釋:
- 測試OS:Ubuntu 18.04
- 開啟某燈:HttpClient使用某燈作為代理(HttpClient. findProxy()設定) 執行,崩潰日誌如下, 可以看到和pub崩潰的日誌類似都有 The setter 'readEventsEnabled=' was called on null. 姑且認為是同一個問題導致的。
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 library中。 於是,為了定位問題,下載SDK程式碼,編譯,跟蹤:
- 下載程式碼 github.com/dart-lang/s…
- 編譯指導 github.com/dart-lang/s…
- 編譯debug 版本
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;但是socket為什麼為null未知;
STEP 4 根本原因定位:
問題已經定位,但是根本原因並不清楚:socket為什麼為null,正常的代理流程應該是什麼樣。 因此,我考慮抓一個正確的場景日誌作參考: 本機建立了兩個proxyserver, 一個是tinyproxy,一個是lantern。 根據Http協議,客戶端首先向proxy server 傳送Connect 請求:
CONNECT www.google.com:443 HTTP/1.1
user-agent: Dart2.5(dart:io)
accept-encoding:gzip
content-length:0
host:www.google.com:443
tinyproxy 回覆:程式正常執行
HTTP/1.0 200 Connection established
Proxy-agent: tinyproxy/1.8.4
lantern 回覆:
HTTP/1.1 200 OK
Date: Wednesday, 14-Aug-19 16:13:22 CST
Keep-Alive: timeout=58
Content-Length: 0
崩潰!
於是,跟蹤HttpResponse解析流程,發現 http_parser.dart, _HttpParser._onData 中在處理Http響應有差異。收到lantern的響應後,由於"Content-Length: 0",_HttpParser關閉了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 flag,當Http Response 中有 Keep-Alive 欄位時,走tinyproxy分支,不關閉socket。
void _doParse() {
...
if (headerField == "keep-alive") {
_keepAlive = true;
}
...
if ((_transferLength == 0 && !_keepAlive) // 不走這個分支,走else
...
複製程式碼
本地測試,問題解決。
最後
洋洋灑灑一大篇,如流水帳一樣記錄了一下Dart SDK的問題定位流程。回頭來看,pub使用了代理,只不過dart:io 使用代理時出現了相容性問題。目前這個問題已經提了issues/37808,因為涉及到HttpResponse欄位的解析,需要對HTTP協議詳細分析後才能修改。所以,待最終方案入庫後SDK更新才能解決pub error的問題。不過對於我本地而言,使用本地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 的方式寫的,如果用async await方式改寫,會更好理解些。
總之,DART 有風險,如坑需謹慎,道路或曲折,前途很光明。