??在使用Flutter開發(fā)應(yīng)用的時(shí)候蠢护,有時(shí)需要使用pub工具獲取依賴的包。但是國(guó)內(nèi)的開發(fā)者往往會(huì)遇到下載失敗的問題卦溢,現(xiàn)象為pub進(jìn)程崩潰糊余,堆棧如下:
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
長(zhǎng)文預(yù)警:TLDR版本如下,如果你只想解決下載問題单寂,方案如下:
關(guān)閉代理贬芥,設(shè)置環(huán)境變量,使用國(guó)內(nèi)鏡像下載宣决。
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
??GitHub上已經(jīng)有多個(gè)Issue蘸劈,例如/flutter/issues/25068
Dart Team回復(fù)的官方解決方案就是上面的辦法。
??如果你對(duì)問題根因感興趣尊沸,請(qǐng)往下看威沫。
??作為一個(gè)程序員要有所追求,我是不滿足Dart team這樣的回復(fù)的 ^^洼专。
??因?yàn)橛幸韵乱蓡枺?/p>
- 我的機(jī)器上使用了某燈棒掠,fluter的官網(wǎng)和dart官網(wǎng)訪問都沒有障礙,為何pub不行屁商;
- pub需要下載的文件烟很,例如https://pub.dartlang.org/packages/bsdiff/versions/0.1.0.tar.gz,從瀏覽器中是可以快速下載的蜡镶。pub只能從中國(guó)鏡像下載雾袱,而且非常慢,項(xiàng)目初始化時(shí)太浪費(fèi)時(shí)間官还。
- 即使連接不上芹橡,pub也不能崩潰處理,從Log看肯定是代碼邏輯有問題望伦。
??從以上三點(diǎn)出發(fā)林说,我猜想pub沒有使用代理,還是走原來(lái)的網(wǎng)絡(luò)鏈接屯伞,所以我決定跟蹤一下這個(gè)問題的根本原因述么。
STEP 1: 分析入口:flutter pub get 的處理流程 (flutter_tools)
?? 要想解決問題,首先需要需要找到入口愕掏,Android Studio工程中度秘,更新package使用的是命令行命令:
flutter pub get
flutter 是FLUTTER SDK 中提供的腳本,封裝了各個(gè)工具,作為SDK的總?cè)肟诮J幔嬲У氖侨缦抡Z(yǔ)句:
"$DART" --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
運(yùn)行時(shí)變量如下:
dart --packages=~/flutter/packages/flutter_tools/.packages /~/flutter/bin/cache/flutter_tools.snapshot pub get
解釋一下:
- flutter 作為一個(gè)shell腳本唆貌,最終通過dart命令調(diào)用flutter_tools執(zhí)行pub get命令;
- --packages是命令運(yùn)行時(shí)依賴的package路徑垢乙,這個(gè)場(chǎng)景沒有用到锨咙;
- .snapshot 文件是DART程序預(yù)編譯生成的快照文件,可執(zhí)行追逮,可以簡(jiǎn)單類比JAVA中的.jar文件酪刀。
- $@ 把后續(xù)命令原封不動(dòng)轉(zhuǎn)發(fā)給fluter_tools處理。
flutter_tools代碼路徑在FLUTTER SDK目錄下, 是一個(gè)DART語(yǔ)言編寫的CLI命令行工具:
~/flutter/packages/flutter_tools
在IDE中可以建立DART Command Line Tool工程查看钮孵,編譯這個(gè)工具骂倘,具體可以參考:/flutter/wiki/The-flutter-tool
簡(jiǎn)單分析一下flutter_tool 的代碼邏輯:
- 項(xiàng)目入口:./bin/flutter_tools.dart; IDE中,配置運(yùn)行時(shí)的文件指定這個(gè)巴席,就可以在IDE中運(yùn)行起來(lái)历涝。
void main(List<String> args) {
executable.main(args);
}
- 命令處理流程:和JAVA, C常見的CLI程序結(jié)構(gòu)類似漾唉,就是分析命令行輸入的字符串荧库,路由到對(duì)應(yīng)模塊進(jìn)行處理,例如常用的flutter doctor 命令就在 commands/doctor.dart 中處理:
class DoctorCommand extends FlutterCommand {......}
- pub命令 :pub命令比較特殊赵刑,flutter_tools通過系統(tǒng)命令行接口分衫,調(diào)用外部命令實(shí)現(xiàn)的:
main() ->
Executable.main->
FlutterCommandRunner.runCommand ->
PackageGetCommand._runPubGet ->
pubGet() (lib/src/dart/pub.dart)
最終通過SDK的pub組件執(zhí)行的命令
/// The command used for running pub.
List<String> _pubCommand(List<String> arguments) {
return <String>[ sdkBinaryName('pub') ]..addAll(arguments);
}
也就是說,代理連接失敗般此,問題不在flutter_tools中蚪战,需要繼續(xù)分析pub流程。
STEP 2: 縮小范圍:pub get 的處理流程 (pub)
- pub 的二進(jìn)制文件路徑在~/flutter/bin/cache/dart-sdk/bin/pub恤煞,同樣屎勘,這是一個(gè)shell腳本施籍,最終執(zhí)行的是居扒。./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 可以打印詳細(xì)log.
- pub流程分析限于篇幅這里省略,根據(jù)崩潰堆棧分析和代碼邏輯腻异,pub使用的是dart:io 中的HttpClient
STEP 3: 問題定位:DEMO復(fù)現(xiàn), 編譯SDK进副,跟蹤SDK邏輯
??既然問題在dart:io中,我于是單獨(dú)寫了一個(gè)DEMO悔常,使用 dart:io 中的 HttpClient 測(cè)試影斑,發(fā)現(xiàn)問題竟然可以簡(jiǎn)單復(fù)現(xiàn)给赞,激動(dòng)不已,繞了一大圈終于找到了責(zé)任人:
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);
}
解釋:
- 測(cè)試OS:Ubuntu 18.04
- 開啟某燈:HttpClient使用某燈作為代理(HttpClient. findProxy()設(shè)置)
執(zhí)行矫户,崩潰日志如下, 可以看到和pub崩潰的日志類似都有 The setter 'readEventsEnabled=' was called on null. 姑且認(rèn)為是同一個(gè)問題導(dǎo)致的片迅。
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,崩潰日志完全沒有打印出現(xiàn)場(chǎng)皆辽,T_T柑蛇,但是可以明確的是問題肯定發(fā)生在Dart SDK 的 dart:io library中。
于是驱闷,為了定位問題耻台,下載SDK代碼,編譯遗嗽,跟蹤:
- 下載代碼 https://github.com/dart-lang/sdk
- 編譯指導(dǎo) https://github.com/dart-lang/sdk/wiki/Building
- 編譯debug 版本
cd dart-sdk/sdk
./tools/build.py --mode debug --arch x64 create_sdk
這里再一次吐槽一下Dart粘我,SDK編譯以后,調(diào)試時(shí)竟然不能打斷點(diǎn)跟蹤痹换,后續(xù)有時(shí)間需要分析一下原因征字。
此處省略定位流程(后續(xù)補(bǔ)充HttClient源碼分析,敬請(qǐng)期待)娇豫。
通過跟蹤代碼邏輯匙姜,最終定位到崩潰地址如下:
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設(shè)置代理,此處socket == null冯痢;但是socket為什么為null未知氮昧;
STEP 4 根本原因定位:
問題已經(jīng)定位,但是根本原因并不清楚:socket為什么為null浦楣,正常的代理流程應(yīng)該是什么樣袖肥。
因此,我考慮抓一個(gè)正確的場(chǎng)景日志作參考:
本機(jī)建立了兩個(gè)proxyserver振劳, 一個(gè)是tinyproxy椎组,一個(gè)是lantern。
根據(jù)Http協(xié)議历恐,客戶端首先向proxy server 發(fā)送Connect 請(qǐng)求:
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 回復(fù):程序正常運(yùn)行
HTTP/1.0 200 Connection established
Proxy-agent: tinyproxy/1.8.4
lantern 回復(fù):
HTTP/1.1 200 OK
Date: Wednesday, 14-Aug-19 16:13:22 CST
Keep-Alive: timeout=58
Content-Length: 0
崩潰寸癌!
于是,跟蹤HttpResponse解析流程弱贼,發(fā)現(xiàn) http_parser.dart, _HttpParser._onData 中在處理Http響應(yīng)有差異蒸苇。收到lantern的響應(yīng)后,由于"Content-Length: 0"吮旅,_HttpParser關(guān)閉了socket溪烤,從而導(dǎo)致上述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;
}
因此,修改方案也很簡(jiǎn)單枪眉,增加一個(gè)_keepAlive flag捺檬,當(dāng)Http Response 中有 Keep-Alive 字段時(shí),走tinyproxy分支贸铜,不關(guān)閉socket堡纬。
void _doParse() {
...
if (headerField == "keep-alive") {
_keepAlive = true;
}
...
if ((_transferLength == 0 && !_keepAlive) // 不走這個(gè)分支,走else
...
本地測(cè)試蒿秦,問題解決烤镐。
最后
??洋洋灑灑一大篇,如流水帳一樣記錄了一下Dart SDK的問題定位流程棍鳖∨谝叮回頭來(lái)看,pub使用了代理渡处,只不過dart:io 使用代理時(shí)出現(xiàn)了兼容性問題镜悉。目前這個(gè)問題已經(jīng)提了issues/37808,因?yàn)樯婕暗紿ttpResponse字段的解析医瘫,需要對(duì)HTTP協(xié)議詳細(xì)分析后才能修改侣肄。所以,待最終方案入庫(kù)后SDK更新才能解決pub error的問題醇份。不過對(duì)于我本地而言稼锅,使用本地SDK編譯的pub已經(jīng)可以正常工作了。
??寫幾點(diǎn)學(xué)習(xí)DART的體會(huì)吧:
- Dart的優(yōu)點(diǎn):
現(xiàn)代化的編成語(yǔ)言僚纷,擁有最流行的語(yǔ)言特性(async-await, stream, future),單線程模型降低編碼難度矩距,提升gc效率。最關(guān)鍵的是基于dart的flutter框架真正的支持跨平臺(tái)怖竭,Android锥债,iOS,F(xiàn)uchsia一統(tǒng)天下侵状,前景無(wú)限光明赞弥。 - Dart的不足:太年輕毅整,不夠成熟穩(wěn)重
例如本文這個(gè)問題可以歸類為一個(gè)兼容性問題趣兄。目前DART還很年輕,有很多類似的兼容性的問題可能還會(huì)出現(xiàn)悼嫉。我使用 JAVA 的 APACHE HttpClient寫了一個(gè)測(cè)試程序就沒有這樣的問題艇潭。JAVA生態(tài)成熟度要遠(yuǎn)高于Dart - DART 還需要在易用性問題上作更好的修改,例如目前遇到的StackTrace不太友好,SDK中的文件不能單步跟蹤調(diào)試等蹋凝,對(duì)開發(fā)人員都形成了一些障礙鲁纠。
- 一點(diǎn)建議:對(duì)于dart:io 這個(gè)lib,大量的代碼還是以Future<>.then 的方式寫的鳍寂,如果用async await方式改寫改含,會(huì)更好理解些。
總之迄汛,DART 有風(fēng)險(xiǎn)捍壤,如坑需謹(jǐn)慎,道路或曲折鞍爱,前途很光明鹃觉。