Flutter 填坑筆記:從flutter?pub?get?error?開始厅贪,定位Dart?SDK問題

??在使用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>

  1. 我的機(jī)器上使用了某燈棒掠,fluter的官網(wǎng)和dart官網(wǎng)訪問都沒有障礙,為何pub不行屁商;
  2. pub需要下載的文件烟很,例如https://pub.dartlang.org/packages/bsdiff/versions/0.1.0.tar.gz,從瀏覽器中是可以快速下載的蜡镶。pub只能從中國(guó)鏡像下載雾袱,而且非常慢,項(xiàng)目初始化時(shí)太浪費(fèi)時(shí)間官还。
  3. 即使連接不上芹橡,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代碼,編譯遗嗽,跟蹤:

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)慎,道路或曲折鞍爱,前途很光明鹃觉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市睹逃,隨后出現(xiàn)的幾起案子盗扇,更是在濱河造成了極大的恐慌,老刑警劉巖沉填,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疗隶,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡翼闹,警方通過查閱死者的電腦和手機(jī)抽减,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)橄碾,“玉大人卵沉,你說我怎么就攤上這事》ㄉ” “怎么了史汗?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拒垃。 經(jīng)常有香客問我停撞,道長(zhǎng),這世上最難降的妖魔是什么悼瓮? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任戈毒,我火速辦了婚禮,結(jié)果婚禮上横堡,老公的妹妹穿的比我還像新娘埋市。我一直安慰自己,他們只是感情好命贴,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布道宅。 她就那樣靜靜地躺著食听,像睡著了一般。 火紅的嫁衣襯著肌膚如雪污茵。 梳的紋絲不亂的頭發(fā)上樱报,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音泞当,去河邊找鬼迹蛤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛襟士,可吹牛的內(nèi)容都是我干的笤受。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼敌蜂,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼箩兽!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起章喉,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤汗贫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后秸脱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體落包,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年摊唇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咐蝇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巷查,死狀恐怖涮俄,靈堂內(nèi)的尸體忽然破棺而出僧凤,到底是詐尸還是另有隱情浅浮,我是刑警寧澤暴浦,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站崇败,受9級(jí)特大地震影響盅称,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜后室,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一缩膝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧岸霹,春花似錦疾层、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至贸桶,卻和暖如春舅逸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背皇筛。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工琉历, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人水醋。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓旗笔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親拄踪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蝇恶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容