【在 Nervos CKB 上做開發(fā)】Nervos CKB 腳本編程簡介[5]:調(diào)試 debug

作者:Xuejie
原文鏈接:https://xuejie.space/2019_10_18_introduction_to_ckb_script_programming_debugging/

Nervos CKB 腳本編程簡介[5]:調(diào)試 debug

事實上默辨,CKB 腳本工作的層級要比其他智能合約低很多,因此 CKB 的調(diào)試過程就顯得相當(dāng)神秘女蜈。在本文中贡蓖,我們將展示如何調(diào)試 CKB 腳本土榴。你會發(fā)現(xiàn)朽肥,其實調(diào)試 CKB 腳本和你日常調(diào)試程序并沒有太大區(qū)別橙数。

本文建立在 ckb v0.23.0 之上座韵。具體的,我在每個項目中使用的是如下版本的 commit:

  • ckb: 7e2ad2d9ed6718360587f3762163229eccd2cf10
  • ckb-sdk-ruby: 18a89d8c69e173ad59ce3e3b3bf79b5d11c5f8f8
  • ckb-duktape:347bf730c08eb0aab7e56e0357945a4d6cee109a
  • ckb-standalone-debugger: 2379e89ae285e4e639b961756c22d8e4fde4d6ab

使用 GDB 調(diào)試 C 程序

CKB 腳本調(diào)試的第一種方案堤舒,通常適用于 C、Rust 等編程語言哺呜。也許你已經(jīng)習(xí)慣了寫 C 的程序舌缤,而 GDB 也是你的好搭檔。你想知道是不是可以用 GDB 來調(diào)試 C 程序某残,答案當(dāng)然是:Yes国撵!你肯定可以通過 GDB 來調(diào)試用 C 編寫的 CKB 腳本!讓我來演示一下:

首先玻墅,我們還是用之前文章中用到的關(guān)于 carrot 的例子:

#include <memory.h>#include "ckb_syscalls.h"
int main(int argc, char* argv[]) {
  int ret;
  size_t index = 0;
  uint64_t len = 0;
  unsigned char buffer[6];
  while (1) {
    len = 6;
    memset(buffer, 0, 6);
    ret = ckb_load_cell_data(buffer, &len, 0, index, CKB_SOURCE_OUTPUT);
    if (ret == CKB_INDEX_OUT_OF_BOUND) {
      break;
    }
    int cmp = memcmp(buffer, "carrot", 6);
    if (cmp) {
      return -1;
    }
    index++;
  }
  return 0;
}

這里我進行了兩處修改:

首先我更新了這個腳本介牙,讓它可以兼容 ckb v0.23.0。在這個版本中澳厢,我們可以使用 ckb_load_cell_data 來獲取 cell 的數(shù)據(jù)环础。

我還在這段代碼中加入了一個小 bug,這樣我們等會兒就可以進行調(diào)試的工作流程剩拢。如果你非常熟悉 C线得,你可能已經(jīng)注意到了,當(dāng)然你沒有在意到的話也完全不用擔(dān)心徐伐,稍后我會解釋的贯钩。

和往常一樣,我們使用官方的 toolchain 來將其編譯成 RISC-V 的代碼:

$ ls
carrot.c
$ git clone https://github.com/nervosnetwork/ckb-system-scripts
$ cp ckb-system-scripts/c/ckb_*.h ./
$ ls
carrot.c  ckb_consts.h  ckb_syscalls.h  ckb-system-scripts/
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
root@3efa454be9af:/# cd /code
root@3efa454be9af:/code# riscv64-unknown-elf-gcc carrot.c -g -o carrot
root@3efa454be9af:/code# exit

請注意办素,當(dāng)我編譯腳本的時候角雷,我添加了 -g,以便生成調(diào)試信息性穿,這在 GDB 中非常有用勺三。對于實際使用的腳本,你總是希望盡量地完善它們來盡量節(jié)省存儲在鏈上的空間需曾。

現(xiàn)在檩咱,讓我們將腳本部署到 CKB 上揭措。保持 CKB 節(jié)點處于運行狀態(tài),并啟動 Ruby SDK:

pry(main)> api = CKB::API.new
pry(main)> wallet = CKB::Wallet.from_hex(api, "<your private key>")
pry(main)> wallet2 = CKB::Wallet.from_hex(api, CKB::Key.random_private_key)
pry(main)> carrot_data = File.read("carrot")
pry(main)> carrot_data.bytesize
=> 19296
pry(main)> carrot_tx_hash = wallet.send_capacity(wallet2.address, CKB::Utils.byte_to_shannon(20000), CKB::Utils.bin_to_hex(carrot_data), fee: 21000)
pry(main)> carrot_data_hash = CKB::Blake2b.hexdigest(carrot_data)
pry(main)> carrot_type_script = CKB::Types::Script.new(code_hash: carrot_data_hash, args: "0x")
pry(main)> carrot_cell_dep = CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: carrot_tx_hash, index: 0))

現(xiàn)在鏈上有了 carrot 的腳本刻蚯,我們可以創(chuàng)建一筆交易來測試這個 carrot 腳本:

pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(100), use_dep_group: false, fee: 5000)
pry(main)> tx.outputs[0].type = carrot_type_script
pry(main)> tx.cell_deps << carrot_cell_dep
pry(main)> tx.witnesses[0] = "0x"
pry(main)> tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))
pry(main)> api.send_transaction(tx)
CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"Script(ValidationFailure(-1))"}

如果你仔細檢查這筆交易绊含,你會發(fā)現(xiàn)在輸出的 cell 中,并沒有以 carrot 開頭的數(shù)據(jù)炊汹。然而我們運行之后仍然是驗證失敗躬充,這意味著我們的腳本一定存在 bug。先前讨便,沒什么別的辦法充甚,你可能需要返回去檢查代碼,希望可以找到出錯的地方霸褒。但現(xiàn)在沒有這個必要了伴找,你可以跳過這里的交易,然后將其輸入到一個獨立的 CKB 調(diào)試器開始調(diào)試它废菱!

首先技矮,讓我們將這筆交易連同使用的環(huán)境,都轉(zhuǎn)存到一個本地文件中:

pry(main)> CKB::MockTransactionDumper.new(api, tx).write("carrot.json")

在這里你還需要跟蹤 carrot 類型腳本的哈希:

pry(main)> carrot_type_script.compute_hash
=> "0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933"

請注意殊轴,你可能會得到和我這里不一樣的哈希衰倦,這得看你使用的環(huán)境。

現(xiàn)在旁理,讓我們來試試 ckb-standalone-debugger:

$ git clone https://github.com/nervosnetwork/ckb-standalone-debugger
$ cd ckb-standalone-debugger/bins
$ cargo build --release
$ ./target/release/ckb-debugger -l 0.0.0.0:2000 -g type -h 0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933 -t carrot.json

注意樊零,你可能需要根據(jù)你的環(huán)境,調(diào)整 carrot 類型腳本的哈夏跷模或者 carrot.json 的路徑∽そ螅現(xiàn)在讓我們試試在一個不同的終端內(nèi)通過 GDB 連接調(diào)試器:

$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
root@66e3b39e0dfd:/# cd /code
root@66e3b39e0dfd:/code# riscv64-unknown-elf-gdb carrot
GNU gdb (GDB) 8.3.0.20190516-git
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-unknown-elf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from carrot...
(gdb) target remote 192.168.1.230:2000
Remote debugging using 192.168.1.230:2000
0x00000000000100c6 in _start ()
(gdb)

注意,這里的 192.168.1.230 是我的工作站在本地網(wǎng)絡(luò)中的 IP 地址芋哭,你可能需要調(diào)整該地址塑悼,因為你的計算機可能是不同的 IP 地址。現(xiàn)在我們可以試一下常見的 GDB 調(diào)試過程:

(gdb) b main
Breakpoint 1 at 0x106b0: file carrot.c, line 6.
(gdb) c
Continuing.

Breakpoint 1, main (argc=0, argv=0x400000) at carrot.c:6
6         size_t index = 0;
(gdb) n
7         uint64_t len = 0;
(gdb) n
11          len = 6;
(gdb) n
12          memset(buffer, 0, 6);
(gdb) n
13          ret = ckb_load_cell_data(buffer, &len, 0, index, CKB_SOURCE_OUTPUT);
(gdb) n
14          if (ret == CKB_INDEX_OUT_OF_BOUND) {
(gdb) n
18          int cmp = memcmp(buffer, "carrot", 6);
(gdb) n
19          if (cmp) {
(gdb) p cmp
$1 = -99
(gdb) p buffer[0]
$2 = 0 '\000'
(gdb) n
20            return -1;

這里我們可以看到哪里出問題了:buffer 中第一個字節(jié)的值是 0楷掉,這和 c 不同厢蒜,因此我們的 buffer 和 carrot 不同。條件 if (cap) { 沒有跳轉(zhuǎn)到下一個循環(huán)烹植,而是跳到了 true 的情況斑鸦,返回了 -1,表明與 carrot 匹配草雕。出現(xiàn)這樣問題的原因是巷屿,當(dāng)兩個 buffers 相等的時候,memcmp 將會返回 0墩虹,當(dāng)它們不相等的時候嘱巾,將返回非零值憨琳。但是我們沒有測試 memcmp 的返回值是否為 0,就直接在 if 條件中使用了它旬昭,這樣 C 會把所有的非零值都視為 true篙螟,這里返回的 -99 就會被判斷為 true。對于初學(xué)者而言问拘,這是在 C 中會遇到的典型的錯誤遍略,我希望你不會再犯這樣的錯誤。

現(xiàn)在我們知道了錯誤的原因骤坐,接下來去修復(fù) carrot 腳本中的錯誤就非常簡單了绪杏。但是正如你看到的,我們設(shè)法從 CKB 上獲取一筆錯誤交易在運行時的狀態(tài)纽绍,然后通過 GDB(一個業(yè)界常見的工具)來對其進行調(diào)試蕾久。而且您在 GDB 上現(xiàn)有的工作流程和工具也可以在這里使用,是不是很棒拌夏?

基于 REPL 的開發(fā)/調(diào)試

然而僧著,GDB 僅僅是現(xiàn)代軟件開發(fā)中的一部分。動態(tài)語言在很大程度上占據(jù)了主導(dǎo)地位辖佣,很多程序員都使用基于 REPL 的開發(fā)/調(diào)試工作流霹抛。這與編譯語言中的 GDB 完全不同搓逾,基本上你需要的是一個運行的環(huán)境卷谈,你可以輸入任何你想要與環(huán)境進行交互的代碼,然后得到不同的結(jié)果霞篡。正如我們將在這里展示的世蔗,CKB 也會支持這種類型的開發(fā)/調(diào)試工作流。

在這里朗兵,我們將使用 ckb-duktape 來展示基于 JavaScript 的 REPL污淋。但是請注意,這只是一個 demo 用來演示一下工作流程余掖,沒有任何東西阻止您將自己喜愛的動態(tài)語言(不管是 Ruby寸爆、Rython、Lisp 等等)移植到 CKB 中去盐欺,并為該語言啟動 REPL赁豆。

首先,讓我們嘗試編譯 duktape:

$ git clone https://github.com/nervosnetwork/ckb-duktape
$ cd ckb-duktape
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
root@982d1e906b76:/# cd /code
root@982d1e906b76:/code# make
riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror c/entry.c -c -o build/entry.o
riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror duktape/duktape.c -c -o build/duktape.o
riscv64-unknown-elf-gcc build/entry.o build/duktape.o -o build/duktape -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s
riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror c/repl.c -c -o build/repl.o
riscv64-unknown-elf-gcc build/repl.o build/duktape.o -o build/repl -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s
root@982d1e906b76:/code# exit

你需要在這里生成 build/repl 二進制文件冗美。和 carrot 的例子類似魔种,我們先將 duktape REPL 的二進制文件部署在 CKB 上:

pry(main)> api = CKB::API.new
pry(main)> wallet = CKB::Wallet.from_hex(api, "<your private key>")
pry(main)> wallet2 = CKB::Wallet.from_hex(api, CKB::Key.random_private_key)
pry(main)> duktape_repl_data = File.read("build/repl")
pry(main)> duktape_repl_data.bytesize
=> 283048
pry(main)> duktape_repl_tx_hash = wallet.send_capacity(wallet2.address, CKB::Utils.byte_to_shannon(300000), CKB::Utils.bin_to_hex(duktape_repl_data), fee: 310000)
pry(main)> duktape_repl_data_hash = CKB::Blake2b.hexdigest(duktape_repl_data)
pry(main)> duktape_repl_type_script = CKB::Types::Script.new(code_hash: duktape_repl_data_hash, args: "0x")
pry(main)> duktape_repl_cell_dep = CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: duktape_repl_tx_hash, index: 0))

我們還需要創(chuàng)建一筆包含 duktape 腳本的交易,我這里使用一個非常簡單的腳本粉洼,當(dāng)然你可以加入更多的數(shù)據(jù)节预,這樣你就可以在 CKB 上玩起來了!

pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(100), use_dep_group: false, fee: 5000)
pry(main)> tx.outputs[0].type = duktape_repl_type_script
pry(main)> tx.cell_deps << duktape_repl_cell_dep
pry(main)> tx.witnesses[0] = "0x"

然后讓我們把它轉(zhuǎn)存到文件中叶摄,并檢查 duktape 類型腳本的哈希:

pry(main)> CKB::MockTransactionDumper.new(api, tx).write("duktape.json")
=> 2765824
pry(main)> duktape_repl_type_script.compute_hash
=> "0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837"

與上面不同的是,我們不需要啟動 GDB安拟,而是可以直接啟動程序:

$ ./target/release/ckb-debugger -g type -h 0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837 -t duktape.json
duk>

你可以看到一個 duk> 提示你輸入 JS 代碼蛤吓!同樣,如果遇到錯誤去扣,請檢查是否需要更改類型腳本的哈希柱衔,或者使用正確的 duktape.json 路徑。我們看到常見的 JS 代碼可以在這里工作運行:

duk> print(1 + 2)
3
= undefined
duk> function foo(a) { return a + 1; }
= undefined
duk> foo(123)
= 124

您還可以使用與 CKB 相關(guān)的功能:

duk> var hash = CKB.load_script_hash()
= undefined
duk> function buf2hex(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), function(x) { return ('00' + x.toString(16)).slice(-2); }).join(''); }
= undefined
duk> buf2hex(hash)
= a8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837

請注意愉棱,我們在這里得到的腳本哈希正是我們當(dāng)前執(zhí)行的類型腳本的哈希唆铐!這將證明 CKB 系統(tǒng)調(diào)試在這里是有效的,我們也可以嘗試更多有趣的東西:

duk> print(CKB.SOURCE.OUTPUT)
2
= undefined
duk> print(CKB.CELL.CAPACITY)
0
= undefined
duk> capacity_field = CKB.load_cell_by_field(0, 0, CKB.SOURCE.OUTPUT, CKB.CELL.CAPACITY)
= [object ArrayBuffer]
duk> buf2hex(capacity_field)
= 00e40b5402000000

這個 00e40b5402000000 可能在一開始看起來有點神秘奔滑,但是請注意 RISC-V 使用的是 little endian(低字節(jié)序)艾岂,所以如果在這里我們將字節(jié)序列顛倒,我們將得到 00000002540be400朋其,在十進制中正好是 10000000000王浴。還要記住,在 CKB 中容量使用的單位是 shannons梅猿,所以 10000000000 正好是 100 個字節(jié)氓辣,這正是我們生成上面的交易時,想要發(fā)送的代幣的數(shù)量袱蚓!現(xiàn)在你看到了如何在 duktape 環(huán)境中與 CKB 愉快地玩耍了 钞啸。

結(jié)論

我們已經(jīng)介紹了兩種不同的在 CKB 中調(diào)試的過程,你可以隨意使用其中一種(或者兩種)喇潘。我已經(jīng)迫不及待地想看你們在 CKB 上玩出花來啦体斩!

加入 Nervos Community

Nervos Community 致力于成為最好的 Nervos 社區(qū),我們將持續(xù)地推廣和普 及 Nervos 技術(shù)颖低,深入挖掘 Nervos 的內(nèi)在價值絮吵,開拓 Nervos 的無限可能, 為每一位想要深入了解 Nervos Network 的人提供一個優(yōu)質(zhì)的平臺忱屑。

添加微信號:BitcoinDog 即可加入 Nervos Community蹬敲,如果是程序員請備注,還會將您拉入開發(fā)者群莺戒。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伴嗡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子脏毯,更是在濱河造成了極大的恐慌闹究,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件食店,死亡現(xiàn)場離奇詭異渣淤,居然都是意外死亡赏寇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門价认,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗅定,“玉大人,你說我怎么就攤上這事用踩∏耍” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵脐彩,是天一觀的道長碎乃。 經(jīng)常有香客問我,道長惠奸,這世上最難降的妖魔是什么梅誓? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮佛南,結(jié)果婚禮上梗掰,老公的妹妹穿的比我還像新娘。我一直安慰自己嗅回,他們只是感情好及穗,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绵载,像睡著了一般埂陆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上尘分,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天猜惋,我揣著相機與錄音丸氛,去河邊找鬼培愁。 笑死,一個胖子當(dāng)著我的面吹牛缓窜,可吹牛的內(nèi)容都是我干的定续。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼禾锤,長吁一口氣:“原來是場噩夢啊……” “哼私股!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起恩掷,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤倡鲸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后黄娘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體峭状,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡克滴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了优床。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片劝赔。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖胆敞,靈堂內(nèi)的尸體忽然破棺而出着帽,到底是詐尸還是另有隱情,我是刑警寧澤移层,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布仍翰,位于F島的核電站,受9級特大地震影響观话,放射性物質(zhì)發(fā)生泄漏歉备。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一匪燕、第九天 我趴在偏房一處隱蔽的房頂上張望蕾羊。 院中可真熱鬧,春花似錦帽驯、人聲如沸龟再。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽利凑。三九已至,卻和暖如春嫌术,著一層夾襖步出監(jiān)牢的瞬間哀澈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工度气, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留割按,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓磷籍,卻偏偏與公主長得像适荣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子院领,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

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

  • 一直記不起來弛矛,是什么時候來過這里。直到你突然說比然,kia丈氓,我們?nèi)iao。我才想起,那是十多年前的事情了万俗。 那是大學(xué)...
    特別嘉賓路易斯閱讀 219評論 0 1
  • 黑夜鱼鼓,窗外透過微光 斑斑點點灑落進屋 睜大眼睛,看不清 世界的紛紛擾擾 閉上眼深呼吸 夢里一聲長嘆 醒來眼角憑添折...
    八月秋子閱讀 284評論 1 6
  • (一)面試方式概述 考生被分成一些人數(shù)相等的小組该编。一般每組 4 人迄本,然后每次有兩組考生參加面試。 首先课竣,考官會向兩...
    清大紫育閱讀 562評論 0 0
  • “哪里如你想的這般簡單嘉赎?”她煩惱的抓抓頭皮,頭上束發(fā)的金絲如意冠晃動著:“劉公公現(xiàn)在能放任分兒出現(xiàn)于樟,不過是因為...
    妖妖不惑閱讀 370評論 0 8
  • 前言 在面試環(huán)節(jié)中迂曲,考察"ThreadLocal"也是面試官的家常便飯靶橱,所以對它理解透徹,是非常有必要的. 有些面...
    美團Java閱讀 44,013評論 34 226