You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

9 lines
327 KiB

<!doctypehtml><html lang=zh-CN><meta charset=UTF-8><meta content=width=device-width name=viewport><meta content=#222 name=theme-color><meta content="Hexo 7.1.0" name=generator><link href=https://fonts.googleapis.com rel=preconnect><link href=https://cdnjs.cloudflare.com rel=preconnect><link href=/images/apple-touch-icon-next.png rel=apple-touch-icon sizes=180x180><link href=/images/favicon-32x32-next.png rel=icon sizes=32x32 type=image/png><link href=/images/favicon-16x16-next.png rel=icon sizes=16x16 type=image/png><link color=#222 href=/images/logo.svg rel=mask-icon><meta content=2X6S9P7CAjXjVvw8YyQR8pCu-B0oEu7O9quLgxXuWyA name=google-site-verification><meta content=dV8JGNzi0c name=baidu-site-verification><script>setTimeout(function(){var d=document.createElement("script");d.setAttribute("data-ad-client","ca-pub-7480618470784058"),d.async=!0,d.src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js",document.body.appendChild(d)},5e3),window.addEventListener?window.addEventListener("load",downloadJsAtOnload,!1):window.attachEvent?window.attachEvent("onload",downloadJsAtOnload):window.onload=downloadJsAtOnload</script><style>:root{--body-bg-color:#f5f7f9;--content-bg-color:#fff;--card-bg-color:#f5f5f5;--text-color:#555;--blockquote-color:#666;--link-color:#555;--link-hover-color:#222;--brand-color:#fff;--brand-hover-color:#fff;--table-row-odd-bg-color:#f9f9f9;--table-row-hover-bg-color:#f5f5f5;--menu-item-bg-color:#f5f5f5;--theme-color:#222;--btn-default-bg:#fff;--btn-default-color:#555;--btn-default-border-color:#555;--btn-default-hover-bg:#222;--btn-default-hover-color:#fff;--btn-default-hover-border-color:#222;--highlight-background:#f3f3f3;--highlight-foreground:#444;--highlight-gutter-background:#e1e1e1;--highlight-gutter-foreground:#555;color-scheme:light}html{line-height:1.15;-webkit-text-size-adjust:100%}details,main{display:block}pre{font-size:1em;overflow:auto;padding:10px}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}.table-container,textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}summary{display:list-item}[hidden],template{display:none}::selection{background:#262a30;color:#eee}body,html{height:100%}body{margin:0;background:var(--body-bg-color);box-sizing:border-box;color:var(--text-color);font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.2em;line-height:2;min-height:100%;position:relative;transition:padding .2s ease-in-out}h1,h2,h3,h4,h5,h6{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-weight:700;line-height:1.5;margin:30px 0 15px}h1{font-size:1.5em}h2{font-size:1.375em}h3{font-size:1.25em}h4{font-size:1.125em}h5{font-size:1em}h6{font-size:.875em}p{margin:0 0 20px}a{background:0 0;border-bottom:1px solid #999;color:var(--link-color);cursor:pointer;outline:0;text-decoration:none;overflow-wrap:break-word}a:hover{border-bottom-color:var(--link-hover-color);color:var(--link-hover-color)}embed,iframe,img,video{display:block;margin-left:auto;margin-right:auto;max-width:100%}hr{box-sizing:content-box;overflow:visible;background-image:repeating-linear-gradient(-45deg,#ddd,#ddd 4px,transparent 4px,transparent 8px);border:0;height:3px;margin:40px 0}blockquote{border-left:4px solid #ddd;color:var(--blockquote-color);margin:0;padding:0 15px}blockquote cite::before{content:'-';padding:0 5px}dt{font-weight:700}dd{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0;font-size:.875em;margin:0 0 20px;width:100%}tbody tr:nth-of-type(odd){background:var(--table-row-odd-bg-color)}tbody tr:hover{background:var(--table-row-hover-bg-color)}caption,td,th{padding:8px}td,th{border:1px solid #ddd;border-bottom:3px solid #ddd}th{font-weight:700;padding-bottom:10px}td{border-bottom-width:1px}.btn{background:var(--btn-default-bg);border:2px solid var(--btn-default-border-color);border-radius:2px;color:var(--btn-default-color);display:inline-block;font-size:.875em;line-height:2;padding:0 20px;transition:background-color .2s ease-in-out}.btn:hover{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.btn+.btn{margin:0 0 8px 8px}.btn .fa-fw{text-align:left;width:1.285714285714286em}.toggle{line-height:0}.toggle .toggle-line{background:#fff;display:block;height:2px;left:0;position:relative;top:0;transition:.4s;width:100%}.toggle .toggle-line:first-child{margin-top:1px}.toggle .toggle-line:not(:first-child){margin-top:4px}.toggle.toggle-arrow :first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}.toggle.toggle-arrow :last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}.toggle.toggle-close :nth-child(2){opacity:0}.toggle.toggle-close :first-child{top:6px;transform:rotate(45deg)}.toggle.toggle-close :last-child{top:-6px;transform:rotate(-45deg)}/*!
Theme: Default
Description: Original highlight.js style
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
Maintainer: @highlightjs/core-team
Website: https://highlightjs.org/
License: see project LICENSE
Touched: 2021
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.code-container:hover .copy-btn,.highlight:hover .copy-btn{opacity:1}.code-container{position:relative}.copy-btn{color:#333;cursor:pointer;line-height:1.6;opacity:0;padding:2px 6px;position:absolute;transition:opacity .2s ease-in-out;background-color:#eee;background-image:linear-gradient(#fcfcfc,#eee);border:1px solid #d5d5d5;border-radius:3px;font-size:.8125em;right:4px;top:8px}code,figure.highlight,kbd,pre{background:var(--highlight-background);color:var(--highlight-foreground)}figure.highlight,pre{line-height:1.6;margin:0 auto 20px}figure.highlight figcaption,pre .caption,pre figcaption{background:var(--highlight-gutter-background);color:var(--highlight-foreground);display:flow-root;font-size:.875em;line-height:1.2;padding:.5em}figure.highlight figcaption a,pre .caption a,pre figcaption a{color:var(--highlight-foreground);float:right}figure.highlight figcaption a:hover,pre .caption a:hover,pre figcaption a:hover{border-bottom-color:var(--highlight-foreground)}code,pre{font-family:consolas,Menlo,monospace,'PingFang SC','Microsoft YaHei'}code{border-radius:3px;font-size:.875em;padding:2px 4px;overflow-wrap:break-word}kbd{border:2px solid #ccc;border-radius:.2em;box-shadow:.1em .1em .2em rgba(0,0,0,.1);font-family:inherit;padding:.1em .3em;white-space:nowrap}figure.highlight{overflow:auto;position:relative}figure.highlight pre{border:0;margin:0;padding:10px 0}figure.highlight table{border:0;margin:0;width:auto}figure.highlight td{border:0;padding:0}figure.highlight .gutter{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}figure.highlight .gutter pre{background:var(--highlight-gutter-background);color:var(--highlight-gutter-foreground);padding-left:10px;padding-right:10px;text-align:right}figure.highlight .code pre{padding-left:10px;width:100%}figure.highlight .marked{background:rgba(0,0,0,.3)}pre .caption,pre figcaption{margin-bottom:10px}.gist table{width:auto}.gist table td{border:0}pre code{background:0 0;padding:0;text-shadow:none}.blockquote-center{border-left:0;margin:40px 0;padding:0;position:relative;text-align:center}.blockquote-center::after,.blockquote-center::before{left:0;line-height:1;opacity:.6;position:absolute;width:100%}.blockquote-center::before{border-top:1px solid #ccc;text-align:left;top:-20px;content:'\f10d';font-family:'Font Awesome 6 Free';font-weight:900}.blockquote-center::after{border-bottom:1px solid #ccc;bottom:-20px;text-align:right;content:'\f10e';font-family:'Font Awesome 6 Free';font-weight:900}.blockquote-center div,.blockquote-center p{text-align:center}.group-picture{margin-bottom:20px}.group-picture .group-picture-row{display:flex;gap:3px;margin-bottom:3px}.group-picture .group-picture-column{flex:1}.group-picture .group-picture-column img{height:100%;margin:0;object-fit:cover;width:100%}.post-body .label{color:#555;padding:0 2px}.post-body .label.default{background:#f0f0f0}.post-body .label.primary{background:#efe6f7}.post-body .label.info{background:#e5f2f8}.post-body .label.success{background:#e7f4e9}.post-body .label.warning{background:#fcf6e1}.post-body .label.danger{background:#fae8eb}.post-body .link-grid{display:grid;grid-gap:1.5rem;gap:1.5rem;grid-template-columns:1fr 1fr;margin-bottom:20px;padding:1rem}.post-body .link-grid .link-grid-container{border:solid #ddd;box-shadow:1rem 1rem .5rem rgba(0,0,0,.5);min-height:5rem;min-width:0;padding:.5rem;position:relative;transition:background .3s}.post-body .link-grid .link-grid-container:hover{animation:.5s next-shake;background:var(--card-bg-color)}.post-body .link-grid .link-grid-container:active{box-shadow:.5rem .5rem .25rem rgba(0,0,0,.5);transform:translate(.2rem,.2rem)}.post-body .link-grid .link-grid-container .link-grid-image{border:1px solid #ddd;border-radius:50%;box-sizing:border-box;height:5rem;padding:3px;position:absolute;width:5rem}.post-body .link-grid .link-grid-container p{margin:0 1rem 0 6rem}.post-body .link-grid .link-grid-container p:first-of-type{font-size:1.2em}.post-body .link-grid .link-grid-container p:last-of-type{font-size:.8em;line-height:1.3rem;opacity:.7}.post-body .link-grid .link-grid-container a{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}.post-body .note{border-radius:3px;margin-bottom:20px;padding:1em;position:relative;border:1px solid #eee;border-left-width:5px}.post-body .note summary{cursor:pointer;outline:0}.post-body .note summary p{display:inline}.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6{border-bottom:initial;margin:0;padding-top:0}.post-body .note :first-child{margin-top:0}.post-body .note :last-child{margin-bottom:0}.post-body .note.default{border-left-color:#777}.post-body .note.default h2,.post-body .note.default h3,.post-body .note.default h4,.post-body .note.default h5,.post-body .note.default h6{color:#777}.post-body .note.primary{border-left-color:#6f42c1}.post-body .note.primary h2,.post-body .note.primary h3,.post-body .note.primary h4,.post-body .note.primary h5,.post-body .note.primary h6{color:#6f42c1}.post-body .note.info{border-left-color:#428bca}.post-body .note.info h2,.post-body .note.info h3,.post-body .note.info h4,.post-body .note.info h5,.post-body .note.info h6{color:#428bca}.post-body .note.success{border-left-color:#5cb85c}.post-body .note.success h2,.post-body .note.success h3,.post-body .note.success h4,.post-body .note.success h5,.post-body .note.success h6{color:#5cb85c}.post-body .note.warning{border-left-color:#f0ad4e}.post-body .note.warning h2,.post-body .note.warning h3,.post-body .note.warning h4,.post-body .note.warning h5,.post-body .note.warning h6{color:#f0ad4e}.post-body .note.danger{border-left-color:#d9534f}.post-body .note.danger h2,.post-body .note.danger h3,.post-body .note.danger h4,.post-body .note.danger h5,.post-body .note.danger h6{color:#d9534f}.post-body .tabs{margin-bottom:20px}.post-body .tabs,.tabs-comment{padding-top:10px}.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{background:var(--content-bg-color);display:flex;display:flex;flex-wrap:wrap;justify-content:center;margin:0;padding:0;position:-webkit-sticky;position:sticky;top:0;z-index:5}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid #ddd;border-left:1px solid transparent;border-right:1px solid transparent;border-radius:0;border-top:3px solid transparent;flex-grow:1;list-style-type:none}@media (max-width:413px){.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:block;margin-bottom:5px}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid transparent;border-left:3px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-radius:0}}.post-body .tabs ul.nav-tabs li.tab a,.tabs-comment ul.nav-tabs li.tab a{border-bottom:initial;display:block;line-height:1.8;padding:.25em .75em;text-align:center;transition:.2s ease-out}.post-body .tabs ul.nav-tabs li.tab a i[class^=fa],.tabs-comment ul.nav-tabs li.tab a i[class^=fa]{width:1.285714285714286em}.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-color:#fc6423 #ddd transparent}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-color:#ddd #ddd #ddd #fc6423}}.post-body .tabs ul.nav-tabs li.tab.active a,.tabs-comment ul.nav-tabs li.tab.active a{cursor:default}.post-body .tabs .tab-content,.tabs-comment .tab-content{border:1px solid #ddd;border-radius:0;border-top-color:transparent}@media (max-width:413px){.post-body .tabs .tab-content,.tabs-comment .tab-content{border-radius:0;border-top-color:#ddd}}.post-body .tabs .tab-content .tab-pane,.tabs-comment .tab-content .tab-pane{padding:20px 20px 0}.post-body .tabs .tab-content .tab-pane:not(.active),.tabs-comment .tab-content .tab-pane:not(.active){display:none}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{display:inline-block;margin:-1px 10px 0;padding:0 10px}.algolia-pagination .current .page-number,.pagination .page-number.current{background:#ccc;border-color:#ccc;color:var(--content-bg-color)}.pagination{border-top:1px solid #eee;margin:120px 0 0;text-align:center}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:0;border-top:1px solid #eee;transition:border-color .2s ease-in-out}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-top-color:var(--link-hover-color)}@media (max-width:767px){.post-body .link-grid{grid-template-columns:1fr}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{margin:0 5px}.pagination{border-top:0}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:1px solid #eee;border-top:0}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-bottom-color:var(--link-hover-color)}.site-meta{text-align:center}}.pagination .space{margin:0;padding:0}.comments{margin-top:60px;overflow:hidden}.comment-button-group{display:flex;display:flex;flex-wrap:wrap;justify-content:center;justify-content:center;margin:1em 0}.comment-button-group .comment-button{margin:.1em .2em}.comment-button-group .comment-button.active{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.comment-position{display:none}.comment-position.active{display:block}.tabs-comment{margin-top:4em;padding-top:0}.tabs-comment .comments{margin-top:0;padding-top:0}.headband{background:var(--theme-color);height:3px}@media (max-width:991px){.headband{display:none}}.site-brand-container{display:flex;flex-shrink:0;padding:0 10px}.use-motion .column,.use-motion .site-brand-container .toggle{opacity:0}.site-meta{flex-grow:1;text-align:center}.custom-logo-image{margin-top:20px}@media (max-width:991px){.custom-logo-image{display:none}}.brand{border-bottom:0;color:var(--brand-color);display:inline-block;padding:0}.brand:hover{color:var(--brand-hover-color)}.site-title{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.375em;font-weight:400;line-height:1.5;margin:0}.site-subtitle{color:#ddd;font-size:.8125em;margin:10px 10px 0}.use-motion .custom-logo-image,.use-motion .site-subtitle,.use-motion .site-title{opacity:0;position:relative;top:-10px}.site-nav-right,.site-nav-toggle{display:none}.site-nav-right .toggle,.site-nav-toggle .toggle{color:var(--text-color);padding:10px;width:22px}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:var(--text-color);border-radius:1px}@media (max-width:767px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height);visibility:unset}}.menu{margin:0;padding:1em 0;text-align:center}.menu-item{display:inline-block;list-style:none;margin:0 10px}@media (max-width:767px){.menu-item{display:block;margin-top:10px}.menu-item.menu-item-search{display:none}}.menu-item a{border-bottom:0;display:block;font-size:.8125em;transition:border-color .2s ease-in-out}.menu-item a.menu-item-active,.menu-item a:hover{background:var(--menu-item-bg-color)}.menu-item i[class^=fa]{margin-right:8px}.menu-item .badge{display:inline-block;font-weight:700;line-height:1;margin-left:.35em;margin-top:.35em;text-align:center;white-space:nowrap}.use-motion .menu-item{visibility:hidden}.github-corner :hover .octo-arm{animation:560ms ease-in-out octocat-wave}.github-corner svg{color:#fff;fill:var(--theme-color);position:absolute;right:0;top:0;z-index:5}@media (max-width:991px){.github-corner{display:none}.github-corner svg{color:var(--theme-color);fill:#fff}.github-corner .github-corner:hover .octo-arm{animation:none}.github-corner .github-corner .octo-arm{animation:560ms ease-in-out octocat-wave}}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}.sidebar-inner{color:#999;padding:18px 10px;text-align:center;display:flex;flex-direction:column;justify-content:center}.cc-license .cc-opacity{border-bottom:0;opacity:.7}.cc-license .cc-opacity:hover{opacity:.9}.cc-license img{display:inline-block}.site-author-image{border:1px solid #eee;max-width:120px;padding:2px}.site-author-name{color:var(--text-color);font-weight:600;margin:0}.site-description{color:#999;font-size:.8125em;margin-top:0}.links-of-author a{font-size:.8125em}.links-of-author i[class^=fa]{margin-right:2px}.sidebar .sidebar-button:not(:first-child){margin-top:15px}.sidebar .sidebar-button button{background:0 0;cursor:pointer;line-height:2;padding:0 15px;border-radius:4px}.sidebar .sidebar-button button i[class^=fa]{margin-right:5px}.links-of-blogroll{font-size:.8125em}.links-of-blogroll-title{font-size:.875em;font-weight:600}.links-of-blogroll-list{list-style:none;margin:0;padding:0}.sidebar-nav{font-size:.875em;height:0;margin:0;overflow:hidden;padding-left:0;pointer-events:none;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}.sidebar-nav-active .sidebar-nav{height:calc(2em + 1px);pointer-events:unset;visibility:unset}.sidebar-nav li{border-bottom:1px solid transparent;color:var(--text-color);cursor:pointer;display:inline-block;transition:border-bottom-color .2s ease-in-out,color .2s ease-in-out}.sidebar-nav li.sidebar-nav-overview{margin-left:10px}.sidebar-nav li:hover{color:#fc6423}.sidebar-overview-active .sidebar-nav-overview,.sidebar-toc-active .sidebar-nav-toc{border-bottom-color:#fc6423;color:#fc6423;transition-delay:0.2s}.sidebar-overview-active .sidebar-nav-overview:hover,.sidebar-toc-active .sidebar-nav-toc:hover{color:#fc6423}.sidebar-panel-container{align-items:start;display:grid;flex:1;overflow-x:hidden;overflow-y:auto;padding-top:0;transition:padding-top .2s ease-in-out}.sidebar-nav-active .sidebar-panel-container{padding-top:20px}.sidebar-panel{animation:.2s ease-in-out deactivate-sidebar-panel;grid-area:1/1;height:0;opacity:0;overflow:hidden;pointer-events:none;transform:translateY(0);transition:.2s ease-in-out;transition-property:opacity,transform,visibility;visibility:hidden}.sidebar-nav-active .sidebar-panel,.sidebar-overview-active .sidebar-panel.post-toc-wrap{transform:translateY(-20px)}.sidebar-overview-active:not(.sidebar-nav-active) .sidebar-panel.post-toc-wrap{transition-delay:0s,0.2s,0s}.sidebar-overview-active .sidebar-panel.site-overview-wrap,.sidebar-toc-active .sidebar-panel.post-toc-wrap{animation-name:activate-sidebar-panel;height:auto;opacity:1;pointer-events:unset;transform:translateY(0);transition-delay:0.2s,0.2s,0s;visibility:unset}.sidebar-panel.site-overview-wrap{display:flex;flex-direction:column;justify-content:center;gap:10px;justify-content:flex-start}@keyframes deactivate-sidebar-panel{from{height:var(--inactive-panel-height,0)}to{height:var(--active-panel-height,0)}}@keyframes activate-sidebar-panel{from{height:var(--inactive-panel-height,auto)}to{height:var(--active-panel-height,auto)}}.sidebar-toggle{bottom:61px;height:16px;padding:5px;width:16px;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px}.sidebar-toggle:hover{opacity:.8}@media (max-width:991px){.sidebar-toggle{right:20px;opacity:.8}}.sidebar-toggle:hover .toggle-line{background:#fc6423}@media (any-hover:hover){body:not(.sidebar-active) .sidebar-toggle:hover :first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}body:not(.sidebar-active) .sidebar-toggle:hover :last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}}.sidebar-active .sidebar-toggle :nth-child(2){opacity:0}.sidebar-active .sidebar-toggle :first-child{top:6px;transform:rotate(45deg)}.sidebar-active .sidebar-toggle :last-child{top:-6px;transform:rotate(-45deg)}.post-toc{font-size:.875em}.post-toc ol{list-style:none;margin:0;padding:0 2px 0 10px;text-align:left}.post-toc ol>:last-child{margin-bottom:5px}.post-toc ol>ol{padding-left:0}.post-toc ol a{transition:.2s ease-in-out}.post-toc .nav-item{line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.post-toc .nav .nav-child{--height:0;height:0;opacity:0;overflow:hidden;transition:.2s ease-in-out;visibility:hidden}.post-toc .nav .active>.nav-child{height:var(--height,auto);opacity:1;visibility:unset}.post-toc .nav .active>a{border-bottom-color:#fc6423;color:#fc6423}.post-toc .nav .active-current>a,.post-toc .nav .active-current>a:hover{color:#fc6423}.site-state{display:flex;flex-wrap:wrap;justify-content:center;line-height:1.4}.site-state-item a{border-bottom:0;display:block}.site-state-item-count{display:block;font-size:1em;font-weight:600}.site-state-item-name{color:#999;font-size:.8125em}.sidebar-post-related{font-size:.8125em;padding:18px 0 0}.popular-posts{margin:0;padding:1em 0;text-align:left}.popular-posts .popular-posts-item{display:block}.popular-posts .popular-posts-item .popular-posts-link{border-bottom:0;display:block;padding:5px 20px;transition:background .2s ease-in-out}.popular-posts .popular-posts-item .popular-posts-link:hover{background:var(--menu-item-bg-color)}.popular-posts .popular-posts-item .popular-posts-time{color:#999}.footer{color:#999;font-size:.875em;padding:20px 0;transition:left .2s ease-in-out,right .2s ease-in-out}.footer.footer-fixed{bottom:0;left:0;position:absolute;right:0}.footer-inner{box-sizing:border-box;text-align:center;display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.menu-item .badge{float:right;margin-left:0}.footer-inner{width:auto}}@media (min-width:1200px){.footer-inner{width:1160px}}@media (min-width:1600px){.footer-inner{width:73%}}.use-motion .footer{opacity:0}.languages{display:inline-block;font-size:1.125em;position:relative}.languages .lang-select-label span{margin:0 .5em}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:red;display:inline-block;margin:0 5px}.beian img{display:inline-block;margin:0 3px;vertical-align:middle}.busuanzi-count #busuanzi_container_site_pv,.busuanzi-count #busuanzi_container_site_uv{display:none}@keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,50%,60%,70%,80%{transform:scale(1.1)}}@media (max-width:567px){.main-inner{padding:initial!important}.posts-expand .post-header{margin-bottom:10px!important}.post-block{margin-top:initial!important;padding:8px 18px!important}.post-body h1,.post-body h2,.post-body h3,.post-body h4,.post-body h5,.post-body h6{margin:20px 0 8px}.post-body .note h1,.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6,.post-body .tabs .tab-content .tab-pane h1,.post-body .tabs .tab-content .tab-pane h2,.post-body .tabs .tab-content .tab-pane h3,.post-body .tabs .tab-content .tab-pane h4,.post-body .tabs .tab-content .tab-pane h5,.post-body .tabs .tab-content .tab-pane h6{margin:0 5px}.post-body>p{margin:0 0 10px}.post-body .note>p,.post-body .tabs .tab-content .tab-pane>p{padding:0 5px}.post-body img,.post-body video{margin-bottom:10px!important}.post-body .fancybox+figcaption,.post-body img+figcaption{margin:-5px auto 15px!important}.post-body .note{margin-bottom:10px!important;padding:10px!important}.post-body .tabs .tab-content .tab-pane{padding:10px 10px 0!important}.post-eof{margin:40px auto 20px!important}.pagination{margin-top:40px}}.back-to-top{font-size:12px;align-items:center;bottom:-100px;color:#fff;display:flex;height:26px;transition:bottom .2s ease-in-out;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px}.back-to-top span{margin-right:8px;display:none}.back-to-top .fa{text-align:center;width:26px}.back-to-top:hover{opacity:.8;color:#fc6423}.back-to-top.back-to-top-on{bottom:30px}.rtl.post-body a,.rtl.post-body h1,.rtl.post-body h2,.rtl.post-body h3,.rtl.post-body h4,.rtl.post-body h5,.rtl.post-body h6,.rtl.post-body li,.rtl.post-body ol,.rtl.post-body p,.rtl.post-body ul{direction:rtl;font-family:UKIJ Ekran}.rtl.post-title{font-family:UKIJ Ekran}.post-button{margin-top:40px;text-align:center}.use-motion .collection-header,.use-motion .comments,.use-motion .pagination,.use-motion .post-block,.use-motion .post-body,.use-motion .post-header{visibility:hidden}.posts-collapse .post-content{margin-bottom:35px;margin-left:35px;position:relative}@media (max-width:767px){.posts-collapse .post-content{margin-left:0;margin-right:0}}.posts-collapse .post-content .collection-title{font-size:1.125em;position:relative}.posts-collapse .post-content .collection-title::before{background:#999;border:1px solid #fff;margin-left:-6px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:10px;width:10px}.posts-collapse .post-content .collection-year{font-size:1.5em;font-weight:700;margin:60px 0;position:relative}.posts-collapse .post-content .collection-year::before{background:#bbb;margin-left:-4px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:8px;width:8px}.posts-collapse .post-content .collection-header{display:block;margin-left:20px}.posts-collapse .post-content .collection-header small{color:#bbb;margin-left:5px}.posts-collapse .post-content .post-header{border-bottom:1px dashed #ccc;margin:30px 2px 0;padding-left:15px;position:relative;transition:border .2s ease-in-out}.posts-collapse .post-content .post-header::before{background:#bbb;border:1px solid #fff;left:-6px;position:absolute;top:.75em;transition:background .2s ease-in-out;border-radius:50%;content:' ';height:6px;width:6px}.posts-collapse .post-content .post-header:hover{border-bottom-color:#666}.posts-collapse .post-content .post-header:hover::before{background:#222}.posts-collapse .post-content .post-meta-container{display:inline;font-size:.75em;margin-right:10px}.posts-collapse .post-content .post-title{display:inline}.posts-collapse .post-content .post-title a{border-bottom:0;color:var(--link-color)}.posts-collapse .post-content::before{background:#f5f5f5;content:' ';height:100%;margin-left:-2px;position:absolute;top:1.25em;width:4px}.post-body{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;overflow-wrap:break-word}@media (min-width:1200px){.post-body{font-size:1.125em}}@media (min-width:992px){.post-body{text-align:justify}}.post-body h1 .header-anchor,.post-body h1 .headerlink,.post-body h2 .header-anchor,.post-body h2 .headerlink,.post-body h3 .header-anchor,.post-body h3 .headerlink,.post-body h4 .header-anchor,.post-body h4 .headerlink,.post-body h5 .header-anchor,.post-body h5 .headerlink,.post-body h6 .header-anchor,.post-body h6 .headerlink{border-bottom-style:none;color:inherit;float:right;font-size:.875em;margin-left:10px;opacity:0}.post-body h1 .header-anchor::before,.post-body h1 .headerlink::before,.post-body h2 .header-anchor::before,.post-body h2 .headerlink::before,.post-body h3 .header-anchor::before,.post-body h3 .headerlink::before,.post-body h4 .header-anchor::before,.post-body h4 .headerlink::before,.post-body h5 .header-anchor::before,.post-body h5 .headerlink::before,.post-body h6 .header-anchor::before,.post-body h6 .headerlink::before{content:'\f0c1';font-family:'Font Awesome 6 Free';font-weight:900}.post-body h1:hover .header-anchor,.post-body h1:hover .headerlink,.post-body h2:hover .header-anchor,.post-body h2:hover .headerlink,.post-body h3:hover .header-anchor,.post-body h3:hover .headerlink,.post-body h4:hover .header-anchor,.post-body h4:hover .headerlink,.post-body h5:hover .header-anchor,.post-body h5:hover .headerlink,.post-body h6:hover .header-anchor,.post-body h6:hover .headerlink{opacity:.5}.post-body h1:hover .header-anchor:hover,.post-body h1:hover .headerlink:hover,.post-body h2:hover .header-anchor:hover,.post-body h2:hover .headerlink:hover,.post-body h3:hover .header-anchor:hover,.post-body h3:hover .headerlink:hover,.post-body h4:hover .header-anchor:hover,.post-body h4:hover .headerlink:hover,.post-body h5:hover .header-anchor:hover,.post-body h5:hover .headerlink:hover,.post-body h6:hover .header-anchor:hover,.post-body h6:hover .headerlink:hover{opacity:1}.post-body .exturl .fa{font-size:.875em;margin-left:4px}.post-body .fancybox+figcaption,.post-body img+figcaption{color:#999;font-size:.875em;font-weight:700;line-height:1;margin:-15px auto 15px;text-align:center}.post-body embed,.post-body iframe,.post-body img,.post-body video{margin-bottom:20px}.post-body .video-container{height:0;margin-bottom:20px;overflow:hidden;padding-top:75%;position:relative;width:100%}.post-body .video-container embed,.post-body .video-container iframe,.post-body .video-container object{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.post-gallery{display:flex;min-height:200px}.post-gallery .post-gallery-image{flex:1}.post-gallery .post-gallery-image:not(:first-child){clip-path:polygon(40px 0,100% 0,100% 100%,0 100%);margin-left:-20px}.post-gallery .post-gallery-image:not(:last-child){margin-right:-20px}.post-gallery .post-gallery-image img{height:100%;object-fit:cover;opacity:1;width:100%}.posts-expand .post-gallery{margin-bottom:60px}.posts-collapse .post-gallery{margin:15px 0}.posts-expand .post-header{font-size:1.125em;margin-bottom:60px;text-align:center}.posts-expand .post-title{font-size:1.5em;font-weight:400;margin:initial;overflow-wrap:break-word}.posts-expand .post-title-link{border-bottom:0;color:var(--link-color);display:inline-block;position:relative}.posts-expand .post-title-link::before{background:var(--link-color);bottom:0;content:'';height:2px;left:0;position:absolute;transform:scaleX(0);transition:transform .2s ease-in-out;width:100%}.posts-expand .post-title-link:hover::before{transform:scaleX(1)}.posts-expand .post-title-link .fa{font-size:.875em;margin-left:5px}.post-sticky-flag{display:inline-block;margin-right:8px;transform:rotate(30deg)}.posts-expand .post-meta-container{color:#999;font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:.75em;margin-top:3px}.posts-expand .post-meta-container .post-description{font-size:.875em;margin-top:2px}.posts-expand .post-meta-container time{border-bottom:1px dashed #999}.post-meta{display:flex;flex-wrap:wrap;justify-content:center}:not(.post-meta-break)+.post-meta-item::before{content:'|';margin:0 .5em}.post-meta-item-icon{margin-right:3px}@media (max-width:991px){.back-to-top{right:20px;opacity:.8}.post-body{text-align:justify}.post-meta-item-text{display:none}}.post-meta-break{flex-basis:100%;height:0}#busuanzi_container_page_pv{display:none}.post-nav{border-top:1px solid #eee;display:flex;gap:30px;justify-content:space-between;margin-top:1em;padding:10px 5px 0}.post-nav-item{flex:1}.post-nav-item a{border-bottom:0;display:block;font-size:.875em;line-height:1.6}.post-nav-item a:active{top:2px}.post-nav-item .fa{font-size:.75em}.post-nav-item:first-child .fa{margin-right:5px}.post-nav-item:last-child{text-align:right}.post-nav-item:last-child .fa{margin-left:5px}.post-footer{display:flex;flex-direction:column;justify-content:center}.post-eof{background:#ccc;height:1px;margin:80px auto 60px;width:8%}.post-block:last-of-type .post-eof{display:none}.post-copyright ul{list-style:none;overflow:hidden;padding:.5em 1em;position:relative;background:var(--card-bg-color);border-left:3px solid #ff2a2a;margin:1em 0 0}.post-copyright ul::after{content:'\f25e';font-family:'Font Awesome 6 Brands';font-size:200px;opacity:.1;position:absolute;right:-50px;top:-150px}.post-tags{margin-top:40px;text-align:center}.post-tags a{display:inline-block;font-size:.8125em}.post-tags a:not(:last-child){margin-right:10px}.social-like{border-top:1px solid #eee;font-size:.875em;margin-top:1em;padding-top:1em;display:flex;flex-wrap:wrap;justify-content:center}.social-like a{border-bottom:none}.reward-container{margin:1em 0 0;padding:1em 0;text-align:center}.reward-container button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:2px solid #fc6423;border-radius:2px;outline:0;transition:.2s ease-in-out;vertical-align:text-top}.reward-container button:hover{background:#fc6423;color:#fff}.post-reward{display:none;padding-top:20px}.post-reward.active{display:block}.post-reward div{display:inline-block}.post-reward div span{display:block}.post-reward img{display:inline-block;margin:.8em 2em 0;max-width:100%;width:180px}@keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}.category-all-page .category-all-title{text-align:center}.category-all-page .category-all{margin-top:20px}.category-all-page .category-list{list-style:none;margin:0;padding:0}.category-all-page .category-list-item{margin:5px 10px}.category-all-page .category-list-count{color:#bbb}.category-all-page .category-list-count::before{content:' ('}.category-all-page .category-list-count::after{content:') '}.category-all-page .category-list-child{padding-left:10px}.event-list hr{background:#222;margin:20px 0 45px}.event-list hr::after{background:#222;color:#fff;content:'NOW';display:inline-block;font-weight:700;padding:0 5px}.event-list .event{--event-background:#222;--event-foreground:#bbb;--event-title:#fff;background:var(--event-background);padding:15px}.event-list .event .event-summary{border-bottom:0;color:var(--event-title);margin:0;padding:0 0 0 35px;position:relative}.event-list .event .event-summary::before{animation:1s ease-in-out infinite alternate dot-flash;background:var(--event-title);left:0;margin-top:-6px;position:absolute;top:50%;border-radius:50%;content:' ';height:12px;width:12px}.event-list .event:nth-of-type(odd) .event-summary::before{animation-delay:.5s}.event-list .event:not(:last-child){margin-bottom:20px}.event-list .event .event-relative-time{color:var(--event-foreground);display:inline-block;font-size:12px;font-weight:400;padding-left:12px}.event-list .event .event-details{color:var(--event-foreground);display:block;line-height:18px;padding:6px 0 6px 35px}.event-list .event .event-details::before{color:var(--event-foreground);display:inline-block;margin-right:9px;width:14px;font-family:'Font Awesome 6 Free';font-weight:900}.event-list .event .event-details.event-location::before{content:'\f041'}.event-list .event .event-details.event-duration::before{content:'\f017'}.event-list .event .event-details.event-description::before{content:'\f024'}.event-list .event-past{--event-background:#f5f5f5;--event-foreground:#999;--event-title:#222}@keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}ul.breadcrumb{font-size:.75em;list-style:none;margin:1em 0;padding:0 2em;text-align:center}ul.breadcrumb li{display:inline}ul.breadcrumb li:not(:first-child)::before{content:'/\00a0';font-weight:400;padding:.5em}ul.breadcrumb li:last-child{font-weight:700}.tag-cloud{text-align:center}.tag-cloud a{display:inline-block;margin:10px}.tag-cloud-0{border-bottom-color:#aaa;color:#aaa}.tag-cloud-1{border-bottom-color:#9a9a9a;color:#9a9a9a}.tag-cloud-2{border-bottom-color:#8b8b8b;color:#8b8b8b}.tag-cloud-3{border-bottom-color:#7c7c7c;color:#7c7c7c}.tag-cloud-4{border-bottom-color:#6c6c6c;color:#6c6c6c}.tag-cloud-5{border-bottom-color:#5d5d5d;color:#5d5d5d}.tag-cloud-6{border-bottom-color:#4e4e4e;color:#4e4e4e}.tag-cloud-7{border-bottom-color:#3e3e3e;color:#3e3e3e}.tag-cloud-8{border-bottom-color:#2f2f2f;color:#2f2f2f}.tag-cloud-9{border-bottom-color:#202020;color:#202020}.tag-cloud-10{border-bottom-color:#111;color:#111}.search-active{overflow:hidden}.search-pop-overlay{background:rgba(0,0,0,0);display:flex;height:100%;left:0;position:fixed;top:0;transition:visibility .4s,background .4s;visibility:hidden;width:100%;z-index:40}.search-active .search-pop-overlay{background:rgba(0,0,0,.3);visibility:visible}.search-popup{background:var(--card-bg-color);border-radius:5px;height:80%;margin:auto;transform:scale(0);transition:transform .4s;width:700px}.search-active .search-popup{transform:scale(1)}@media (max-width:767px){.search-popup{border-radius:0;height:100%;width:100%}}.search-popup .popup-btn-close,.search-popup .search-icon{color:#999;font-size:18px;padding:0 10px}.search-popup .popup-btn-close{cursor:pointer}.search-popup .popup-btn-close:hover .fa{color:#222}.search-popup .search-header{background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;padding:5px}.search-popup input.search-input{background:0 0;border:0;outline:0;width:100%}.search-popup input.search-input::-webkit-search-cancel-button{display:none}.search-popup .search-result-container{height:calc(100% - 55px);overflow:auto;padding:5px 25px}.search-popup .search-result-container hr{margin:5px 0 10px}.search-popup .search-result-container hr:first-child{display:none}.search-popup .search-result-list{margin:0 5px;padding:0}.search-popup a.search-result-title{font-weight:700}.search-popup p.search-result{border-bottom:1px dashed #ccc;padding:5px 0}.search-input-container{flex-grow:1}.search-input-container form{padding:2px}.search-stats{align-items:center;display:flex;justify-content:space-between}.search-stats img{height:1em;margin:0}.algolia-pagination{margin:40px 0;opacity:1;padding:0}.algolia-pagination .pagination-item{display:inline-block}.algolia-pagination .current .page-number{cursor:default}.algolia-pagination .disabled-item{visibility:hidden}.use-motion .animated{animation-fill-mode:none;visibility:inherit}.use-motion .sidebar .animated{animation-fill-mode:both}header.header{background:var(--content-bg-color);border-radius:initial;box-shadow:initial}.main{align-items:stretch;display:flex;justify-content:space-between;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.main{width:auto}}@media (min-width:1200px){.main{width:1160px}}@media (min-width:1600px){.main{width:73%}}@media (max-width:991px){header.header{border-radius:initial}.main{display:block;width:auto}}.main-inner{border-radius:initial;box-sizing:border-box;width:calc(100% - 252px)}.footer-inner{padding-left:252px}@media (max-width:991px){.main-inner{border-radius:initial;width:100%}.footer-inner{padding-left:0;padding-right:0;width:auto}}.column{width:240px}.site-brand-container{background:var(--theme-color)}.site-meta{padding:20px 0}.site-nav-right .toggle,.site-nav-toggle .toggle{color:#fff}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:#fff}@media (min-width:768px) and (max-width:991px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height);visibility:unset}}.menu .menu-item{display:block;margin:0}.menu .menu-item a{padding:5px 20px;position:relative;text-align:left;transition-property:background-color}.menu .menu-item .badge{background:#ccc;border-radius:10px;color:var(--content-bg-color);float:right;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.main-menu .menu-item-active::after{background:#bbb;border-radius:50%;content:' ';height:6px;margin-top:-3px;position:absolute;right:15px;top:50%;width:6px}.sub-menu{margin:0;padding:6px 0}.sub-menu .menu-item{display:inline-block}.sub-menu .menu-item a{background:0 0;margin:5px 10px;padding:initial}.sub-menu .menu-item a:hover{background:0 0;color:#fc6423}.sub-menu .menu-item-active{border-bottom-color:#fc6423;color:#fc6423}.sub-menu .menu-item-active:hover{border-bottom-color:#fc6423}.sidebar{position:-webkit-sticky;position:sticky;top:12px}@media (max-width:991px){.column{width:auto}.site-nav-on .site-brand-container{box-shadow:0 0 16px rgba(0,0,0,.5)}.menu .menu-item.menu-item-search,.sidebar{display:none}}.sidebar-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;box-sizing:border-box;color:var(--text-color);margin-top:12px;max-height:calc(100vh - 24px);visibility:hidden}.site-state-item{padding:0 10px}.sidebar .sidebar-button{border-bottom:1px dotted #ccc;border-top:1px dotted #ccc}.sidebar .sidebar-button button{border:0;color:#fc6423;display:block;width:100%}.sidebar .sidebar-button button:hover{background:0 0;border:0;color:#e34603}.links-of-author{display:flex;flex-wrap:wrap;justify-content:center}.links-of-author-item{margin:5px 0 0;width:50%}.links-of-author-item a{box-sizing:border-box;max-width:100%;overflow:hidden;padding:0 5px;text-overflow:ellipsis;white-space:nowrap;border-bottom:0;border-radius:4px;display:block}.links-of-author-item a:hover{background:var(--body-bg-color)}.main-inner{background:var(--content-bg-color);box-shadow:initial;padding:40px}@media (max-width:991px){.main-inner{padding:20px}}.sub-menu{border-bottom:1px solid #ddd}.post-block:first-of-type{padding-top:40px}@media (max-width:767px){.pagination{margin-bottom:10px}}</style><link as=style href=https://fonts.googleapis.com/css?family=Lato:300,300italic,400,400italic,700,700italic&display=swap&subset=latin,latin-ext onload=this.rel='stylesheet' rel=preload><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css integrity=sha256-yIDrPSXHZdOZhAqiBP7CKzIwMQmRCJ8UeB8Jo17YC4o= rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.1.1/animate.min.css integrity=sha256-PR7ttpcvz8qrF57fur/yAx1qXMFJeJFiA6pSzWi0OIE= rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/5.0.28/fancybox/fancybox.css integrity=sha256-6cQIC71/iBIYXFK+0RHAvwmjwWzkWd+r7v/BX3/vZDc= rel=stylesheet><script class=next-config data-name=main type=application/json>{"hostname":"nicksxs.me","root":"/","images":"/images","scheme":"Pisces","darkmode":false,"version":"8.19.1","exturl":false,"sidebar":{"position":"left","display":"post","padding":18,"offset":12},"copycode":{"enable":true,"style":"default"},"fold":{"enable":false,"height":500},"bookmark":{"enable":false,"color":"#222","save":"auto"},"mediumzoom":false,"lazyload":true,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"stickytabs":false,"motion":{"enable":true,"async":false,"transition":{"menu_item":"fadeInDown","post_block":"fadeIn","post_header":"fadeInDown","post_body":"fadeInDown","coll_header":"fadeInLeft","sidebar":"fadeInUp"}},"prism":false,"i18n":{"placeholder":"搜索...","empty":"没有找到任何搜索结果:${query}","hits_time":"找到 ${hits} 个搜索结果(用时 ${time} 毫秒)","hits":"找到 ${hits} 个搜索结果"},"algolia":{"appID":"663Q99OQQD","apiKey":"e461a1371d12cec1491c1153b288a9ed","indexName":"nicksxs","hits":{"per_page":10}}}</script><script src=/js/config.js></script><meta content="learn from zero,技术博客,Nicksxs,史学森" name=description><meta content=website property=og:type><meta content="Nicksxs's Blog" property=og:title><meta content=https://nicksxs.me/index.html property=og:url><meta content="Nicksxs's Blog" property=og:site_name><meta content="learn from zero,技术博客,Nicksxs,史学森" property=og:description><meta content=zh_CN property=og:locale><meta content=Nicksxs property=article:author><meta content=Nicksxs,史学森,米方方,米方方的男朋友,森哥 property=article:tag><meta content=summary name=twitter:card><link href=https://nicksxs.me/ rel=canonical><script class=next-config data-name=page type=application/json>{"sidebar":"","isHome":true,"isPost":false,"lang":"zh-CN","comments":"","permalink":"","path":"index.html","title":""}</script><script class=next-config data-name=calendar type=application/json>""</script><title>Nicksxs's Blog - What hurts more, the pain of hard work or the pain of regret?</title><script async src=https://www.googletagmanager.com/gtag/js?id=UA-61358619-1></script><script class=next-config data-name=google_analytics type=application/json>{"tracking_id":"UA-61358619-1","only_pageview":false,"measure_protocol_api_secret":null}</script><script src=/js/third-party/analytics/google-analytics.js></script><script src=/js/third-party/analytics/baidu-analytics.js></script><script async src=https://hm.baidu.com/hm.js?20f33b3c0c0eff9b1522999c0015646d></script><noscript><link href=/css/noscript.css rel=stylesheet></noscript><link title="Nicksxs's Blog" href=/atom.xml rel=alternate type=application/atom+xml><body class=use-motion itemscope itemtype=http://schema.org/WebPage><div class=headband></div><main class=main><div class=column><header class=header itemscope itemtype=http://schema.org/WPHeader><div class=site-brand-container><div class=site-nav-toggle><div aria-label=切换导航栏 class=toggle role=button><span class=toggle-line></span><span class=toggle-line></span><span class=toggle-line></span></div></div><div class=site-meta><a class=brand href=/ rel=start><i class=logo-line></i><h1 class=site-title>Nicksxs's Blog</h1><i class=logo-line></i></a><p class=site-subtitle itemprop=description>What hurts more, the pain of hard work or the pain of regret?</div><div class=site-nav-right><div class="toggle popup-trigger" aria-label=搜索 role=button><i class="fa fa-search fa-fw fa-lg"></i></div></div></div><nav class=site-nav><ul class="main-menu menu"><li class="menu-item menu-item-home"><a href=/ rel=section><i class="fa fa-home fa-fw"></i>首页</a><li class="menu-item menu-item-about"><a href=/about/ rel=section><i class="fa fa-user fa-fw"></i>关于我</a><li class="menu-item menu-item-mirror"><a href=https://nicksxs.com/ rel=section target=_blank><i class="fa fa-user fa-fw"></i>国内镜像</a><li class="menu-item menu-item-tags"><a href=/tags/ rel=section><i class="fa fa-tags fa-fw"></i>标签</a><li class="menu-item menu-item-categories"><a href=/categories/ rel=section><i class="fa fa-th fa-fw"></i>分类</a><li class="menu-item menu-item-archives"><a href=/archives/ rel=section><i class="fa fa-archive fa-fw"></i>归档</a><li class="menu-item menu-item-top"><a href=/top/ rel=section><i class="fa fa-th fa-fw"></i>热度</a><li class="menu-item menu-item-sitemap"><a href=/sitemap.xml rel=section><i class="fa fa-sitemap fa-fw"></i>站点地图</a><li class="menu-item menu-item-commonweal"><a href=/404/ rel=section><i class="fa fa-heartbeat fa-fw"></i>公益 404</a><li class="menu-item menu-item-search"><a class=popup-trigger role=button><i class="fa fa-search fa-fw"></i>搜索</a></ul></nav><div class=search-pop-overlay><div class="popup search-popup"><div class=search-header><span class=search-icon><i class="fa fa-search"></i></span><div class=search-input-container></div><span class=popup-btn-close role=button><i class="fa fa-times-circle"></i></span></div><div class=search-result-container><div class=algolia-stats><hr></div><div class=algolia-hits></div><div class=algolia-pagination></div></div></div></div></header><aside class=sidebar><div class="sidebar-inner sidebar-overview-active"><ul class=sidebar-nav><li class=sidebar-nav-toc>文章目录<li class=sidebar-nav-overview>站点概览</ul><div class=sidebar-panel-container><div class="post-toc-wrap sidebar-panel"></div><div class="site-overview-wrap sidebar-panel"><div class="site-author animated" itemprop=author itemscope itemtype=http://schema.org/Person><img alt=Nicksxs class=site-author-image itemprop=image src=/uploads/avatar.jpg><p class=site-author-name itemprop=name>Nicksxs<div class=site-description itemprop=description>learn from zero,技术博客,Nicksxs,史学森</div></div><div class="site-state-wrap animated"><nav class=site-state><div class="site-state-item site-state-posts"><a href=/archives/><span class=site-state-item-count>386</span> <span class=site-state-item-name>日志</span></a></div><div class="site-state-item site-state-categories"><a href=/categories/><span class=site-state-item-count>182</span> <span class=site-state-item-name>分类</span></a></div><div class="site-state-item site-state-tags"><a href=/tags/><span class=site-state-item-count>328</span> <span class=site-state-item-name>标签</span></a></div></nav></div><div class="links-of-author animated"><span class=links-of-author-item><a rel="noopener me" title="GitHub → https://github.com/nicksxs" href=https://github.com/nicksxs target=_blank><i class="fab fa-github fa-fw"></i>GitHub</a> </span><span class=links-of-author-item><a rel="noopener me" title="E-Mail → mailto:nicksxs1202@gmail.com" href=mailto:nicksxs1202@gmail.com target=_blank><i class="fa fa-envelope fa-fw"></i>E-Mail</a></span></div><div class="cc-license animated" itemprop=license><a class=cc-opacity href=https://creativecommons.org/licenses/by-nc-sa/4.0/ rel=noopener target=_blank><img alt="Creative Commons" src=https://cdnjs.cloudflare.com/ajax/libs/creativecommons-vocabulary/2020.11.3/assets/license_badges/small/by_nc_sa.svg></a></div><div class=post-gallery itemscope itemtype=http://schema.org/ImageGallery style=height:120px;min-height:120px><div class=post-gallery-row><a class="post-gallery-img fancybox" href=https://url.cn/LLWrL7gx itemprop=url itemscope itemtype=http://schema.org/ImageObject rel=gallery_ target=_blank><img alt=腾讯云推广 itemprop=contentUrl src=https://img.nicksxs.com/blog/345X200.jpg style=padding:12px></a></div></div><div data-text-font="Trebuchet MS, Helvetica" class=widget-wrap data-canvas-id=sidebar-tag-cloud-canvas data-freeze-active=true data-max-speed=0.1 data-outline-colour=#E2E1D1 data-script-src=/js/tagcanvas.js data-tag-cloud data-text-colour=#333 data-text-height=20><div class="widget tagcloud" data-tag-cloud-container id=myCanvasContainer><canvas height=250 id=sidebar-tag-cloud-canvas style=width:100% width=250><ul class=tag-list itemprop=keywords><li class=tag-list-item><a class=tag-list-link href=/tags/Java/ rel=tag>Java</a><li class=tag-list-item><a class=tag-list-link href=/tags/java/ rel=tag>java</a><li class=tag-list-item><a class=tag-list-link href=/tags/leetcode/ rel=tag>leetcode</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E7%94%9F%E6%B4%BB/ rel=tag>生活</a><li class=tag-list-item><a class=tag-list-link href=/tags/LLM/ rel=tag>LLM</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E9%A2%98%E8%A7%A3/ rel=tag>题解</a><li class=tag-list-item><a class=tag-list-link href=/tags/SpringBoot/ rel=tag>SpringBoot</a><li class=tag-list-item><a class=tag-list-link href=/tags/c/ rel=tag>c++</a><li class=tag-list-item><a class=tag-list-link href=/tags/Tomcat/ rel=tag>Tomcat</a><li class=tag-list-item><a class=tag-list-link href=/tags/Mysql/ rel=tag>Mysql</a><li class=tag-list-item><a class=tag-list-link href=/tags/Mybatis/ rel=tag>Mybatis</a><li class=tag-list-item><a class=tag-list-link href=/tags/redis/ rel=tag>redis</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ rel=tag>小技巧</a><li class=tag-list-item><a class=tag-list-link href=/tags/mysql/ rel=tag>mysql</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ rel=tag>数据结构</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%BA%90%E7%A0%81/ rel=tag>源码</a><li class=tag-list-item><a class=tag-list-link href=/tags/MQ/ rel=tag>MQ</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ rel=tag>消息队列</a><li class=tag-list-item><a class=tag-list-link href=/tags/RocketMQ/ rel=tag>RocketMQ</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E8%BF%90%E5%8A%A8/ rel=tag>运动</a><li class=tag-list-item><a class=tag-list-link href=/tags/php/ rel=tag>php</a><li class=tag-list-item><a class=tag-list-link href=/tags/Spring/ rel=tag>Spring</a><li class=tag-list-item><a class=tag-list-link href=/tags/linux/ rel=tag>linux</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%87%8F%E8%82%A5/ rel=tag>减肥</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E8%B7%91%E6%AD%A5/ rel=tag>跑步</a><li class=tag-list-item><a class=tag-list-link href=/tags/Dubbo/ rel=tag>Dubbo</a><li class=tag-list-item><a class=tag-list-link href=/tags/headscale/ rel=tag>headscale</a><li class=tag-list-item><a class=tag-list-link href=/tags/Windows/ rel=tag>Windows</a><li class=tag-list-item><a class=tag-list-link href=/tags/Docker/ rel=tag>Docker</a><li class=tag-list-item><a class=tag-list-link href=/tags/git/ rel=tag>git</a><li class=tag-list-item><a class=tag-list-link href=/tags/nas/ rel=tag>nas</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%B9%B2%E6%B4%BB/ rel=tag>干活</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E7%AE%97%E6%B3%95/ rel=tag>算法</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%B7%A5%E5%85%B7/ rel=tag>工具</a><li class=tag-list-item><a class=tag-list-link href=/tags/llm/ rel=tag>llm</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ rel=tag>糟心事</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ rel=tag>年终总结</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/ rel=tag>读后感</a><li class=tag-list-item><a class=tag-list-link href=/tags/Apollo/ rel=tag>Apollo</a><li class=tag-list-item><a class=tag-list-link href=/tags/RPC/ rel=tag>RPC</a><li class=tag-list-item><a class=tag-list-link href=/tags/nginx/ rel=tag>nginx</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%89%8A%E5%B3%B0%E5%A1%AB%E8%B0%B7/ rel=tag>削峰填谷</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/ rel=tag>中间件</a><li class=tag-list-item><a class=tag-list-link href=/tags/markdown/ rel=tag>markdown</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E8%A7%84%E5%88%99/ rel=tag>规则</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%9D%AD%E5%B7%9E/ rel=tag>杭州</a><li class=tag-list-item><a class=tag-list-link href=/tags/2021/ rel=tag>2021</a><li class=tag-list-item><a class=tag-list-link href=/tags/aqs/ rel=tag>aqs</a><li class=tag-list-item><a class=tag-list-link href=/tags/C/ rel=tag>C++</a><li class=tag-list-item><a class=tag-list-link href=/tags/Disruptor/ rel=tag>Disruptor</a><li class=tag-list-item><a class=tag-list-link href=/tags/Binary-Tree/ rel=tag>Binary Tree</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/ rel=tag>二叉树</a><li class=tag-list-item><a class=tag-list-link href=/tags/namespace/ rel=tag>namespace</a><li class=tag-list-item><a class=tag-list-link href=/tags/ssh/ rel=tag>ssh</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ rel=tag>端口转发</a><li class=tag-list-item><a class=tag-list-link href=/tags/grep/ rel=tag>grep</a><li class=tag-list-item><a class=tag-list-link href=/tags/Rust/ rel=tag>Rust</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%89%80%E6%9C%89%E6%9D%83/ rel=tag>所有权</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%86%85%E5%AD%98%E5%88%86%E5%B8%83/ rel=tag>内存分布</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%96%B0%E8%AF%AD%E8%A8%80/ rel=tag>新语言</a></ul></canvas></div></div><script defer src=/js/tagcloud-loader.js></script></div></div></div><div class="sidebar-inner sidebar-blogroll"><div class="links-of-blogroll animated"><div class=links-of-blogroll-title><i class="fa fa-globe fa-fw"></i> 链接</div><ul class=links-of-blogroll-list><li class=links-of-blogroll-item><a href=https://covermusic.cn/ rel=noopener target=_blank title=https://covermusic.cn>69伙伴</a></ul></div></div></aside></div><div class="main-inner index posts-expand"><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/ itemprop=url>从ASM到ByteKit再到Arthas:一条Java字节码增强链路</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-08-30 15:10:00 / 修改时间:23:44:29" datetime=2026-08-30T15:10:00+08:00>2026-08-30</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/java/ itemprop=url rel=index><span itemprop=name>java</span></a> </span>, <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/java/%E6%BA%90%E7%A0%81/ itemprop=url rel=index><span itemprop=name>源码</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-increment=false data-path=/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/ data-title=从ASM到ByteKit再到Arthas:一条Java字节码增强链路 title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/08/30/%E4%BB%8EASM%E5%88%B0ByteKit%E5%86%8D%E5%88%B0Arthas-%E4%B8%80%E6%9D%A1Java%E5%AD%97%E8%8A%82%E7%A0%81%E5%A2%9E%E5%BC%BA%E9%93%BE%E8%B7%AF/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/08/30/从ASM到ByteKit再到Arthas-一条Java字节码增强链路/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><p>之前写过几篇 Java Agent 和 Javassist,也单独介绍过 Arthas 的 <code>vmtool</code>。这些内容放在一起看,会发现它们其实一直围绕着同一个问题:<blockquote><p>不修改业务源码,能不能在一个 Java 方法执行前后,临时加上一些观察逻辑?</blockquote><p>比如有这样一个方法:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> String <span class="title function_">placeOrder</span><span class=params>(String user, <span class=type>int</span> quantity)</span> {</span><br><span class=line> <span class=keyword>if</span> (<span class=string>"error"</span>.equals(user)) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">IllegalArgumentException</span>(<span class=string>"user cannot be error"</span>);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> validateQuantity(quantity);</span><br><span class=line> <span class=type>String</span> <span class=variable>sku</span> <span class=operator>=</span> queryInventory(user);</span><br><span class=line> <span class=keyword>return</span> saveOrder(user, sku, quantity);</span><br><span class=line>}</span><br></pre></table></figure><p>我们希望知道:<ul><li>调用时传了什么参数;<li>正常执行返回了什么;<li>抛异常时是什么异常;<li>整个方法以及内部几个调用分别耗时多久。</ul><p>如果能改源码,加几行日志当然最简单。但在线上排查时,往往既不方便改代码,也不希望为了几行日志重新发布和重启应用。一般现在会使用arthas来查看参数,返回等,不过仔细研究下,ASM、ByteKit 和 Arthas,正好可以看成从底到顶的三种解决层次。<p><img alt="ASM、ByteKit 与 Arthas 的分层关系" data-src=/uploads/asm-bytekit-arthas/tool-stack.svg><p>简单概括就是:<table><thead><tr><th>工具<th>它解决的问题<th>使用者面对的东西<tbody><tr><td>ASM<td>怎样正确读写 class 字节码<td>指令、操作数栈、局部变量、栈帧<tr><td>ByteKit<td>怎样更方便地表达诊断插桩<td><code>@AtEnter</code>、<code>@AtExit</code>、<code>@Binding</code><tr><td>Arthas<td>怎样把插桩变成可以直接使用的排障工具<td><code>watch</code>、<code>trace</code>、<code>jad</code>、<code>reset</code></table><p>这里最容易产生的误解是把它们看成三套互相竞争的技术。实际上,ByteKit 构建在 ASM 之上,而 Arthas 的字节码增强又使用了 ByteKit。它们更像是同一条链路上的不同抽象层。<p>本文会沿着这条链路,从一个可以运行的例子开始,一层层看清楚它们分别做了什么。<p>完整示例的源码会在本文末尾直接渲染,本地工程放在:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>source/code/asm-bytekit-arthas-demo</span><br></pre></table></figure><p>下面的代码块来自这个目录中的真实文件,不依赖目录页面。示例要求 JDK 8 以上和 Maven 3.6 以上,本文实际使用 JDK 8、ASM 9.9.1、ByteKit 0.1.7 和 Arthas 4.3.2 验证。<h2 id=先从-class-是怎样被改掉的说起><a title="先从 class 是怎样被改掉的说起" class=headerlink href=#先从-class-是怎样被改掉的说起></a>先从 class 是怎样被改掉的说起</h2><p>在看 ASM 的 API 之前,先把 Java 程序从源码到运行的过程简化一下:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br></pre><td class=code><pre><span class=line>DemoService.java</span><br><span class=line> |</span><br><span class=line> | javac</span><br><span class=line> v</span><br><span class=line>DemoService.class,也就是一组 byte[]</span><br><span class=line> |</span><br><span class=line> | ClassLoader.defineClass</span><br><span class=line> v</span><br><span class=line>JVM 中的 Class&LT?> 和方法实现</span><br><span class=line> |</span><br><span class=line> | 解释执行 / JIT 编译</span><br><span class=line> v</span><br><span class=line>机器指令</span><br></pre></table></figure><p>平时我们关注的是 <code>.java</code> 和对象,字节码工具关注的则是中间那段 <code>byte[]</code>。<p>Java 在 <code>java.lang.instrument</code> 包里提供了 <code>Instrumentation</code> 接口。Agent 可以向它注册一个 <code>ClassFileTransformer</code>:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>instrumentation.addTransformer(transformer, <span class=literal>true</span>);</span><br></pre></table></figure><p>当 JVM 加载类,或者对已加载的类执行 <code>retransformClasses()</code> 时,会调用 Transformer:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line><span class=type>byte</span>[] transform(</span><br><span class=line> ClassLoader loader,</span><br><span class=line> String className,</span><br><span class=line> Class&LT?> classBeingRedefined,</span><br><span class=line> ProtectionDomain protectionDomain,</span><br><span class=line> <span class=type>byte</span>[] classfileBuffer</span><br><span class=line>)</span><br></pre></table></figure><p>这里最重要的就是最后一个参数和返回值:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>JVM 给 Transformer 原始 classfileBuffer</span><br><span class=line> |</span><br><span class=line> | 修改</span><br><span class=line> v</span><br><span class=line>Transformer 返回新的 byte[]</span><br><span class=line> |</span><br><span class=line> | 校验并安装</span><br><span class=line> v</span><br><span class=line>JVM 开始执行增强后的方法</span><br></pre></table></figure><p>如果 Transformer 返回 <code>null</code>,表示这个类不修改。返回原始数组也能达到类似效果,但按照接口语义,不处理时返回 <code>null</code> 更清楚。<p>这也是为什么 ASM、Javassist、Byte Buddy 和 ByteKit 虽然 API 完全不同,最后却都能挂到 Java Agent 上:它们最终都在完成同一件事,把一份 class 字节数组转换成另一份 class 字节数组。<h2 id=premain-和-agentmain-又是什么关系><a title="premain 和 agentmain 又是什么关系" class=headerlink href=#premain-和-agentmain-又是什么关系></a>premain 和 agentmain 又是什么关系</h2><p>之前 Agent 示例里使用过 <code>premain</code>:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">premain</span><span class=params>(String agentArgs, Instrumentation instrumentation)</span> {</span><br><span class=line> instrumentation.addTransformer(<span class=keyword>new</span> <span class="title class_">AsmTimingTransformer</span>(), <span class=literal>true</span>);</span><br><span class=line>}</span><br></pre></table></figure><p>配合 JVM 参数启动:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>java -javaagent:asm-agent.jar -jar app.jar</span><br></pre></table></figure><p><code>premain</code> 在应用 <code>main</code> 方法之前执行,所以特别适合 APM、链路追踪、覆盖率等需要从启动阶段持续工作的工具。<p>但 Arthas 面对的是一个已经运行的 JVM,它使用的是另一条入口:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">agentmain</span><span class=params>(String agentArgs, Instrumentation instrumentation)</span> {</span><br><span class=line> <span class=comment>// 动态挂载之后执行</span></span><br><span class=line>}</span><br></pre></table></figure><p>两者拿到的都是 <code>Instrumentation</code>,主要区别只是进入目标 JVM 的时机:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>启动时加载:-javaagent -> premain -> main</span><br><span class=line></span><br><span class=line>运行时加载:目标 JVM 已运行 -> Attach API -> agentmain</span><br></pre></table></figure><p>所以“Java Agent”和“Arthas 动态挂载”并不是两套原理。Arthas 只是把 Agent 从启动参数加载,变成了运行时 Attach。<h2 id=第一层:直接使用-ASM-改方法><a title="第一层:直接使用 ASM 改方法" class=headerlink href=#第一层:直接使用-ASM-改方法></a>第一层:直接使用 ASM 改方法</h2><p>ASM 是一个很底层的字节码操作库。这里的“底层”并不是说它直接操作二进制位,而是说它暴露出来的概念已经非常接近 JVM class 文件:<ul><li>类名使用 <code>com/example/DemoService</code> 这种 internal name;<li>方法由名字和 descriptor 一起确定;<li>方法内容是一条条 JVM 指令;<li>我们需要关心局部变量、操作数栈和 stack map frame。</ul><h3 id=为什么方法名还不够><a class=headerlink href=#为什么方法名还不够 title=为什么方法名还不够></a>为什么方法名还不够</h3><p>Java 支持方法重载,下面两个方法名字相同:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>placeOrder(String user, <span class=type>int</span> quantity)</span><br><span class=line>placeOrder(String user)</span><br></pre></table></figure><p>字节码里需要用 descriptor 区分。本文目标方法的 descriptor 是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>(Ljava/lang/String;I)Ljava/lang/String;</span><br></pre></table></figure><p>拆开看并不复杂:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>(</span><br><span class=line> Ljava/lang/String; 第一个参数 String</span><br><span class=line> I 第二个参数 int</span><br><span class=line>)</span><br><span class=line>Ljava/lang/String; 返回值 String</span><br></pre></table></figure><p>因此 ASM Transformer 里同时匹配类名、方法名和 descriptor:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line><span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_CLASS</span> <span class=operator>=</span></span><br><span class=line> <span class=string>"com/nicksxs/bytecode/demo/app/DemoService"</span>;</span><br><span class=line><span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_METHOD</span> <span class=operator>=</span> <span class=string>"placeOrder"</span>;</span><br><span class=line><span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_DESCRIPTOR</span> <span class=operator>=</span></span><br><span class=line> <span class=string>"(Ljava/lang/String;I)Ljava/lang/String;"</span>;</span><br></pre></table></figure><p>这比只匹配方法名稳妥,否则一个类里所有重载方法都可能被增强。<h3 id=ClassReader、Visitor-和-ClassWriter><a title="ClassReader、Visitor 和 ClassWriter" class=headerlink href=#ClassReader、Visitor-和-ClassWriter></a>ClassReader、Visitor 和 ClassWriter</h3><p>ASM 最典型的处理流程是:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br></pre><td class=code><pre><span class=line><span class=type>ClassReader</span> <span class=variable>reader</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassReader</span>(classfileBuffer);</span><br><span class=line><span class=type>ClassWriter</span> <span class=variable>writer</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassWriter</span>(</span><br><span class=line> reader,</span><br><span class=line> ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS</span><br><span class=line>);</span><br><span class=line></span><br><span class=line><span class=type>ClassVisitor</span> <span class=variable>visitor</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassVisitor</span>(Opcodes.ASM9, writer) {</span><br><span class=line> <span class=meta>@Override</span></span><br><span class=line> <span class=keyword>public</span> MethodVisitor <span class="title function_">visitMethod</span><span class=params>(</span></span><br><span class=line><span class=params> <span class=type>int</span> access,</span></span><br><span class=line><span class=params> String name,</span></span><br><span class=line><span class=params> String descriptor,</span></span><br><span class=line><span class=params> String signature,</span></span><br><span class=line><span class=params> String[] exceptions)</span> {</span><br><span class=line> <span class=type>MethodVisitor</span> <span class=variable>mv</span> <span class=operator>=</span> <span class=built_in>super</span>.visitMethod(</span><br><span class=line> access, name, descriptor, signature, exceptions);</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (TARGET_METHOD.equals(name)</span><br><span class=line> && TARGET_DESCRIPTOR.equals(descriptor)) {</span><br><span class=line> <span class=keyword>return</span> <span class=keyword>new</span> <span class="title class_">TimingAdvice</span>(mv, access, name, descriptor);</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> mv;</span><br><span class=line> }</span><br><span class=line>};</span><br><span class=line></span><br><span class=line>reader.accept(visitor, ClassReader.EXPAND_FRAMES);</span><br><span class=line><span class=keyword>return</span> writer.toByteArray();</span><br></pre></table></figure><p>可以把它理解成一条流水线:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>原始 byte[]</span><br><span class=line> |</span><br><span class=line>ClassReader 解析 class 结构</span><br><span class=line> |</span><br><span class=line>ClassVisitor 访问类、字段和方法</span><br><span class=line> |</span><br><span class=line>MethodVisitor 在目标方法中插入指令</span><br><span class=line> |</span><br><span class=line>ClassWriter 重新生成 byte[]</span><br></pre></table></figure><p>ASM 使用 Visitor 模式的一个好处是可以边读边写,不一定要把整个 class 转成另一套庞大的对象模型。需要更方便地增删和遍历指令时,也可以使用 ASM Tree API,把类读成 <code>ClassNode</code> 和 <code>MethodNode</code>。<h3 id=为什么插一行日志也会牵涉操作数栈><a class=headerlink href=#为什么插一行日志也会牵涉操作数栈 title=为什么插一行日志也会牵涉操作数栈></a>为什么插一行日志也会牵涉操作数栈</h3><p>源码中的:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>System.out.println(<span class=string>"hello"</span>);</span><br></pre></table></figure><p>落实到字节码,大致是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>GETSTATIC java/lang/System.out : Ljava/io/PrintStream;</span><br><span class=line>LDC "hello"</span><br><span class=line>INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V</span><br></pre></table></figure><p>前两条指令把 <code>PrintStream</code> 对象和字符串压入操作数栈,第三条再把它们消费掉。顺序错了、类型错了或者某条分支上的栈高度不一致,JVM 验证类时就可能抛出 <code>VerifyError</code>。<p>本文示例使用 <code>AdviceAdapter</code>,它在 <code>MethodVisitor</code> 上又封装了一层,提供:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>onMethodEnter()</span><br><span class=line>onMethodExit(<span class=type>int</span> opcode)</span><br></pre></table></figure><p>我们仍然是在生成 JVM 指令,只是少写了一些容易出错的样板代码。<h3 id=在方法入口记录参数和开始时间><a class=headerlink href=#在方法入口记录参数和开始时间 title=在方法入口记录参数和开始时间></a>在方法入口记录参数和开始时间</h3><p>示例在入口先调用 <code>System.nanoTime()</code>,把结果放进一个新建的局部变量:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br></pre><td class=code><pre><span class=line><span class=meta>@Override</span></span><br><span class=line><span class=keyword>protected</span> <span class=keyword>void</span> <span class="title function_">onMethodEnter</span><span class=params>()</span> {</span><br><span class=line> invokeStatic(SYSTEM_TYPE, NANO_TIME);</span><br><span class=line> startNanosLocal = newLocal(Type.LONG_TYPE);</span><br><span class=line> storeLocal(startNanosLocal);</span><br><span class=line></span><br><span class=line> getStatic(SYSTEM_TYPE, <span class=string>"out"</span>, PRINT_STREAM_TYPE);</span><br><span class=line> newInstance(STRING_BUILDER_TYPE);</span><br><span class=line> dup();</span><br><span class=line> invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILDER_INIT);</span><br><span class=line> push(<span class=string>"[ASM] enter placeOrder user="</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> loadArg(<span class=number>0</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> push(<span class=string>", quantity="</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> loadArg(<span class=number>1</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_INT);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, TO_STRING);</span><br><span class=line> invokeVirtual(PRINT_STREAM_TYPE, PRINTLN);</span><br><span class=line>}</span><br></pre></table></figure><p>这里的 <code>loadArg(0)</code> 和 <code>loadArg(1)</code>,最后会变成相应的局部变量加载指令。ASM 知道方法是不是静态方法,因此会帮我们处理 <code>this</code> 占用的第 0 个局部变量槽位。<h3 id=一个方法不一定只有一个出口><a class=headerlink href=#一个方法不一定只有一个出口 title=一个方法不一定只有一个出口></a>一个方法不一定只有一个出口</h3><p>正常返回可能使用:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>IRETURN</span><br><span class=line>LRETURN</span><br><span class=line>FRETURN</span><br><span class=line>DRETURN</span><br><span class=line>ARETURN</span><br><span class=line>RETURN</span><br></pre></table></figure><p>异常还会经过 <code>ATHROW</code>。如果直接找到最后一条指令插日志,很容易漏掉前面的提前返回和异常分支。<p><code>AdviceAdapter.onMethodExit()</code> 会在这些退出指令前回调:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br></pre><td class=code><pre><span class=line><span class=meta>@Override</span></span><br><span class=line><span class=keyword>protected</span> <span class=keyword>void</span> <span class="title function_">onMethodExit</span><span class=params>(<span class=type>int</span> opcode)</span> {</span><br><span class=line> invokeStatic(SYSTEM_TYPE, NANO_TIME);</span><br><span class=line> loadLocal(startNanosLocal);</span><br><span class=line> math(SUB, Type.LONG_TYPE);</span><br><span class=line> <span class=type>int</span> <span class=variable>elapsedLocal</span> <span class=operator>=</span> newLocal(Type.LONG_TYPE);</span><br><span class=line> storeLocal(elapsedLocal);</span><br><span class=line></span><br><span class=line> <span class=comment>// 后面生成 println("cost=" + elapsed + " ns") 的指令</span></span><br><span class=line>}</span><br></pre></table></figure><p>示例还根据 <code>opcode == ATHROW</code> 区分正常返回和显式抛出异常。<p>这里有个值得注意的细节:只在 <code>ATHROW</code> 前插代码,能捕获方法字节码里显式执行的 <code>ATHROW</code>,却不等同于给整个方法套了一层 <code>try/finally</code>。如果某个子调用抛出异常并直接向外传播,目标方法本身不一定存在一条可见的 <code>ATHROW</code> 指令。真正通用的异常退出增强,需要改写异常表,ByteKit 的 <code>@AtExceptionExit</code> 就封装了这部分处理。<h3 id=COMPUTE-MAXS-和-COMPUTE-FRAMES-在算什么><a title="COMPUTE_MAXS 和 COMPUTE_FRAMES 在算什么" class=headerlink href=#COMPUTE-MAXS-和-COMPUTE-FRAMES-在算什么></a>COMPUTE_MAXS 和 COMPUTE_FRAMES 在算什么</h3><p>每个方法在 class 文件里都需要声明:<ul><li>最大操作数栈深度;<li>最大局部变量槽位数量;<li>分支合流位置的 stack map frame。</ul><p>插入新指令和新局部变量以后,这些数据可能发生变化。示例让 <code>ClassWriter</code> 自动计算:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS</span><br></pre></table></figure><p>这非常适合入门示例,但不是说以后就完全不用理解栈帧。复杂项目里,计算 frame 时可能需要解析类型的共同父类,从而牵涉目标 ClassLoader;不恰当地加载业务类,还可能带来类初始化或类加载隔离问题。Arthas 和 ByteKit 里有不少代码,就是在处理这些工程细节。<h2 id=运行-ASM-示例><a title="运行 ASM 示例" class=headerlink href=#运行-ASM-示例></a>运行 ASM 示例</h2><p>进入示例目录并构建:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line><span class=built_in>cd</span> <span class=built_in>source</span>/code/asm-bytekit-arthas-demo</span><br><span class=line>mvn clean package</span><br></pre></table></figure><p>先运行没有增强的版本:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>./run.sh baseline 4 10</span><br></pre></table></figure><p>输出只有应用自己的日志:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>demo pid=21257</span><br><span class=line>iterations=4</span><br><span class=line>[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1</span><br><span class=line>[APP] order created: user=user-2, sku=SKU-USER-2, quantity=2</span><br><span class=line>[APP] order created: user=user-3, sku=SKU-USER-3, quantity=3</span><br><span class=line>[APP] failed: user cannot be error</span><br></pre></table></figure><p>再带上 ASM Agent:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>./run.sh asm 4 10</span><br></pre></table></figure><p>可以看到业务源码没有改变,但 <code>placeOrder()</code> 前后已经出现了 Agent 插入的输出:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>[ASM agent] installed</span><br><span class=line>[ASM agent] transforming com/nicksxs/bytecode/demo/app/DemoService</span><br><span class=line>[ASM] enter placeOrder user=user-1, quantity=1</span><br><span class=line>[ASM] return placeOrder cost=114967458 ns</span><br><span class=line>[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1</span><br><span class=line>...</span><br><span class=line>[ASM] enter placeOrder user=error, quantity=4</span><br><span class=line>[ASM] throw placeOrder cost=478125 ns</span><br><span class=line>[APP] failed: user cannot be error</span><br></pre></table></figure><p>这一层已经把字节码增强的基本原理跑通了。不过问题也很明显:为了获取两个参数和一个耗时,我们写了不少类型、descriptor、局部变量和指令生成代码。如果要支持任意方法的参数、返回值、异常和内部调用,代码量会迅速增加。<p>ByteKit 就是为了解决这一层的重复劳动。<h2 id=第二层:ByteKit-把字节码操作变成诊断语义><a title="第二层:ByteKit 把字节码操作变成诊断语义" class=headerlink href=#第二层:ByteKit-把字节码操作变成诊断语义></a>第二层:ByteKit 把字节码操作变成诊断语义</h2><p>ByteKit 官方对自己的定位很明确:<blockquote><p>基于 ASM 提供更高层的字节码处理能力,面向诊断和 APM 领域,而不是一套通用字节码库。</blockquote><p>所以 ByteKit 并没有取代 ASM。它做的是把诊断工具经常需要的动作整理成更容易描述的概念。<h3 id=注入点解决“在哪里执行”><a class=headerlink href=#注入点解决“在哪里执行” title=注入点解决“在哪里执行”></a>注入点解决“在哪里执行”</h3><p>常见注入点包括:<table><thead><tr><th>注解<th>对应位置<tbody><tr><td><code>@AtEnter</code><td>方法刚进入<tr><td><code>@AtExit</code><td>方法正常返回<tr><td><code>@AtExceptionExit</code><td>方法异常退出<tr><td><code>@AtInvoke</code><td>方法内部调用另一个方法<tr><td><code>@AtInvokeException</code><td>内部调用抛出异常<tr><td><code>@AtLine</code><td>指定源码行<tr><td><code>@AtFieldAccess</code><td>访问字段<tr><td><code>@AtSyncEnter</code> / <code>@AtSyncExit</code><td>进入或退出同步块</table><p>ASM 层要自己寻找 <code>RETURN</code>、<code>ATHROW</code>、<code>MethodInsnNode</code> 或行号节点;ByteKit 层可以先说清楚“我要方法退出点”或者“我要内部调用结束点”。<h3 id=Binding-解决“在这里能拿到什么”><a title="Binding 解决“在这里能拿到什么”" class=headerlink href=#Binding-解决“在这里能拿到什么”></a>Binding 解决“在这里能拿到什么”</h3><p>注入点确定以后,还需要把运行时数据交给增强逻辑:<table><thead><tr><th>Binding<th>得到的数据<tbody><tr><td><code>@Binding.This</code><td>当前对象 <code>this</code><tr><td><code>@Binding.Args</code><td>方法参数数组<tr><td><code>@Binding.Return</code><td>返回值<tr><td><code>@Binding.Throwable</code><td>抛出的异常<tr><td><code>@Binding.MethodName</code><td>方法名<tr><td><code>@Binding.MethodDesc</code><td>方法 descriptor<tr><td><code>@Binding.Field</code><td>当前对象的指定字段<tr><td><code>@Binding.InvokeArgs</code><td>被调用方法的参数<tr><td><code>@Binding.InvokeReturn</code><td>被调用方法的返回值</table><p>本文的 ByteKit 拦截器就变得非常直观:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">DemoInterceptor</span> {</span><br><span class=line></span><br><span class=line> <span class=meta>@AtEnter(inline = true)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atEnter</span><span class=params>(</span></span><br><span class=line><span class=params> <span class=meta>@Binding</span>.MethodName String methodName,</span></span><br><span class=line><span class=params> <span class=meta>@Binding</span>.Args Object[] args)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] enter "</span></span><br><span class=line> + methodName + <span class=string>" args="</span> + Arrays.toString(args));</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=meta>@AtExit(inline = true)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atExit</span><span class=params>(<span class=meta>@Binding</span>.Return Object returnObject)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] return "</span> + returnObject);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=meta>@AtExceptionExit(inline = true, onException = Throwable.class)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atExceptionExit</span><span class=params>(</span></span><br><span class=line><span class=params> <span class=meta>@Binding</span>.Throwable Throwable throwable)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] throw "</span></span><br><span class=line> + throwable.getClass().getSimpleName()</span><br><span class=line> + <span class=string>": "</span> + throwable.getMessage());</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><p>同样是获取参数、返回值和异常,这段代码比直接写 ASM 更接近我们的真实意图。<h3 id=ByteKit-不是通过反射“监听”方法><a title="ByteKit 不是通过反射“监听”方法" class=headerlink href=#ByteKit-不是通过反射“监听”方法></a>ByteKit 不是通过反射“监听”方法</h3><p>看到这些注解,很容易把它想成 Spring AOP 那样的代理调用。实际上 ByteKit 处理的仍然是目标方法本身的字节码。<p>示例的 Transformer 核心逻辑是:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br></pre><td class=code><pre><span class=line><span class=type>DefaultInterceptorClassParser</span> <span class=variable>parser</span> <span class=operator>=</span></span><br><span class=line> <span class=keyword>new</span> <span class="title class_">DefaultInterceptorClassParser</span>();</span><br><span class=line>List&LTInterceptorProcessor> interceptors =</span><br><span class=line> parser.parse(DemoInterceptor.class);</span><br><span class=line></span><br><span class=line><span class=type>ClassNode</span> <span class=variable>classNode</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassNode</span>();</span><br><span class=line><span class=type>ClassReader</span> <span class=variable>reader</span> <span class=operator>=</span> AsmUtils.toClassNode(</span><br><span class=line> classfileBuffer, classNode);</span><br><span class=line></span><br><span class=line><span class=keyword>for</span> (MethodNode methodNode : classNode.methods) {</span><br><span class=line> <span class=keyword>if</span> (!<span class=string>"placeOrder"</span>.equals(methodNode.name)) {</span><br><span class=line> <span class=keyword>continue</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=type>MethodProcessor</span> <span class=variable>methodProcessor</span> <span class=operator>=</span></span><br><span class=line> <span class=keyword>new</span> <span class="title class_">MethodProcessor</span>(classNode, methodNode);</span><br><span class=line> <span class=keyword>for</span> (InterceptorProcessor interceptor : interceptors) {</span><br><span class=line> interceptor.process(methodProcessor);</span><br><span class=line> }</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=keyword>return</span> AsmUtils.toBytes(classNode, loader, reader);</span><br></pre></table></figure><p>它大致经历四步:<ol><li>解析拦截器上的 <code>@AtEnter</code>、<code>@AtExit</code> 和 Binding;<li>用 ASM Tree API 把原始 class 读成 <code>ClassNode</code>;<li>找到目标 <code>MethodNode</code>,由 <code>MethodProcessor</code> 插入指令;<li>再把修改后的节点树写回 <code>byte[]</code>。</ol><p>ByteKit 的价值不是绕过字节码,而是把一批成熟的字节码生成规则封装起来。<h3 id=inline-到底内联了什么><a title="inline 到底内联了什么" class=headerlink href=#inline-到底内联了什么></a>inline 到底内联了什么</h3><p>示例的注解都设置了:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>inline = <span class=literal>true</span></span><br></pre></table></figure><p>可以把它理解成:增强后并不是简单地在业务方法里调用:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>DemoInterceptor.atEnter(...);</span><br></pre></table></figure><p>而是尽量把拦截器方法里的指令复制到目标方法中。这样做可以减少一次回调调用,也降低目标 ClassLoader 必须能加载拦截器类的要求。<p>不过内联也不是免费午餐。增强逻辑太大,会让目标方法字节码膨胀;增强代码引用的其他类型,仍然需要目标类加载器能够解析。因此诊断插桩通常应该保持短小,把复杂状态管理放在稳定的桥接 API 后面。<h3 id=为什么-ByteKit-有防重复增强><a title="为什么 ByteKit 有防重复增强" class=headerlink href=#为什么-ByteKit-有防重复增强></a>为什么 ByteKit 有防重复增强</h3><p>诊断工具可能在同一个类上连续执行 <code>watch</code>、<code>trace</code> 和 <code>monitor</code>。如果每次都无条件再插一遍入口和出口代码,目标方法会越来越大,输出也会重复。<p>ByteKit 提供了位置过滤机制,可以检查某个注入点是否已经存在。Arthas 的 <code>Enhancer</code> 还会专门检查目标方法中是否已经调用 <code>SpyAPI.atEnter</code>、<code>atExit</code> 和 <code>atExceptionExit</code>,避免重复插入同类探针。<p>这是 ASM 示例和真正工程化诊断工具之间很典型的差距:基础 API 能完成一次修改,框架还要负责多次修改之间的组合、去重和恢复。<h2 id=运行-ByteKit-示例><a title="运行 ByteKit 示例" class=headerlink href=#运行-ByteKit-示例></a>运行 ByteKit 示例</h2><p>执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>./run.sh bytekit 4 10</span><br></pre></table></figure><p>本文实际验证得到:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>[ByteKit agent] installed</span><br><span class=line>[ByteKit agent] transforming com/nicksxs/bytecode/demo/app/DemoService</span><br><span class=line>[ByteKit] enter placeOrder args=[user-1, 1]</span><br><span class=line>[ByteKit] return order created: user=user-1, sku=SKU-USER-1, quantity=1</span><br><span class=line>[APP] order created: user=user-1, sku=SKU-USER-1, quantity=1</span><br><span class=line>...</span><br><span class=line>[ByteKit] enter placeOrder args=[error, 4]</span><br><span class=line>[ByteKit] throw IllegalArgumentException: user cannot be error</span><br><span class=line>[APP] failed: user cannot be error</span><br></pre></table></figure><p>到这里,我们已经用两种方式修改了同一个方法:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>ASM:自己生成每一段参数加载、时间计算和输出指令</span><br><span class=line></span><br><span class=line>ByteKit:声明注入点和需要绑定的数据,由 ByteKit 生成指令</span><br></pre></table></figure><p>但这两种方式仍然需要我们自己写 Agent、打包、指定目标类并启动应用。线上排障真正想要的是:输入一条命令,就临时观察某个方法。<p>这就是 Arthas 所处的第三层。<h2 id=第三层:Arthas-把动态增强变成排障命令><a title="第三层:Arthas 把动态增强变成排障命令" class=headerlink href=#第三层:Arthas-把动态增强变成排障命令></a>第三层:Arthas 把动态增强变成排障命令</h2><p>先启动不带任何 Agent 的 Demo,并让它持续执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>./run.sh app</span><br></pre></table></figure><p>另开一个终端,启动 Arthas:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>curl -O https://arthas.aliyun.com/arthas-boot.jar</span><br><span class=line>java -jar arthas-boot.jar --use-version 4.3.2</span><br></pre></table></figure><p>选择:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>com.nicksxs.bytecode.demo.app.DemoApplication</span><br></pre></table></figure><p>这一步之后,Arthas Agent 已经通过 Attach API 进入目标 JVM,并拿到了 <code>Instrumentation</code>。<h3 id=第一步先确认类和-ClassLoader><a title="第一步先确认类和 ClassLoader" class=headerlink href=#第一步先确认类和-ClassLoader></a>第一步先确认类和 ClassLoader</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>sc -d com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><p>本文测试时的关键输出是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>class-info com.nicksxs.bytecode.demo.app.DemoService</span><br><span class=line>code-source .../asm-bytekit-arthas-demo/target/classes/</span><br><span class=line>class-loader +-sun.misc.Launcher$AppClassLoader@73d16e93</span><br><span class=line>classLoaderHash 73d16e93</span><br></pre></table></figure><p>这一步并不多余。同一个类名可能被多个 ClassLoader 各加载一份,特别是在 Tomcat、插件系统和热部署环境里。如果不先确认 ClassLoader,后面的命令可能命中错误的类,或者提示匹配到了多个类。<p>也可以继续查看方法:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>sm com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><h3 id=watch-观察一次方法的输入和输出><a title="watch 观察一次方法的输入和输出" class=headerlink href=#watch-观察一次方法的输入和输出></a>watch 观察一次方法的输入和输出</h3><p>执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'{params,returnObj,throwExp}'</span> -x 2 -n 4</span><br></pre></table></figure><p>几个参数分别表示:<table><thead><tr><th>参数<th>含义<tbody><tr><td><code>params</code><td>当前调用的参数数组<tr><td><code>returnObj</code><td>正常返回值<tr><td><code>throwExp</code><td>异常,没有异常时为 <code>null</code><tr><td><code>-x 2</code><td>对象展开深度为 2<tr><td><code>-n 4</code><td>命中 4 次后自动退出</table><p>实际输出之一:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>method=com.nicksxs.bytecode.demo.app.DemoService.placeOrder location=AtExit</span><br><span class=line>[cost=123.336292ms] result=@ArrayList[</span><br><span class=line> @Object[][</span><br><span class=line> @String[user-25],</span><br><span class=line> @Integer[25],</span><br><span class=line> ],</span><br><span class=line> @String[order created: user=user-25, sku=SKU-USER-25, quantity=25],</span><br><span class=line> null,</span><br><span class=line>]</span><br></pre></table></figure><p>可以看到,<code>watch</code> 输出里的 <code>location=AtExit</code>,和 ByteKit 的 <code>@AtExit</code> 已经能对应起来。这里出现的 <code>params</code>、<code>returnObj</code> 和 <code>throwExp</code>,本质上也来自方法入口、正常出口和异常出口处插入的探针。<h3 id=trace-为什么能看到内部调用><a title="trace 为什么能看到内部调用" class=headerlink href=#trace-为什么能看到内部调用></a>trace 为什么能看到内部调用</h3><p>执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>trace com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'#cost > 0'</span> -n 2</span><br></pre></table></figure><p>本文实测输出:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>`---[116.058125ms] DemoService:placeOrder()</span><br><span class=line> +---[25.93% 30.089459ms ] DemoService:validateQuantity() #10</span><br><span class=line> +---[38.65% 44.852583ms ] DemoService:queryInventory() #11</span><br><span class=line> `---[35.08% 40.714208ms ] DemoService:saveOrder() #12</span><br></pre></table></figure><p>异常分支则是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>`---[0.902ms] DemoService:placeOrder() [throws Exception]</span><br><span class=line> `---throw:java.lang.IllegalArgumentException #7</span><br><span class=line> [user cannot be error]</span><br></pre></table></figure><p><code>watch</code> 主要关心目标方法的入口和出口,<code>trace</code> 还需要在方法内部的调用指令前后插入探针。对应到 ByteKit,就是 <code>@AtInvoke</code>、调用完成和调用异常等位置。<p>Arthas 源码里的 <code>Enhancer</code> 在 <code>isTracing</code> 为真时,会加入 <code>SpyTraceInterceptor</code>,遍历目标方法里的调用指令,并为每个调用注册 trace listener。最终看到的调用树,是这些 before/after/exception 事件按线程和调用层级组合出来的。<h3 id=jad-看到的是-JVM-当前认识的类><a title="jad 看到的是 JVM 当前认识的类" class=headerlink href=#jad-看到的是-JVM-当前认识的类></a>jad 看到的是 JVM 当前认识的类</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>jad com.nicksxs.bytecode.demo.app.DemoService placeOrder</span><br></pre></table></figure><p>它会把目标 JVM 中的 class 反编译出来,而不是去源码目录读取 <code>DemoService.java</code>。这在排查“代码明明改了,线上为什么还是旧逻辑”“同名 jar 到底加载了哪一份”时很有用。<p>本文 Demo 得到:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> String <span class="title function_">placeOrder</span><span class=params>(String user, <span class=type>int</span> quantity)</span> {</span><br><span class=line> <span class=keyword>if</span> (<span class=string>"error"</span>.equals(user)) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">IllegalArgumentException</span>(<span class=string>"user cannot be error"</span>);</span><br><span class=line> }</span><br><span class=line> <span class=built_in>this</span>.validateQuantity(quantity);</span><br><span class=line> <span class=type>String</span> <span class=variable>sku</span> <span class=operator>=</span> <span class=built_in>this</span>.queryInventory(user);</span><br><span class=line> <span class=keyword>return</span> <span class=built_in>this</span>.saveOrder(user, sku, quantity);</span><br><span class=line>}</span><br></pre></table></figure><p>需要注意,<code>watch -n</code> 或 <code>trace -n</code> 达到次数退出后,对应 listener 会结束,Arthas 也可能重新组织该类上的增强。因此 <code>jad</code> 展示的是执行命令当时 JVM 中有效的字节码状态,不应该把它简单理解成磁盘源码查看器。<h3 id=诊断结束为什么要-reset><a title="诊断结束为什么要 reset" class=headerlink href=#诊断结束为什么要-reset></a>诊断结束为什么要 reset</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>reset com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><p><code>watch</code>、<code>trace</code>、<code>monitor</code> 不是在 JVM 外面被动偷看,它们会修改目标类。诊断结束后执行 <code>reset</code>,可以移除 Arthas 对这个类的增强,让方法回到未观测状态。<p>官方文档也特别提醒,线上使用增强命令时要缩小类和方法匹配范围,并在结束后执行 <code>reset</code> 或 <code>stop</code>。<h2 id=从源码看-Arthas-是怎样使用-ByteKit-的><a title="从源码看 Arthas 是怎样使用 ByteKit 的" class=headerlink href=#从源码看-Arthas-是怎样使用-ByteKit-的></a>从源码看 Arthas 是怎样使用 ByteKit 的</h2><p>如果只看命令,很容易觉得 Arthas 内部可能有一套完全不同的魔法。直接看 <code>com.taobao.arthas.core.advisor.Enhancer</code>,链路就很清楚了。<p>首先,<code>Enhancer</code> 自己就是:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">Enhancer</span> <span class=keyword>implements</span> <span class="title class_">ClassFileTransformer</span></span><br></pre></table></figure><p>它收到 <code>classfileBuffer</code> 后,用 ByteKit 提供的 <code>AsmUtils</code> 读取:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=type>ClassNode</span> <span class=variable>classNode</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassNode</span>(Opcodes.ASM9);</span><br><span class=line><span class=type>ClassReader</span> <span class=variable>classReader</span> <span class=operator>=</span> AsmUtils.toClassNode(</span><br><span class=line> classfileBuffer, classNode);</span><br></pre></table></figure><p>然后解析 Arthas 自己定义的拦截器:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line><span class=type>DefaultInterceptorClassParser</span> <span class=variable>parser</span> <span class=operator>=</span></span><br><span class=line> <span class=keyword>new</span> <span class="title class_">DefaultInterceptorClassParser</span>();</span><br><span class=line></span><br><span class=line>interceptorProcessors.addAll(</span><br><span class=line> parser.parse(SpyInterceptor1.class));</span><br><span class=line>interceptorProcessors.addAll(</span><br><span class=line> parser.parse(SpyInterceptor2.class));</span><br><span class=line>interceptorProcessors.addAll(</span><br><span class=line> parser.parse(SpyInterceptor3.class));</span><br></pre></table></figure><p>如果是 <code>trace</code>,还会加入 <code>SpyTraceInterceptor</code>。找到匹配方法后,再执行:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line><span class=type>MethodProcessor</span> <span class=variable>methodProcessor</span> <span class=operator>=</span></span><br><span class=line> <span class=keyword>new</span> <span class="title class_">MethodProcessor</span>(classNode, methodNode, locationFilter);</span><br><span class=line></span><br><span class=line><span class=keyword>for</span> (InterceptorProcessor interceptor : interceptorProcessors) {</span><br><span class=line> interceptor.process(methodProcessor);</span><br><span class=line>}</span><br></pre></table></figure><p>最后重新生成 class:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=type>byte</span>[] enhanced = AsmUtils.toBytes(</span><br><span class=line> classNode, inClassLoader, classReader);</span><br><span class=line><span class=keyword>return</span> enhanced;</span><br></pre></table></figure><p>这和我们写的 ByteKit Demo 几乎是同一条主干,只是 Arthas 多了大量工程能力:<ul><li>类名、方法名和 ClassLoader 匹配;<li>多命令 listener 管理;<li>防止重复插桩;<li>trace 内部调用注册;<li>CGLIB 构造函数等特殊 class 的处理;<li>原始类和增强状态管理;<li>命令退出后的恢复;<li>把事件送回 Arthas 自己的 ClassLoader。</ul><p>因此可以把它们的关系写成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br></pre><td class=code><pre><span class=line>用户输入 watch / trace</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Arthas 创建 Matcher 和 AdviceListener</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Enhancer 作为 ClassFileTransformer</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>ByteKit 把 AtEnter / AtExit / AtInvoke 转换为增强动作</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>ASM 读取、修改并写回 class byte[]</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Instrumentation.retransformClasses()</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>JVM 执行插入了 SpyAPI 调用的新方法</span><br></pre></table></figure><h2 id=SpyAPI-为什么要放在特殊位置><a title="SpyAPI 为什么要放在特殊位置" class=headerlink href=#SpyAPI-为什么要放在特殊位置></a>SpyAPI 为什么要放在特殊位置</h2><p>Arthas 自己的核心类由独立 ClassLoader 加载,而业务类可能来自 AppClassLoader、WebAppClassLoader 或自定义 ClassLoader。业务方法增强后如果直接引用 Arthas Core 里的某个类,业务 ClassLoader 不一定能找到它。<p>Arthas 使用 <code>java.arthas.SpyAPI</code> 作为一座稳定的桥:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>业务 ClassLoader 中被增强的方法</span><br><span class=line> |</span><br><span class=line> | SpyAPI.atEnter / atExit</span><br><span class=line> v</span><br><span class=line>可被目标类访问的 SpyAPI</span><br><span class=line> |</span><br><span class=line> | 转发事件</span><br><span class=line> v</span><br><span class=line>Arthas AdviceListener</span><br></pre></table></figure><p><code>Enhancer.transform()</code> 的开头还会检查目标 ClassLoader 能否加载 <code>SpyAPI</code>,不能加载就放弃增强。这正是字节码工具里一个很现实的问题:生成一条方法调用指令并不难,难的是确保这条指令在目标类的加载环境里永远能解析成功。<p>我们的小 Demo 把 Agent 和应用都放在同一个 classpath 中,所以感觉不到这种复杂度。真正的诊断工具必须认真处理 ClassLoader 边界。<h2 id=ASM、ByteKit、Arthas-和-Javassist-怎么选><a title="ASM、ByteKit、Arthas 和 Javassist 怎么选" class=headerlink href=#ASM、ByteKit、Arthas-和-Javassist-怎么选></a>ASM、ByteKit、Arthas 和 Javassist 怎么选</h2><p>之前的文章使用 Javassist,通过字符串形式插入代码:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>method.insertBefore(<span class=string>"System.out.println($args);"</span>);</span><br><span class=line>method.insertAfter(<span class=string>"System.out.println($_);"</span>);</span><br></pre></table></figure><p>它的优势是接近 Java 源码,入门很直观。把它和本文三个层次放在一起,可以这样理解:<table><thead><tr><th>工具<th>最适合的场景<th>优点<th>需要承担的复杂度<tbody><tr><td>Javassist<td>快速生成或修改类、简单 Agent<td>接近源码表达,容易上手<td>字符串代码、类型和复杂控制流处理<tr><td>ASM<td>框架底层、需要精确控制和低开销<td>能控制每条指令,生态基础扎实<td>栈、frame、descriptor、异常表<tr><td>ByteKit<td>APM、诊断类插桩框架<td>注入点和 Binding 丰富,适合复用<td>仍要负责 Agent、匹配、生命周期<tr><td>Arthas<td>直接排查运行中的 Java 应用<td>开箱即用、动态挂载、命令完整<td>需要控制线上影响和匹配范围</table><p>如果只是想确认线上某个参数或耗时,优先用 Arthas,而不是现场写 ASM。<p>如果在开发自己的诊断或 APM 能力,ByteKit 能减少大量通用插桩工作。<p>如果 ByteKit 没有提供需要的注入位置,或者正在实现更底层的字节码框架,再下沉到 ASM。<p>理解 ASM 的意义,不是以后所有事情都手写指令,而是知道上层工具究竟替我们处理了哪些问题。<h2 id=retransform-也不是任意热更新><a title="retransform 也不是任意热更新" class=headerlink href=#retransform-也不是任意热更新></a>retransform 也不是任意热更新</h2><p><code>Instrumentation.retransformClasses()</code> 可以替换方法实现,但它不是完整的类结构热更新。通常不能在重转换时随意:<ul><li>新增或删除字段;<li>新增或删除方法;<li>修改方法签名;<li>改变继承关系;<li>改变 nest host、record component 等类结构属性。</ul><p><code>watch</code> 和 <code>trace</code> 只在已有方法内部增加指令,没有改变类的 schema,所以适合使用 retransform。<p>这也解释了为什么 Arthas 的 <code>mc</code> 加 <code>retransform</code> 能修改已有方法逻辑,却不能把一个全新的字段热塞进已经加载的类。限制来自 JVM 类重定义规则,不是 Arthas 少实现了一个参数。<h2 id=多个-Agent-同时工作时发生什么><a title="多个 Agent 同时工作时发生什么" class=headerlink href=#多个-Agent-同时工作时发生什么></a>多个 Agent 同时工作时发生什么</h2><p>一个 JVM 可以注册多个 <code>ClassFileTransformer</code>。类加载或重转换时,它们会按照 JVM 规定的顺序依次收到字节码,后面的 Transformer 看到的可能已经是前面修改过的结果。<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>原始 class</span><br><span class=line> |</span><br><span class=line>APM Transformer</span><br><span class=line> |</span><br><span class=line>覆盖率 Transformer</span><br><span class=line> |</span><br><span class=line>Arthas Transformer</span><br><span class=line> |</span><br><span class=line>最终 class</span><br></pre></table></figure><p>如果某个工具:<ul><li>生成了不合法的 frame;<li>没有保留其他工具插入的指令;<li>重复增强同一位置;<li>对异常表和构造函数处理不兼容;</ul><p>就可能出现 <code>VerifyError</code>、<code>ClassFormatError</code>、方法过大,或者某个 Agent 的增强被另一个覆盖。<p>因此线上已经运行 SkyWalking、Pinpoint、JaCoCo 或安全 Agent 时,使用 Arthas 增强命令要更加谨慎。先精确匹配一个方法、限制命中次数,再观察是否存在兼容性问题。<h2 id=字节码增强的性能开销在哪里><a class=headerlink href=#字节码增强的性能开销在哪里 title=字节码增强的性能开销在哪里></a>字节码增强的性能开销在哪里</h2><p>“没有重启应用”不等于“完全没有运行影响”。主要开销可以分成三部分。<h3 id=重转换本身的开销><a class=headerlink href=#重转换本身的开销 title=重转换本身的开销></a>重转换本身的开销</h3><p>类被 retransform 后,JVM 之前为它生成的 JIT 编译代码可能失效,相关方法需要重新解释执行并再次编译。因此刚增强或刚 reset 后,可能看到 JIT 编译线程活跃,方法性能也可能短暂波动。<h3 id=每次调用探针的开销><a class=headerlink href=#每次调用探针的开销 title=每次调用探针的开销></a>每次调用探针的开销</h3><p>入口、出口和内部调用都会多执行一些指令。<code>trace</code> 的粒度比只观察一次出口的 <code>watch</code> 更细,通常也会有更多事件。<h3 id=输出和表达式计算的开销><a class=headerlink href=#输出和表达式计算的开销 title=输出和表达式计算的开销></a>输出和表达式计算的开销</h3><p>把一个很大的对象用 <code>-x 5</code> 展开、执行复杂 OGNL 表达式、对高频方法持续输出,开销往往比那几条入口探针本身更大。<p>所以线上使用时建议:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>精确类名 > 大范围通配符</span><br><span class=line>精确方法名 > 整个类所有方法</span><br><span class=line>带 -n 次数限制 > 一直运行</span><br><span class=line>较小的 -x 展开深度 > 深度遍历对象图</span><br><span class=line>带条件表达式 > 输出所有请求</span><br></pre></table></figure><p>例如只观察慢于 100 毫秒的调用:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'{params,returnObj,throwExp}'</span> <span class=string>'#cost > 100'</span> -x 2 -n 5</span><br></pre></table></figure><h2 id=把整条链路再走一遍><a class=headerlink href=#把整条链路再走一遍 title=把整条链路再走一遍></a>把整条链路再走一遍</h2><p>现在回到最开始的问题:不修改源码,怎样观察 <code>placeOrder()</code>?<h3 id=使用-ASM><a title="使用 ASM" class=headerlink href=#使用-ASM></a>使用 ASM</h3><p>我们自己完成:<ol><li>Agent 注册 Transformer;<li>匹配类名、方法名和 descriptor;<li>找到方法入口和每一个退出指令;<li>管理局部变量和操作数栈;<li>生成新的 class 字节数组。</ol><h3 id=使用-ByteKit><a title="使用 ByteKit" class=headerlink href=#使用-ByteKit></a>使用 ByteKit</h3><p>仍然需要 Agent 和 Transformer,但注入逻辑变成:<figure class="highlight java"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=meta>@AtEnter</span> + <span class=meta>@Binding</span>.Args</span><br><span class=line><span class=meta>@AtExit</span> + <span class=meta>@Binding</span>.Return</span><br><span class=line><span class=meta>@AtExceptionExit</span> + <span class=meta>@Binding</span>.Throwable</span><br></pre></table></figure><p>ByteKit 再把这些声明转换为 ASM 指令。<h3 id=使用-Arthas><a title="使用 Arthas" class=headerlink href=#使用-Arthas></a>使用 Arthas</h3><p>只需要输入:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>watch ...</span><br><span class=line>trace ...</span><br></pre></table></figure><p>Arthas 负责动态 Attach、类和方法匹配、ByteKit 插桩、listener 管理、结果展示以及恢复。<p>所以三者最准确的关系并不是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ASM vs ByteKit vs Arthas</span><br></pre></table></figure><p>而是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>Arthas</span><br><span class=line> 使用 ByteKit 表达诊断插桩</span><br><span class=line> 使用 ASM 操作字节码</span><br><span class=line> 通过 Instrumentation 交给 JVM</span><br></pre></table></figure><p>理解到这一层以后,再看 <code>watch</code> 输出里的 <code>AtExit</code>、<code>trace</code> 的内部调用树,以及 Agent 里的 <code>ClassFileTransformer</code>,它们就不再是几个零散的名词,而是同一条执行链路上的不同环节。<h2 id=完整工程源码><a class=headerlink href=#完整工程源码 title=完整工程源码></a>完整工程源码</h2><p>前面为了讲清原理,只截取了每个环节最关键的代码。下面通过 Hexo 的 <code>include_code</code> 标签直接加载本地工程文件,博客页面会把它们渲染成完整的代码块。<p>工程结构如下:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br></pre><td class=code><pre><span class=line>asm-bytekit-arthas-demo/</span><br><span class=line>├── pom.xml</span><br><span class=line>├── run.sh</span><br><span class=line>├── arthas-commands.txt</span><br><span class=line>└── src/main/java/com/nicksxs/bytecode/demo/</span><br><span class=line> ├── app/</span><br><span class=line> │ ├── DemoApplication.java</span><br><span class=line> │ └── DemoService.java</span><br><span class=line> ├── asm/</span><br><span class=line> │ ├── AsmAgent.java</span><br><span class=line> │ └── AsmTimingTransformer.java</span><br><span class=line> └── bytekit/</span><br><span class=line> ├── ByteKitAgent.java</span><br><span class=line> ├── ByteKitTransformer.java</span><br><span class=line> └── DemoInterceptor.java</span><br></pre></table></figure><h3 id=Maven-配置><a title="Maven 配置" class=headerlink href=#Maven-配置></a>Maven 配置</h3><p><code>pom.xml</code> 中除了 ASM 和 ByteKit 依赖,还通过 <code>maven-jar-plugin</code> 分别生成两个带不同 <code>Premain-Class</code> 的 Agent JAR。<figure class="highlight xml"><figcaption><span>pom.xml</span><a href=/code/asm-bytekit-arthas-demo/pom.xml>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br><span class=line>47</span><br><span class=line>48</span><br><span class=line>49</span><br><span class=line>50</span><br><span class=line>51</span><br><span class=line>52</span><br><span class=line>53</span><br><span class=line>54</span><br><span class=line>55</span><br><span class=line>56</span><br><span class=line>57</span><br><span class=line>58</span><br><span class=line>59</span><br><span class=line>60</span><br><span class=line>61</span><br><span class=line>62</span><br><span class=line>63</span><br><span class=line>64</span><br><span class=line>65</span><br><span class=line>66</span><br><span class=line>67</span><br><span class=line>68</span><br><span class=line>69</span><br><span class=line>70</span><br><span class=line>71</span><br><span class=line>72</span><br><span class=line>73</span><br><span class=line>74</span><br><span class=line>75</span><br><span class=line>76</span><br><span class=line>77</span><br><span class=line>78</span><br><span class=line>79</span><br><span class=line>80</span><br><span class=line>81</span><br><span class=line>82</span><br><span class=line>83</span><br><span class=line>84</span><br><span class=line>85</span><br><span class=line>86</span><br><span class=line>87</span><br><span class=line>88</span><br><span class=line>89</span><br><span class=line>90</span><br><span class=line>91</span><br><span class=line>92</span><br><span class=line>93</span><br><span class=line>94</span><br><span class=line>95</span><br><span class=line>96</span><br><span class=line>97</span><br><span class=line>98</span><br><span class=line>99</span><br></pre><td class=code><pre><span class=line><span class=meta>&LT?xml version=<span class=string>"1.0"</span> encoding=<span class=string>"UTF-8"</span>?></span></span><br><span class=line><span class=tag><<span class=name>project</span> <span class=attr>xmlns</span>=<span class=string>"http://maven.apache.org/POM/4.0.0"</span></span></span><br><span class=line><span class=tag> <span class=attr>xmlns:xsi</span>=<span class=string>"http://www.w3.org/2001/XMLSchema-instance"</span></span></span><br><span class=line><span class=tag> <span class=attr>xsi:schemaLocation</span>=<span class=string>"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"</span>></span></span><br><span class=line> <span class=tag><<span class=name>modelVersion</span>></span>4.0.0<span class=tag>&LT/<span class=name>modelVersion</span>></span></span><br><span class=line></span><br><span class=line> <span class=tag><<span class=name>groupId</span>></span>com.nicksxs<span class=tag>&LT/<span class=name>groupId</span>></span></span><br><span class=line> <span class=tag><<span class=name>artifactId</span>></span>asm-bytekit-arthas-demo<span class=tag>&LT/<span class=name>artifactId</span>></span></span><br><span class=line> <span class=tag><<span class=name>version</span>></span>1.0.0<span class=tag>&LT/<span class=name>version</span>></span></span><br><span class=line></span><br><span class=line> <span class=tag><<span class=name>properties</span>></span></span><br><span class=line> <span class=tag><<span class=name>project.build.sourceEncoding</span>></span>UTF-8<span class=tag>&LT/<span class=name>project.build.sourceEncoding</span>></span></span><br><span class=line> <span class=tag><<span class=name>maven.compiler.source</span>></span>8<span class=tag>&LT/<span class=name>maven.compiler.source</span>></span></span><br><span class=line> <span class=tag><<span class=name>maven.compiler.target</span>></span>8<span class=tag>&LT/<span class=name>maven.compiler.target</span>></span></span><br><span class=line> <span class=tag><<span class=name>asm.version</span>></span>9.9.1<span class=tag>&LT/<span class=name>asm.version</span>></span></span><br><span class=line> <span class=tag><<span class=name>bytekit.version</span>></span>0.1.7<span class=tag>&LT/<span class=name>bytekit.version</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>properties</span>></span></span><br><span class=line></span><br><span class=line> <span class=tag><<span class=name>dependencies</span>></span></span><br><span class=line> <span class=tag><<span class=name>dependency</span>></span></span><br><span class=line> <span class=tag><<span class=name>groupId</span>></span>org.ow2.asm<span class=tag>&LT/<span class=name>groupId</span>></span></span><br><span class=line> <span class=tag><<span class=name>artifactId</span>></span>asm<span class=tag>&LT/<span class=name>artifactId</span>></span></span><br><span class=line> <span class=tag><<span class=name>version</span>></span>${asm.version}<span class=tag>&LT/<span class=name>version</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>dependency</span>></span></span><br><span class=line> <span class=tag><<span class=name>dependency</span>></span></span><br><span class=line> <span class=tag><<span class=name>groupId</span>></span>org.ow2.asm<span class=tag>&LT/<span class=name>groupId</span>></span></span><br><span class=line> <span class=tag><<span class=name>artifactId</span>></span>asm-commons<span class=tag>&LT/<span class=name>artifactId</span>></span></span><br><span class=line> <span class=tag><<span class=name>version</span>></span>${asm.version}<span class=tag>&LT/<span class=name>version</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>dependency</span>></span></span><br><span class=line> <span class=tag><<span class=name>dependency</span>></span></span><br><span class=line> <span class=tag><<span class=name>groupId</span>></span>com.alibaba<span class=tag>&LT/<span class=name>groupId</span>></span></span><br><span class=line> <span class=tag><<span class=name>artifactId</span>></span>bytekit-core<span class=tag>&LT/<span class=name>artifactId</span>></span></span><br><span class=line> <span class=tag><<span class=name>version</span>></span>${bytekit.version}<span class=tag>&LT/<span class=name>version</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>dependency</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>dependencies</span>></span></span><br><span class=line></span><br><span class=line> <span class=tag><<span class=name>build</span>></span></span><br><span class=line> <span class=tag><<span class=name>plugins</span>></span></span><br><span class=line> <span class=tag><<span class=name>plugin</span>></span></span><br><span class=line> <span class=tag><<span class=name>groupId</span>></span>org.apache.maven.plugins<span class=tag>&LT/<span class=name>groupId</span>></span></span><br><span class=line> <span class=tag><<span class=name>artifactId</span>></span>maven-compiler-plugin<span class=tag>&LT/<span class=name>artifactId</span>></span></span><br><span class=line> <span class=tag><<span class=name>version</span>></span>3.14.1<span class=tag>&LT/<span class=name>version</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>plugin</span>></span></span><br><span class=line> <span class=tag><<span class=name>plugin</span>></span></span><br><span class=line> <span class=tag><<span class=name>groupId</span>></span>org.apache.maven.plugins<span class=tag>&LT/<span class=name>groupId</span>></span></span><br><span class=line> <span class=tag><<span class=name>artifactId</span>></span>maven-jar-plugin<span class=tag>&LT/<span class=name>artifactId</span>></span></span><br><span class=line> <span class=tag><<span class=name>version</span>></span>3.4.2<span class=tag>&LT/<span class=name>version</span>></span></span><br><span class=line> <span class=tag><<span class=name>executions</span>></span></span><br><span class=line> <span class=tag><<span class=name>execution</span>></span></span><br><span class=line> <span class=tag><<span class=name>id</span>></span>asm-agent<span class=tag>&LT/<span class=name>id</span>></span></span><br><span class=line> <span class=tag><<span class=name>phase</span>></span>package<span class=tag>&LT/<span class=name>phase</span>></span></span><br><span class=line> <span class=tag><<span class=name>goals</span>></span></span><br><span class=line> <span class=tag><<span class=name>goal</span>></span>jar<span class=tag>&LT/<span class=name>goal</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>goals</span>></span></span><br><span class=line> <span class=tag><<span class=name>configuration</span>></span></span><br><span class=line> <span class=tag><<span class=name>classifier</span>></span>asm-agent<span class=tag>&LT/<span class=name>classifier</span>></span></span><br><span class=line> <span class=tag><<span class=name>archive</span>></span></span><br><span class=line> <span class=tag><<span class=name>manifestEntries</span>></span></span><br><span class=line> <span class=tag><<span class=name>Premain-Class</span>></span>com.nicksxs.bytecode.demo.asm.AsmAgent<span class=tag>&LT/<span class=name>Premain-Class</span>></span></span><br><span class=line> <span class=tag><<span class=name>Can-Retransform-Classes</span>></span>true<span class=tag>&LT/<span class=name>Can-Retransform-Classes</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>manifestEntries</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>archive</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>configuration</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>execution</span>></span></span><br><span class=line> <span class=tag><<span class=name>execution</span>></span></span><br><span class=line> <span class=tag><<span class=name>id</span>></span>bytekit-agent<span class=tag>&LT/<span class=name>id</span>></span></span><br><span class=line> <span class=tag><<span class=name>phase</span>></span>package<span class=tag>&LT/<span class=name>phase</span>></span></span><br><span class=line> <span class=tag><<span class=name>goals</span>></span></span><br><span class=line> <span class=tag><<span class=name>goal</span>></span>jar<span class=tag>&LT/<span class=name>goal</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>goals</span>></span></span><br><span class=line> <span class=tag><<span class=name>configuration</span>></span></span><br><span class=line> <span class=tag><<span class=name>classifier</span>></span>bytekit-agent<span class=tag>&LT/<span class=name>classifier</span>></span></span><br><span class=line> <span class=tag><<span class=name>archive</span>></span></span><br><span class=line> <span class=tag><<span class=name>manifestEntries</span>></span></span><br><span class=line> <span class=tag><<span class=name>Premain-Class</span>></span>com.nicksxs.bytecode.demo.bytekit.ByteKitAgent<span class=tag>&LT/<span class=name>Premain-Class</span>></span></span><br><span class=line> <span class=tag><<span class=name>Can-Retransform-Classes</span>></span>true<span class=tag>&LT/<span class=name>Can-Retransform-Classes</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>manifestEntries</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>archive</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>configuration</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>execution</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>executions</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>plugin</span>></span></span><br><span class=line> <span class=tag><<span class=name>plugin</span>></span></span><br><span class=line> <span class=tag><<span class=name>groupId</span>></span>org.apache.maven.plugins<span class=tag>&LT/<span class=name>groupId</span>></span></span><br><span class=line> <span class=tag><<span class=name>artifactId</span>></span>maven-dependency-plugin<span class=tag>&LT/<span class=name>artifactId</span>></span></span><br><span class=line> <span class=tag><<span class=name>version</span>></span>3.8.1<span class=tag>&LT/<span class=name>version</span>></span></span><br><span class=line> <span class=tag><<span class=name>executions</span>></span></span><br><span class=line> <span class=tag><<span class=name>execution</span>></span></span><br><span class=line> <span class=tag><<span class=name>id</span>></span>copy-runtime-dependencies<span class=tag>&LT/<span class=name>id</span>></span></span><br><span class=line> <span class=tag><<span class=name>phase</span>></span>package<span class=tag>&LT/<span class=name>phase</span>></span></span><br><span class=line> <span class=tag><<span class=name>goals</span>></span></span><br><span class=line> <span class=tag><<span class=name>goal</span>></span>copy-dependencies<span class=tag>&LT/<span class=name>goal</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>goals</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>execution</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>executions</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>plugin</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>plugins</span>></span></span><br><span class=line> <span class=tag>&LT/<span class=name>build</span>></span></span><br><span class=line><span class=tag>&LT/<span class=name>project</span>></span></span><br></pre></table></figure><h3 id=示例应用><a class=headerlink href=#示例应用 title=示例应用></a>示例应用</h3><p><code>DemoApplication</code> 会循环调用 <code>DemoService.placeOrder()</code>。每四次调用构造一次异常,方便同时验证正常返回和异常退出。<figure class="highlight java"><figcaption><span>DemoApplication.java</span><a href=/code/asm-bytekit-arthas-demo/src/main/java/com/nicksxs/bytecode/demo/app/DemoApplication.java>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br></pre><td class=code><pre><span class=line><span class=keyword>package</span> com.nicksxs.bytecode.demo.app;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> java.lang.management.ManagementFactory;</span><br><span class=line></span><br><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">DemoApplication</span> {</span><br><span class=line></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">main</span><span class=params>(String[] args)</span> <span class=keyword>throws</span> Exception {</span><br><span class=line> <span class=type>int</span> <span class=variable>iterations</span> <span class=operator>=</span> args.length > <span class=number>0</span> ? Integer.parseInt(args[<span class=number>0</span>]) : <span class=number>5</span>;</span><br><span class=line> <span class=type>long</span> <span class=variable>pauseMillis</span> <span class=operator>=</span> args.length > <span class=number>1</span> ? Long.parseLong(args[<span class=number>1</span>]) : <span class=number>500L</span>;</span><br><span class=line></span><br><span class=line> System.out.println(<span class=string>"demo pid="</span> + currentPid());</span><br><span class=line> System.out.println(<span class=string>"iterations="</span> + (iterations == <span class=number>0</span> ? <span class=string>"infinite"</span> : iterations));</span><br><span class=line></span><br><span class=line> <span class=type>DemoService</span> <span class=variable>service</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">DemoService</span>();</span><br><span class=line> <span class=type>int</span> <span class=variable>index</span> <span class=operator>=</span> <span class=number>1</span>;</span><br><span class=line> <span class=keyword>while</span> (iterations == <span class=number>0</span> || index <= iterations) {</span><br><span class=line> <span class=type>String</span> <span class=variable>user</span> <span class=operator>=</span> index % <span class=number>4</span> == <span class=number>0</span> ? <span class=string>"error"</span> : <span class=string>"user-"</span> + index;</span><br><span class=line> <span class=keyword>try</span> {</span><br><span class=line> <span class=type>String</span> <span class=variable>result</span> <span class=operator>=</span> service.placeOrder(user, index);</span><br><span class=line> System.out.println(<span class=string>"[APP] "</span> + result);</span><br><span class=line> } <span class=keyword>catch</span> (Exception e) {</span><br><span class=line> System.out.println(<span class=string>"[APP] failed: "</span> + e.getMessage());</span><br><span class=line> }</span><br><span class=line> index++;</span><br><span class=line> Thread.sleep(pauseMillis);</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> String <span class="title function_">currentPid</span><span class=params>()</span> {</span><br><span class=line> <span class=type>String</span> <span class=variable>runtimeName</span> <span class=operator>=</span> ManagementFactory.getRuntimeMXBean().getName();</span><br><span class=line> <span class=type>int</span> <span class=variable>separator</span> <span class=operator>=</span> runtimeName.indexOf(<span class=string>'@'</span>);</span><br><span class=line> <span class=keyword>return</span> separator > <span class=number>0</span> ? runtimeName.substring(<span class=number>0</span>, separator) : runtimeName;</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><figure class="highlight java"><figcaption><span>DemoService.java</span><a href=/code/asm-bytekit-arthas-demo/src/main/java/com/nicksxs/bytecode/demo/app/DemoService.java>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br></pre><td class=code><pre><span class=line><span class=keyword>package</span> com.nicksxs.bytecode.demo.app;</span><br><span class=line></span><br><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">DemoService</span> {</span><br><span class=line></span><br><span class=line> <span class=keyword>public</span> String <span class="title function_">placeOrder</span><span class=params>(String user, <span class=type>int</span> quantity)</span> {</span><br><span class=line> <span class=keyword>if</span> (<span class=string>"error"</span>.equals(user)) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">IllegalArgumentException</span>(<span class=string>"user cannot be error"</span>);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> validateQuantity(quantity);</span><br><span class=line> <span class=type>String</span> <span class=variable>sku</span> <span class=operator>=</span> queryInventory(user);</span><br><span class=line> <span class=keyword>return</span> saveOrder(user, sku, quantity);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=keyword>void</span> <span class="title function_">validateQuantity</span><span class=params>(<span class=type>int</span> quantity)</span> {</span><br><span class=line> sleep(<span class=number>20L</span>);</span><br><span class=line> <span class=keyword>if</span> (quantity <= <span class=number>0</span>) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">IllegalArgumentException</span>(<span class=string>"quantity must be positive"</span>);</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> String <span class="title function_">queryInventory</span><span class=params>(String user)</span> {</span><br><span class=line> sleep(<span class=number>40L</span>);</span><br><span class=line> <span class=keyword>return</span> <span class=string>"SKU-"</span> + user.toUpperCase();</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> String <span class="title function_">saveOrder</span><span class=params>(String user, String sku, <span class=type>int</span> quantity)</span> {</span><br><span class=line> sleep(<span class=number>30L</span>);</span><br><span class=line> <span class=keyword>return</span> <span class=string>"order created: user="</span> + user + <span class=string>", sku="</span> + sku + <span class=string>", quantity="</span> + quantity;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=keyword>void</span> <span class="title function_">sleep</span><span class=params>(<span class=type>long</span> millis)</span> {</span><br><span class=line> <span class=keyword>try</span> {</span><br><span class=line> Thread.sleep(millis);</span><br><span class=line> } <span class=keyword>catch</span> (InterruptedException e) {</span><br><span class=line> Thread.currentThread().interrupt();</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">IllegalStateException</span>(<span class=string>"interrupted"</span>, e);</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><h3 id=ASM-Agent><a title="ASM Agent" class=headerlink href=#ASM-Agent></a>ASM Agent</h3><p>Agent 入口只负责注册 Transformer,具体的类匹配和指令生成都在 <code>AsmTimingTransformer</code> 中。<figure class="highlight java"><figcaption><span>AsmAgent.java</span><a href=/code/asm-bytekit-arthas-demo/src/main/java/com/nicksxs/bytecode/demo/asm/AsmAgent.java>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line><span class=keyword>package</span> com.nicksxs.bytecode.demo.asm;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> java.lang.instrument.Instrumentation;</span><br><span class=line></span><br><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">AsmAgent</span> {</span><br><span class=line></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">premain</span><span class=params>(String agentArgs, Instrumentation instrumentation)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ASM agent] installed"</span>);</span><br><span class=line> instrumentation.addTransformer(<span class=keyword>new</span> <span class="title class_">AsmTimingTransformer</span>(), <span class=literal>true</span>);</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><figure class="highlight java"><figcaption><span>AsmTimingTransformer.java</span><a href=/code/asm-bytekit-arthas-demo/src/main/java/com/nicksxs/bytecode/demo/asm/AsmTimingTransformer.java>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br><span class=line>47</span><br><span class=line>48</span><br><span class=line>49</span><br><span class=line>50</span><br><span class=line>51</span><br><span class=line>52</span><br><span class=line>53</span><br><span class=line>54</span><br><span class=line>55</span><br><span class=line>56</span><br><span class=line>57</span><br><span class=line>58</span><br><span class=line>59</span><br><span class=line>60</span><br><span class=line>61</span><br><span class=line>62</span><br><span class=line>63</span><br><span class=line>64</span><br><span class=line>65</span><br><span class=line>66</span><br><span class=line>67</span><br><span class=line>68</span><br><span class=line>69</span><br><span class=line>70</span><br><span class=line>71</span><br><span class=line>72</span><br><span class=line>73</span><br><span class=line>74</span><br><span class=line>75</span><br><span class=line>76</span><br><span class=line>77</span><br><span class=line>78</span><br><span class=line>79</span><br><span class=line>80</span><br><span class=line>81</span><br><span class=line>82</span><br><span class=line>83</span><br><span class=line>84</span><br><span class=line>85</span><br><span class=line>86</span><br><span class=line>87</span><br><span class=line>88</span><br><span class=line>89</span><br><span class=line>90</span><br><span class=line>91</span><br><span class=line>92</span><br><span class=line>93</span><br><span class=line>94</span><br><span class=line>95</span><br><span class=line>96</span><br><span class=line>97</span><br><span class=line>98</span><br><span class=line>99</span><br><span class=line>100</span><br><span class=line>101</span><br><span class=line>102</span><br><span class=line>103</span><br><span class=line>104</span><br><span class=line>105</span><br><span class=line>106</span><br><span class=line>107</span><br><span class=line>108</span><br><span class=line>109</span><br><span class=line>110</span><br><span class=line>111</span><br><span class=line>112</span><br><span class=line>113</span><br><span class=line>114</span><br><span class=line>115</span><br></pre><td class=code><pre><span class=line><span class=keyword>package</span> com.nicksxs.bytecode.demo.asm;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> java.io.PrintStream;</span><br><span class=line><span class=keyword>import</span> java.lang.instrument.ClassFileTransformer;</span><br><span class=line><span class=keyword>import</span> java.security.ProtectionDomain;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> org.objectweb.asm.ClassReader;</span><br><span class=line><span class=keyword>import</span> org.objectweb.asm.ClassVisitor;</span><br><span class=line><span class=keyword>import</span> org.objectweb.asm.ClassWriter;</span><br><span class=line><span class=keyword>import</span> org.objectweb.asm.MethodVisitor;</span><br><span class=line><span class=keyword>import</span> org.objectweb.asm.Opcodes;</span><br><span class=line><span class=keyword>import</span> org.objectweb.asm.Type;</span><br><span class=line><span class=keyword>import</span> org.objectweb.asm.commons.AdviceAdapter;</span><br><span class=line><span class=keyword>import</span> org.objectweb.asm.commons.Method;</span><br><span class=line></span><br><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">AsmTimingTransformer</span> <span class=keyword>implements</span> <span class="title class_">ClassFileTransformer</span> {</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_CLASS</span> <span class=operator>=</span> <span class=string>"com/nicksxs/bytecode/demo/app/DemoService"</span>;</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_METHOD</span> <span class=operator>=</span> <span class=string>"placeOrder"</span>;</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_DESCRIPTOR</span> <span class=operator>=</span> <span class=string>"(Ljava/lang/String;I)Ljava/lang/String;"</span>;</span><br><span class=line></span><br><span class=line> <span class=meta>@Override</span></span><br><span class=line> <span class=keyword>public</span> <span class=type>byte</span>[] transform(ClassLoader loader, String className, Class&LT?> classBeingRedefined,</span><br><span class=line> ProtectionDomain protectionDomain, <span class=type>byte</span>[] classfileBuffer) {</span><br><span class=line> <span class=keyword>if</span> (!TARGET_CLASS.equals(className)) {</span><br><span class=line> <span class=keyword>return</span> <span class=literal>null</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> System.out.println(<span class=string>"[ASM agent] transforming "</span> + className);</span><br><span class=line> <span class=type>ClassReader</span> <span class=variable>reader</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassReader</span>(classfileBuffer);</span><br><span class=line> <span class=type>ClassWriter</span> <span class=variable>writer</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassWriter</span>(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);</span><br><span class=line> <span class=type>ClassVisitor</span> <span class=variable>visitor</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassVisitor</span>(Opcodes.ASM9, writer) {</span><br><span class=line> <span class=meta>@Override</span></span><br><span class=line> <span class=keyword>public</span> MethodVisitor <span class="title function_">visitMethod</span><span class=params>(<span class=type>int</span> access, String name, String descriptor, String signature,</span></span><br><span class=line><span class=params> String[] exceptions)</span> {</span><br><span class=line> <span class=type>MethodVisitor</span> <span class=variable>methodVisitor</span> <span class=operator>=</span> <span class=built_in>super</span>.visitMethod(access, name, descriptor, signature, exceptions);</span><br><span class=line> <span class=keyword>if</span> (!TARGET_METHOD.equals(name) || !TARGET_DESCRIPTOR.equals(descriptor)) {</span><br><span class=line> <span class=keyword>return</span> methodVisitor;</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> <span class=keyword>new</span> <span class="title class_">TimingAdvice</span>(methodVisitor, access, name, descriptor);</span><br><span class=line> }</span><br><span class=line> };</span><br><span class=line> reader.accept(visitor, ClassReader.EXPAND_FRAMES);</span><br><span class=line> <span class=keyword>return</span> writer.toByteArray();</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=keyword>class</span> <span class="title class_">TimingAdvice</span> <span class=keyword>extends</span> <span class="title class_">AdviceAdapter</span> {</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Type</span> <span class=variable>SYSTEM_TYPE</span> <span class=operator>=</span> Type.getType(System.class);</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Type</span> <span class=variable>PRINT_STREAM_TYPE</span> <span class=operator>=</span> Type.getType(PrintStream.class);</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Type</span> <span class=variable>STRING_BUILDER_TYPE</span> <span class=operator>=</span> Type.getType(StringBuilder.class);</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Type</span> <span class=variable>STRING_TYPE</span> <span class=operator>=</span> Type.getType(String.class);</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Method</span> <span class=variable>NANO_TIME</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">Method</span>(<span class=string>"nanoTime"</span>, Type.LONG_TYPE, <span class=keyword>new</span> <span class="title class_">Type</span>[<span class=number>0</span>]);</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Method</span> <span class=variable>STRING_BUILDER_INIT</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">Method</span>(<span class=string>"&LTinit>"</span>, Type.VOID_TYPE, <span class=keyword>new</span> <span class="title class_">Type</span>[<span class=number>0</span>]);</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Method</span> <span class=variable>APPEND_STRING</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">Method</span>(<span class=string>"append"</span>, STRING_BUILDER_TYPE,</span><br><span class=line> <span class=keyword>new</span> <span class="title class_">Type</span>[] { STRING_TYPE });</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Method</span> <span class=variable>APPEND_INT</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">Method</span>(<span class=string>"append"</span>, STRING_BUILDER_TYPE,</span><br><span class=line> <span class=keyword>new</span> <span class="title class_">Type</span>[] { Type.INT_TYPE });</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Method</span> <span class=variable>APPEND_LONG</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">Method</span>(<span class=string>"append"</span>, STRING_BUILDER_TYPE,</span><br><span class=line> <span class=keyword>new</span> <span class="title class_">Type</span>[] { Type.LONG_TYPE });</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Method</span> <span class=variable>TO_STRING</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">Method</span>(<span class=string>"toString"</span>, STRING_TYPE, <span class=keyword>new</span> <span class="title class_">Type</span>[<span class=number>0</span>]);</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>Method</span> <span class=variable>PRINTLN</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">Method</span>(<span class=string>"println"</span>, Type.VOID_TYPE, <span class=keyword>new</span> <span class="title class_">Type</span>[] { STRING_TYPE });</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=type>int</span> startNanosLocal;</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class="title function_">TimingAdvice</span><span class=params>(MethodVisitor methodVisitor, <span class=type>int</span> access, String name, String descriptor)</span> {</span><br><span class=line> <span class=built_in>super</span>(Opcodes.ASM9, methodVisitor, access, name, descriptor);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=meta>@Override</span></span><br><span class=line> <span class=keyword>protected</span> <span class=keyword>void</span> <span class="title function_">onMethodEnter</span><span class=params>()</span> {</span><br><span class=line> invokeStatic(SYSTEM_TYPE, NANO_TIME);</span><br><span class=line> startNanosLocal = newLocal(Type.LONG_TYPE);</span><br><span class=line> storeLocal(startNanosLocal);</span><br><span class=line></span><br><span class=line> getStatic(SYSTEM_TYPE, <span class=string>"out"</span>, PRINT_STREAM_TYPE);</span><br><span class=line> newInstance(STRING_BUILDER_TYPE);</span><br><span class=line> dup();</span><br><span class=line> invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILDER_INIT);</span><br><span class=line> push(<span class=string>"[ASM] enter placeOrder user="</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> loadArg(<span class=number>0</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> push(<span class=string>", quantity="</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> loadArg(<span class=number>1</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_INT);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, TO_STRING);</span><br><span class=line> invokeVirtual(PRINT_STREAM_TYPE, PRINTLN);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=meta>@Override</span></span><br><span class=line> <span class=keyword>protected</span> <span class=keyword>void</span> <span class="title function_">onMethodExit</span><span class=params>(<span class=type>int</span> opcode)</span> {</span><br><span class=line> invokeStatic(SYSTEM_TYPE, NANO_TIME);</span><br><span class=line> loadLocal(startNanosLocal);</span><br><span class=line> math(SUB, Type.LONG_TYPE);</span><br><span class=line> <span class=type>int</span> <span class=variable>elapsedLocal</span> <span class=operator>=</span> newLocal(Type.LONG_TYPE);</span><br><span class=line> storeLocal(elapsedLocal);</span><br><span class=line></span><br><span class=line> getStatic(SYSTEM_TYPE, <span class=string>"out"</span>, PRINT_STREAM_TYPE);</span><br><span class=line> newInstance(STRING_BUILDER_TYPE);</span><br><span class=line> dup();</span><br><span class=line> invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILDER_INIT);</span><br><span class=line> push(opcode == ATHROW ? <span class=string>"[ASM] throw placeOrder cost="</span> : <span class=string>"[ASM] return placeOrder cost="</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> loadLocal(elapsedLocal);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_LONG);</span><br><span class=line> push(<span class=string>" ns"</span>);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, APPEND_STRING);</span><br><span class=line> invokeVirtual(STRING_BUILDER_TYPE, TO_STRING);</span><br><span class=line> invokeVirtual(PRINT_STREAM_TYPE, PRINTLN);</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><h3 id=ByteKit-Agent><a title="ByteKit Agent" class=headerlink href=#ByteKit-Agent></a>ByteKit Agent</h3><p>ByteKit 版本同样由 Agent 注册 Transformer,但插入点和运行时数据改用注解与 Binding 描述。<figure class="highlight java"><figcaption><span>ByteKitAgent.java</span><a href=/code/asm-bytekit-arthas-demo/src/main/java/com/nicksxs/bytecode/demo/bytekit/ByteKitAgent.java>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line><span class=keyword>package</span> com.nicksxs.bytecode.demo.bytekit;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> java.lang.instrument.Instrumentation;</span><br><span class=line></span><br><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">ByteKitAgent</span> {</span><br><span class=line></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">premain</span><span class=params>(String agentArgs, Instrumentation instrumentation)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit agent] installed"</span>);</span><br><span class=line> instrumentation.addTransformer(<span class=keyword>new</span> <span class="title class_">ByteKitTransformer</span>(), <span class=literal>true</span>);</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><figure class="highlight java"><figcaption><span>ByteKitTransformer.java</span><a href=/code/asm-bytekit-arthas-demo/src/main/java/com/nicksxs/bytecode/demo/bytekit/ByteKitTransformer.java>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br><span class=line>47</span><br><span class=line>48</span><br><span class=line>49</span><br></pre><td class=code><pre><span class=line><span class=keyword>package</span> com.nicksxs.bytecode.demo.bytekit;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> java.lang.instrument.ClassFileTransformer;</span><br><span class=line><span class=keyword>import</span> java.security.ProtectionDomain;</span><br><span class=line><span class=keyword>import</span> java.util.List;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> com.alibaba.bytekit.asm.MethodProcessor;</span><br><span class=line><span class=keyword>import</span> com.alibaba.bytekit.asm.interceptor.InterceptorProcessor;</span><br><span class=line><span class=keyword>import</span> com.alibaba.bytekit.asm.interceptor.parser.DefaultInterceptorClassParser;</span><br><span class=line><span class=keyword>import</span> com.alibaba.bytekit.utils.AsmUtils;</span><br><span class=line><span class=keyword>import</span> com.alibaba.deps.org.objectweb.asm.ClassReader;</span><br><span class=line><span class=keyword>import</span> com.alibaba.deps.org.objectweb.asm.tree.ClassNode;</span><br><span class=line><span class=keyword>import</span> com.alibaba.deps.org.objectweb.asm.tree.MethodNode;</span><br><span class=line></span><br><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">ByteKitTransformer</span> <span class=keyword>implements</span> <span class="title class_">ClassFileTransformer</span> {</span><br><span class=line></span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_CLASS</span> <span class=operator>=</span> <span class=string>"com/nicksxs/bytecode/demo/app/DemoService"</span>;</span><br><span class=line> <span class=keyword>private</span> <span class=keyword>static</span> <span class=keyword>final</span> <span class=type>String</span> <span class=variable>TARGET_METHOD</span> <span class=operator>=</span> <span class=string>"placeOrder"</span>;</span><br><span class=line></span><br><span class=line> <span class=meta>@Override</span></span><br><span class=line> <span class=keyword>public</span> <span class=type>byte</span>[] transform(ClassLoader loader, String className, Class&LT?> classBeingRedefined,</span><br><span class=line> ProtectionDomain protectionDomain, <span class=type>byte</span>[] classfileBuffer) {</span><br><span class=line> <span class=keyword>if</span> (!TARGET_CLASS.equals(className)) {</span><br><span class=line> <span class=keyword>return</span> <span class=literal>null</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>try</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit agent] transforming "</span> + className);</span><br><span class=line> <span class=type>DefaultInterceptorClassParser</span> <span class=variable>parser</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">DefaultInterceptorClassParser</span>();</span><br><span class=line> List&LTInterceptorProcessor> interceptors = parser.parse(DemoInterceptor.class);</span><br><span class=line></span><br><span class=line> <span class=type>ClassNode</span> <span class=variable>classNode</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">ClassNode</span>();</span><br><span class=line> <span class=type>ClassReader</span> <span class=variable>reader</span> <span class=operator>=</span> AsmUtils.toClassNode(classfileBuffer, classNode);</span><br><span class=line> <span class=keyword>for</span> (MethodNode methodNode : classNode.methods) {</span><br><span class=line> <span class=keyword>if</span> (!TARGET_METHOD.equals(methodNode.name)) {</span><br><span class=line> <span class=keyword>continue</span>;</span><br><span class=line> }</span><br><span class=line> <span class=type>MethodProcessor</span> <span class=variable>methodProcessor</span> <span class=operator>=</span> <span class=keyword>new</span> <span class="title class_">MethodProcessor</span>(classNode, methodNode);</span><br><span class=line> <span class=keyword>for</span> (InterceptorProcessor interceptor : interceptors) {</span><br><span class=line> interceptor.process(methodProcessor);</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> AsmUtils.toBytes(classNode, loader, reader);</span><br><span class=line> } <span class=keyword>catch</span> (Throwable throwable) {</span><br><span class=line> throwable.printStackTrace(System.err);</span><br><span class=line> <span class=keyword>return</span> <span class=literal>null</span>;</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><figure class="highlight java"><figcaption><span>DemoInterceptor.java</span><a href=/code/asm-bytekit-arthas-demo/src/main/java/com/nicksxs/bytecode/demo/bytekit/DemoInterceptor.java>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br></pre><td class=code><pre><span class=line><span class=keyword>package</span> com.nicksxs.bytecode.demo.bytekit;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> java.util.Arrays;</span><br><span class=line></span><br><span class=line><span class=keyword>import</span> com.alibaba.bytekit.asm.binding.Binding;</span><br><span class=line><span class=keyword>import</span> com.alibaba.bytekit.asm.interceptor.annotation.AtEnter;</span><br><span class=line><span class=keyword>import</span> com.alibaba.bytekit.asm.interceptor.annotation.AtExceptionExit;</span><br><span class=line><span class=keyword>import</span> com.alibaba.bytekit.asm.interceptor.annotation.AtExit;</span><br><span class=line></span><br><span class=line><span class=keyword>public</span> <span class=keyword>class</span> <span class="title class_">DemoInterceptor</span> {</span><br><span class=line></span><br><span class=line> <span class=meta>@AtEnter(inline = true)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atEnter</span><span class=params>(<span class=meta>@Binding</span>.MethodName String methodName, <span class=meta>@Binding</span>.Args Object[] args)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] enter "</span> + methodName + <span class=string>" args="</span> + Arrays.toString(args));</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=meta>@AtExit(inline = true)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atExit</span><span class=params>(<span class=meta>@Binding</span>.Return Object returnObject)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] return "</span> + returnObject);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=meta>@AtExceptionExit(inline = true, onException = Throwable.class)</span></span><br><span class=line> <span class=keyword>public</span> <span class=keyword>static</span> <span class=keyword>void</span> <span class="title function_">atExceptionExit</span><span class=params>(<span class=meta>@Binding</span>.Throwable Throwable throwable)</span> {</span><br><span class=line> System.out.println(<span class=string>"[ByteKit] throw "</span> + throwable.getClass().getSimpleName()</span><br><span class=line> + <span class=string>": "</span> + throwable.getMessage());</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><h3 id=运行脚本和-Arthas-命令><a title="运行脚本和 Arthas 命令" class=headerlink href=#运行脚本和-Arthas-命令></a>运行脚本和 Arthas 命令</h3><p><code>run.sh</code> 把原始应用、ASM Agent、ByteKit Agent 和 Arthas 目标应用四种运行方式放在了一起。<figure class="highlight bash"><figcaption><span>run.sh</span><a href=/code/asm-bytekit-arthas-demo/run.sh>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br></pre><td class=code><pre><span class=line><span class=meta>#!/usr/bin/env bash</span></span><br><span class=line><span class=built_in>set</span> -euo pipefail</span><br><span class=line></span><br><span class=line>ROOT_DIR=<span class=string>"<span class=subst>$(cd <span class=string>"<span class=subst>$(dirname <span class=string>"<span class=variable>$0</span>"</span>)</span>"</span> && pwd)</span>"</span></span><br><span class=line>MODE=<span class=string>"<span class=variable>${1:-baseline}</span>"</span></span><br><span class=line>ITERATIONS=<span class=string>"<span class=variable>${2:-5}</span>"</span></span><br><span class=line>PAUSE_MILLIS=<span class=string>"<span class=variable>${3:-300}</span>"</span></span><br><span class=line>MVN_BIN=<span class=string>"<span class=variable>${MVN_BIN:-mvn}</span>"</span></span><br><span class=line>JAVA_BIN=<span class=string>"<span class=variable>${JAVA_BIN:-java}</span>"</span></span><br><span class=line></span><br><span class=line><span class=keyword>if</span> [[ ! -d <span class=string>"<span class=variable>$ROOT_DIR</span>/target/classes"</span> ]]; <span class=keyword>then</span></span><br><span class=line> <span class=keyword>if</span> ! <span class=built_in>command</span> -v <span class=string>"<span class=variable>$MVN_BIN</span>"</span> >/dev/null 2>&1; <span class=keyword>then</span></span><br><span class=line> <span class=built_in>echo</span> <span class=string>"mvn was not found. Install Maven or set MVN_BIN to its absolute path."</span> >&2</span><br><span class=line> <span class=built_in>exit</span> 1</span><br><span class=line> <span class=keyword>fi</span></span><br><span class=line> <span class=string>"<span class=variable>$MVN_BIN</span>"</span> -q -f <span class=string>"<span class=variable>$ROOT_DIR</span>/pom.xml"</span> package</span><br><span class=line><span class=keyword>fi</span></span><br><span class=line></span><br><span class=line>CLASSPATH=<span class=string>"<span class=variable>$ROOT_DIR</span>/target/classes:<span class=variable>$ROOT_DIR</span>/target/dependency/*"</span></span><br><span class=line>MAIN_CLASS=<span class=string>"com.nicksxs.bytecode.demo.app.DemoApplication"</span></span><br><span class=line>VERSION=<span class=string>"1.0.0"</span></span><br><span class=line></span><br><span class=line><span class=keyword>case</span> <span class=string>"<span class=variable>$MODE</span>"</span> <span class=keyword>in</span></span><br><span class=line> baseline)</span><br><span class=line> <span class=built_in>exec</span> <span class=string>"<span class=variable>$JAVA_BIN</span>"</span> -<span class=built_in>cp</span> <span class=string>"<span class=variable>$CLASSPATH</span>"</span> <span class=string>"<span class=variable>$MAIN_CLASS</span>"</span> <span class=string>"<span class=variable>$ITERATIONS</span>"</span> <span class=string>"<span class=variable>$PAUSE_MILLIS</span>"</span></span><br><span class=line> ;;</span><br><span class=line> asm)</span><br><span class=line> <span class=built_in>exec</span> <span class=string>"<span class=variable>$JAVA_BIN</span>"</span> \</span><br><span class=line> -javaagent:<span class=string>"<span class=variable>$ROOT_DIR</span>/target/asm-bytekit-arthas-demo-<span class=variable>$VERSION</span>-asm-agent.jar"</span> \</span><br><span class=line> -<span class=built_in>cp</span> <span class=string>"<span class=variable>$CLASSPATH</span>"</span> <span class=string>"<span class=variable>$MAIN_CLASS</span>"</span> <span class=string>"<span class=variable>$ITERATIONS</span>"</span> <span class=string>"<span class=variable>$PAUSE_MILLIS</span>"</span></span><br><span class=line> ;;</span><br><span class=line> bytekit)</span><br><span class=line> <span class=built_in>exec</span> <span class=string>"<span class=variable>$JAVA_BIN</span>"</span> \</span><br><span class=line> -javaagent:<span class=string>"<span class=variable>$ROOT_DIR</span>/target/asm-bytekit-arthas-demo-<span class=variable>$VERSION</span>-bytekit-agent.jar"</span> \</span><br><span class=line> -<span class=built_in>cp</span> <span class=string>"<span class=variable>$CLASSPATH</span>"</span> <span class=string>"<span class=variable>$MAIN_CLASS</span>"</span> <span class=string>"<span class=variable>$ITERATIONS</span>"</span> <span class=string>"<span class=variable>$PAUSE_MILLIS</span>"</span></span><br><span class=line> ;;</span><br><span class=line> app)</span><br><span class=line> <span class=built_in>exec</span> <span class=string>"<span class=variable>$JAVA_BIN</span>"</span> -<span class=built_in>cp</span> <span class=string>"<span class=variable>$CLASSPATH</span>"</span> <span class=string>"<span class=variable>$MAIN_CLASS</span>"</span> 0 1000</span><br><span class=line> ;;</span><br><span class=line> *)</span><br><span class=line> <span class=built_in>echo</span> <span class=string>"usage: <span class=variable>$0</span> {baseline|asm|bytekit|app} [iterations] [pauseMillis]"</span> >&2</span><br><span class=line> <span class=built_in>exit</span> 1</span><br><span class=line> ;;</span><br><span class=line><span class=keyword>esac</span></span><br></pre></table></figure><figure class="highlight bash"><figcaption><span>arthas-commands.txt</span><a href=/code/asm-bytekit-arthas-demo/arthas-commands.txt>view raw</a></figcaption><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br></pre><td class=code><pre><span class=line><span class=comment># Run ./run.sh app first, then attach Arthas to DemoApplication.</span></span><br><span class=line></span><br><span class=line>sc -d com.nicksxs.bytecode.demo.app.DemoService</span><br><span class=line>sm com.nicksxs.bytecode.demo.app.DemoService</span><br><span class=line>watch com.nicksxs.bytecode.demo.app.DemoService placeOrder <span class=string>'{params,returnObj,throwExp}'</span> -x 2 -n 4</span><br><span class=line>trace com.nicksxs.bytecode.demo.app.DemoService placeOrder <span class=string>'#cost > 0'</span> -n 3</span><br><span class=line>jad com.nicksxs.bytecode.demo.app.DemoService placeOrder</span><br><span class=line>reset com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><h2 id=完整验证清单><a class=headerlink href=#完整验证清单 title=完整验证清单></a>完整验证清单</h2><p>示例目录下依次执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br></pre><td class=code><pre><span class=line><span class=built_in>cd</span> <span class=built_in>source</span>/code/asm-bytekit-arthas-demo</span><br><span class=line></span><br><span class=line><span class=comment># 编译应用和两个 Agent</span></span><br><span class=line>mvn clean package</span><br><span class=line></span><br><span class=line><span class=comment># 1. 原始应用</span></span><br><span class=line>./run.sh baseline 4 10</span><br><span class=line></span><br><span class=line><span class=comment># 2. ASM 增强</span></span><br><span class=line>./run.sh asm 4 10</span><br><span class=line></span><br><span class=line><span class=comment># 3. ByteKit 增强</span></span><br><span class=line>./run.sh bytekit 4 10</span><br><span class=line></span><br><span class=line><span class=comment># 4. 为 Arthas 保持应用运行</span></span><br><span class=line>./run.sh app</span><br></pre></table></figure><p>另一个终端中:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>curl -O https://arthas.aliyun.com/arthas-boot.jar</span><br><span class=line>java -jar arthas-boot.jar --use-version 4.3.2</span><br></pre></table></figure><p>挂载后依次执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line>sc -d com.nicksxs.bytecode.demo.app.DemoService</span><br><span class=line></span><br><span class=line>watch com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'{params,returnObj,throwExp}'</span> -x 2 -n 4</span><br><span class=line></span><br><span class=line>trace com.nicksxs.bytecode.demo.app.DemoService placeOrder \</span><br><span class=line> <span class=string>'#cost > 0'</span> -n 3</span><br><span class=line></span><br><span class=line>jad com.nicksxs.bytecode.demo.app.DemoService placeOrder</span><br><span class=line></span><br><span class=line>reset com.nicksxs.bytecode.demo.app.DemoService</span><br></pre></table></figure><p>到这里,ASM、ByteKit 和 Arthas 的三个层次就都不只是概念,而是可以在同一个业务方法上亲手验证的结果了。<h2 id=参考资料><a class=headerlink href=#参考资料 title=参考资料></a>参考资料</h2><ul><li><a href=https://asm.ow2.io/ rel=noopener target=_blank>ASM 官方文档</a><li><a href=https://asm.ow2.io/versions.html rel=noopener target=_blank>ASM 版本记录</a><li><a href=https://github.com/alibaba/bytekit rel=noopener target=_blank>ByteKit 源码与说明</a><li><a href=https://github.com/alibaba/bytekit/blob/master/bytekit-core/src/test/java/com/example/ByteKitDemo.java rel=noopener target=_blank>ByteKit 官方示例 ByteKitDemo</a><li><a href=https://github.com/alibaba/arthas rel=noopener target=_blank>Arthas 源码</a><li><a href=https://github.com/alibaba/arthas/blob/master/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java rel=noopener target=_blank>Arthas Enhancer 源码</a><li><a href=https://arthas.aliyun.com/doc/commands.html rel=noopener target=_blank>Arthas 命令列表</a><li><a href=https://arthas.aliyun.com/doc/retransform.html rel=noopener target=_blank>Arthas retransform 文档</a><li><a href=https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html rel=noopener target=_blank>Java Instrumentation API</a></ul></div><footer class=post-footer><div class=post-eof></div></footer></article></div><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/08/23/8GB%E6%98%BE%E5%AD%98%E5%A6%82%E4%BD%95%E8%B7%9135B-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3FreeToken%E7%9A%84%E4%B8%93%E5%AE%B6%E7%BC%93%E5%AD%98%E4%B8%8ECPU-GPU%E5%8D%8F%E5%90%8C/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/08/23/8GB%E6%98%BE%E5%AD%98%E5%A6%82%E4%BD%95%E8%B7%9135B-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3FreeToken%E7%9A%84%E4%B8%93%E5%AE%B6%E7%BC%93%E5%AD%98%E4%B8%8ECPU-GPU%E5%8D%8F%E5%90%8C/ itemprop=url>8GB显存如何跑35B:深入理解FreeToken的专家缓存与CPU-GPU协同</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-08-23 20:41:00 / 修改时间:22:47:18" datetime=2026-08-23T20:41:00+08:00>2026-08-23</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/LLM/ itemprop=url rel=index><span itemprop=name>LLM</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-increment=false data-path=/2026/08/23/8GB%E6%98%BE%E5%AD%98%E5%A6%82%E4%BD%95%E8%B7%9135B-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3FreeToken%E7%9A%84%E4%B8%93%E5%AE%B6%E7%BC%93%E5%AD%98%E4%B8%8ECPU-GPU%E5%8D%8F%E5%90%8C/ data-title=8GB显存如何跑35B:深入理解FreeToken的专家缓存与CPU-GPU协同 title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/08/23/8GB%E6%98%BE%E5%AD%98%E5%A6%82%E4%BD%95%E8%B7%9135B-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3FreeToken%E7%9A%84%E4%B8%93%E5%AE%B6%E7%BC%93%E5%AD%98%E4%B8%8ECPU-GPU%E5%8D%8F%E5%90%8C/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/08/23/8GB显存如何跑35B-深入理解FreeToken的专家缓存与CPU-GPU协同/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><style>@media (max-width:767px){.post-body{min-width:0;overflow-x:clip}.post-body figure.highlight{max-width:100%;overflow-x:auto}.post-body>table{display:block;max-width:100%;overflow-x:auto}}</style><p>前几天看到一个挺夸张的结果:<blockquote><p>FreeToken 在一台只有 8GB 显存的 RTX 4060 Laptop 上运行 Qwen3.6-35B-A3B,解码速度达到了 39.3 tok/s。</blockquote><p>第一反应肯定是:35B 模型只算 4bit 权重,理论上也要 17.5GB 左右,8GB 显存怎么可能放得下?<p>后来翻了下 <a href=https://arxiv.org/abs/2608.16157 rel=noopener target=_blank>FreeToken 的论文</a>、<a href=https://github.com/FlashML-org/FreeToken rel=noopener target=_blank>源码</a>,又对比了下 llama.cpp,发现这个问题里最容易混淆的是两个完全不同的概念:<ul><li>模型能不能全部放进显存<li>生成一个 token 时,到底需要计算多少参数</ul><p>FreeToken 并没有把 35B 参数凭空塞进 8GB 显存里。它做的是把完整的 MoE 专家权重留在内存,把 GPU 显存变成一个会不断换入、换出的专家缓存,再让 CPU 和 GPU 一起处理缓存没有命中的专家。<p>先把结论用一句话说出来:<blockquote><p>MoE 让“大模型每次只用一小部分参数”成为可能,FreeToken 解决的则是“这一小部分参数怎样及时出现在最合适的计算设备上”。</blockquote><p>这篇就从这里展开。<h2 id=先纠正一个容易混淆的模型><a class=headerlink href=#先纠正一个容易混淆的模型 title=先纠正一个容易混淆的模型></a>先纠正一个容易混淆的模型</h2><p>之前讨论的是 <code>Qwen3.8-27B</code>,但是 FreeToken 论文里 8GB 显存运行 35B 的案例其实是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>Qwen3.6-35B-A3B</span><br></pre></table></figure><p>这两个模型虽然数字接近,结构却完全不同。<table><thead><tr><th>模型<th align=right>结构<th align=right>总参数<th align=right>每个 token 激活的参数<tbody><tr><td>Qwen3.8-27B<td align=right>Dense<td align=right>27B<td align=right>接近全部 27B<tr><td>Qwen3.6-35B-A3B<td align=right>MoE<td align=right>35B<td align=right>约 3B</table><p>Qwen3.8-27B 的官方模型卡明确写的是 Dense 模型。它有 64 层,每层的 FFN 都需要参与计算,没有“256 个专家只挑 8 个”这回事。<p>Qwen3.6-35B-A3B 则有 40 个 MoE 层,每层包含 256 个路由专家。一个 token 只会选择其中 8 个,再加上 1 个始终参与计算的共享专家。<p>名字里的:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>35B-A3B</span><br><span class=line> │ └── 每个 token 大约激活 3B 参数</span><br><span class=line> └────── 模型一共仍然有 35B 参数</span><br></pre></table></figure><p><code>A3B</code> 只是在说一次前向计算经过了多少参数,并不是模型文件只有 3B,也不是内存只需要保存 3B。<p><img alt="Dense 和 MoE 的计算路径" data-src=/uploads/freetoken/dense-vs-moe.svg><p>Dense 像是一整栋楼只有一条通道,每次都要从头走到尾。MoE 则像每层都有很多条支路,路由器会根据当前 token 选择其中几条。<p>这带来了两个结果:<ul><li>计算量可以接近一个 3B 模型<li>完整的 35B 权重仍然必须放在某个地方</ul><p>MoE 解决的是计算稀疏,并没有自动解决存储问题。FreeToken 真正处理的就是后半个问题。<h2 id=35B-模型的内存账应该怎么算><a title="35B 模型的内存账应该怎么算" class=headerlink href=#35B-模型的内存账应该怎么算></a>35B 模型的内存账应该怎么算</h2><p>先做一个很粗略的计算。假设不考虑量化元数据、对齐和少量高精度权重:<table><thead><tr><th>权重精度<th align=right>每个参数占用<th align=right>35B 参数的理论大小<tbody><tr><td>BF16 / FP16<td align=right>2 bytes<td align=right>70GB<tr><td>FP8 / INT8<td align=right>1 byte<td align=right>35GB<tr><td>4bit<td align=right>0.5 byte<td align=right>17.5GB</table><p>所以即使用 4bit,完整模型也明显超过 8GB。实际运行时还要额外留出:<ul><li>KV Cache<li>中间激活<li>CUDA Graph 和算子工作区<li>视觉编码器或者其他非专家权重<li>桌面环境占用的显存</ul><p>因此“8GB 运行 35B”绝对不能理解为“35B 的所有权重都常驻 8GB 显存”。<p>论文中的 4060 Laptop 机器实际有:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>GPU: RTX 4060 Laptop,8GB VRAM</span><br><span class=line>内存: 32GiB LPDDR5</span><br><span class=line>PCIe: 4.0 x8</span><br><span class=line>模型: Qwen3.6-35B-A3B NVFP4</span><br></pre></table></figure><p>NVFP4 把专家权重压缩到了接近 4bit,完整专家池主要放在 32GiB 系统内存中。显存里放的是:<ol><li>注意力、路由器、归一化等非专家权重<li>KV Cache 和运行时工作区<li>一部分最近使用的专家</ol><p>所以这其实是一个三级存储问题:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>磁盘:保存模型文件</span><br><span class=line> ↓ 启动时读取</span><br><span class=line>内存:保存完整专家池,是权重的 source of truth</span><br><span class=line> ↓ 按需搬运</span><br><span class=line>显存:保存非专家权重、KV Cache 和热点专家</span><br></pre></table></figure><p>显存不再是模型的完整仓库,而是最快的一层缓存。<p><img alt="FreeToken 的内存层次" data-src=/uploads/freetoken/memory-hierarchy.svg><p>这跟操作系统的虚拟内存、CPU Cache 有一点像:完整数据在容量更大的慢速介质里,当前最可能使用的数据留在小而快的介质里。<p>不过模型推理有一个特别麻烦的地方:下一步会用到哪个专家,是运行到路由器之后才知道的。所以光有“缓存”两个字还不够,还需要解决命中、换入、并发和调度的问题。<h2 id=FreeToken-的基本结构><a title="FreeToken 的基本结构" class=headerlink href=#FreeToken-的基本结构></a>FreeToken 的基本结构</h2><p>FreeToken 把一个 MoE 模型拆成两部分:<h3 id=非专家权重常驻-GPU><a title="非专家权重常驻 GPU" class=headerlink href=#非专家权重常驻-GPU></a>非专家权重常驻 GPU</h3><p>注意力层、Gated DeltaNet、路由器、共享专家等不会像路由专家那样从几百个里面选几个。它们每次都会使用,所以尽量常驻 GPU。<h3 id=完整路由专家池常驻内存><a class=headerlink href=#完整路由专家池常驻内存 title=完整路由专家池常驻内存></a>完整路由专家池常驻内存</h3><p>所有层的全部专家都保留在 Host RAM 中。这一份是完整权重,也是最终兜底:<ul><li>GPU 缓存命中,直接在 GPU 上算<li>GPU 缓存未命中,可以把专家搬进显存<li>如果不值得搬,也可以直接由 CPU 计算</ul><p>因此 GPU 缓存大小只影响速度,不影响模型能不能算出正确结果。即使缓存是冷的,Host RAM 中仍然有完整专家。<h3 id=剩余显存组成全局专家缓存><a class=headerlink href=#剩余显存组成全局专家缓存 title=剩余显存组成全局专家缓存></a>剩余显存组成全局专家缓存</h3><p>FreeToken 没有给每一层固定切一块显存,而是把专家缓存做成跨层共享的 slot pool。<p>一个 slot 保存的是某一层某一个专家需要的全部权重:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>(layer_id, expert_id) -> gpu_slot</span><br></pre></table></figure><p>以 Qwen3.6-35B-A3B 为例:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>40 层 × 每层 256 个专家</span><br><span class=line>= 10240 个不同的 (layer, expert)</span><br></pre></table></figure><p>显存不需要有 10240 个 slot。假设当前只能放 600 个,就由所有层共同竞争这 600 个位置。最近使用过的专家留下,长时间没用的专家被淘汰。<p>这比“每层固定留 15 个专家”灵活,因为不同层、不同会话的热点分布并不平均。<h2 id=生成阶段:专家缓存到底怎么工作><a class=headerlink href=#生成阶段:专家缓存到底怎么工作 title=生成阶段:专家缓存到底怎么工作></a>生成阶段:专家缓存到底怎么工作</h2><p>大模型推理通常分成两个阶段:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>Prefill:一次处理完整输入,得到第一个输出 token</span><br><span class=line>Decode:之后每轮只新增一个 token</span><br></pre></table></figure><p>这两个阶段虽然都经过 MoE 层,访问专家的模式却完全相反。先看 Decode。<p>假设某一层有 256 个专家,当前 token 被路由到了 8 个专家:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>[17, 42, 58, 91, 103, 144, 201, 233]</span><br></pre></table></figure><p>FreeToken 会先查询每个专家是否在 GPU slot 中。<p>源码中的核心映射可以简化成:<figure class="highlight python"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br></pre><td class=code><pre><span class=line><span class=comment># 正向表:某层某专家当前在哪个 GPU slot</span></span><br><span class=line>slot_for_id[layer_id][expert_id] = slot_id</span><br><span class=line></span><br><span class=line><span class=comment># 反向表:某个 slot 当前装的是谁</span></span><br><span class=line>id_of_slot[slot_id] = layer_id * num_experts + expert_id</span><br><span class=line></span><br><span class=line><span class=comment># LRU 时间戳</span></span><br><span class=line>usage[slot_id] = current_step</span><br></pre></table></figure><p>一次查询之后会得到两组:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>命中 H:已经在 GPU 中,可以立即计算</span><br><span class=line>未命中 M:仍然只在内存中</span><br></pre></table></figure><p>命中的部分很好处理。真正有意思的是未命中。<p>传统想法通常有两个极端:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>方案 A:未命中专家全部通过 PCIe 搬到 GPU</span><br><span class=line>方案 B:未命中专家全部留在内存,由 CPU 计算</span><br></pre></table></figure><p>FreeToken 选择了第三种:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>一部分搬到 GPU</span><br><span class=line>另一部分同时交给 CPU</span><br></pre></table></figure><p><img alt="FreeToken 的 Decode 路径" data-src=/uploads/freetoken/decode-path.svg><p>具体执行顺序大致是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>1. 路由器给出逻辑 expert id</span><br><span class=line>2. 在全局 LRU 缓存中查找 slot</span><br><span class=line>3. 命中的专家直接改写为物理 slot id</span><br><span class=line>4. 为需要搬运的 miss 选择 LRU victim</span><br><span class=line>5. 先提交 CPU 专家计算</span><br><span class=line>6. 同时通过 PCIe 把另一部分 miss 搬进 GPU slot</span><br><span class=line>7. GPU 计算“原有 hit + 新填入的专家”</span><br><span class=line>8. 等待 CPU 部分完成</span><br><span class=line>9. 把 CPU 和 GPU 的部分结果相加</span><br></pre></table></figure><p>对应到源码,<code>python/freetoken/layers/moe.py</code> 中的 <code>_decode_hybrid()</code> 做的正是这件事。它先提交 CPU task,再调用 <code>copy_missing()</code> 和 GPU expert GEMM,最后:<figure class="highlight python"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>return</span> gpu_routed + cpu_routed</span><br></pre></table></figure><p>为什么可以直接相加?<p>一个标准 MoE 层的结果本来就是多个专家输出的加权和:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>y = g1 × E1(x) + g2 × E2(x) + ... + gk × Ek(x)</span><br></pre></table></figure><p>把专家集合拆成 GPU 集合和 CPU 集合,只是改变了计算地点:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>y_gpu = GPU 上专家的加权和</span><br><span class=line>y_cpu = CPU 上专家的加权和</span><br><span class=line></span><br><span class=line>y = y_gpu + y_cpu</span><br></pre></table></figure><p>FreeToken 会保证一条路由只由一边计算一次。源码中,交给 CPU 的专家在 GPU 路径里权重会被置零,交给 GPU 的专家在 CPU 路径里会被标记成 <code>-1</code> 并跳过。<p>所以这里不是删掉几个专家,也不是近似计算。除了模型本身使用的量化精度,CPU/GPU 分流不会额外改变 MoE 的数学定义。<h2 id=q-:为什么不是一半给-CPU、一半给-GPU><a title="q*:为什么不是一半给 CPU、一半给 GPU" class=headerlink href=#q-:为什么不是一半给-CPU、一半给-GPU></a>q*:为什么不是一半给 CPU、一半给 GPU</h2><p>现在还剩一个问题:假设有 4 个专家没有命中,到底应该搬几个?<p>如果全部搬运,CPU 可能在旁边闲着。如果全部交给 CPU,PCIe 和 GPU 又会闲着。最合适的比例跟机器有关。<p>FreeToken 用两个实际测出来的带宽做决定:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>B_P:内存通过 PCIe 向 GPU 搬专家的有效带宽</span><br><span class=line>B_H:CPU 直接读取并计算专家时的有效内存带宽</span><br><span class=line>m:这一层没有命中的专家数量</span><br><span class=line>q:其中选择搬入 GPU 的数量</span><br></pre></table></figure><p>注意,PCIe DMA 和 CPU 计算都要从同一份内存中读专家权重。PCIe 正在以 <code>B_P</code> 读数据时,能留给 CPU 的大约是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>B_R = B_H - B_P</span><br></pre></table></figure><p>假设每个专家大小都是 <code>S</code>,两个并发分支的耗时近似为:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>GPU 搬运分支:T_fill ≈ q × S / B_P</span><br><span class=line>CPU 计算分支:T_cpu ≈ (m - q) × S / (B_H - B_P)</span><br></pre></table></figure><p>两个任务是并行的,总时间取较慢的那个。要让硬件利用得最充分,就尽量让它们同时结束:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>q × S / B_P ≈ (m - q) × S / (B_H - B_P)</span><br></pre></table></figure><p>两边约掉 <code>S</code>,整理一下:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>q* ≈ m × B_P / B_H</span><br></pre></table></figure><p>这就是论文中的 <code>q* policy</code>。<p>这个推导默认 <code>B_H > B_P</code>,也就是 PCIe 占满以后还有一部分 Host 带宽能留给 CPU。实际执行时还会把 <code>q</code> 取整并限制在合法范围。对于 <code>m > 0</code> 的情况,可以把它理解成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>q = clamp(round(q*), 1, m)</span><br></pre></table></figure><p>当 <code>B_H</code> 接近或者低于 <code>B_P</code> 时,CPU 已经没有值得利用的剩余带宽,策略就退化成 <code>q = m</code>,把 miss 都送去 GPU;至少保留一次 fill,也能让冷缓存继续升温。<p>用论文里 4060 Laptop 的实测数据算一次:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>B_P = 11.8 GB/s</span><br><span class=line>B_H = 47.5 GB/s</span><br><span class=line>m = 4</span><br><span class=line></span><br><span class=line>q* ≈ 4 × 11.8 / 47.5</span><br><span class=line> ≈ 0.99</span><br></pre></table></figure><p>取整之后就是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>1 个专家通过 PCIe 搬进 GPU</span><br><span class=line>3 个专家直接由 CPU 计算</span><br></pre></table></figure><p>这里有一个挺反直觉的地方:CPU 并不是因为算力特别强才参与,而是因为 Decode 的 batch 通常很小,专家 GEMV 更容易受内存带宽限制。CPU 计算那 3 个专家,实际上是在利用 PCIe 没有吃掉的剩余内存带宽。<p>如果换到另一台机器:<ul><li>PCIe 很快、CPU 内存带宽不高,<code>q*</code> 会接近 <code>m</code>,更适合多搬到 GPU<li>内存带宽明显高于 PCIe,CPU 可以承接更多 miss<li>专家本来就在缓存里,则根本不进入 <code>q*</code> 分流</ul><p>所以 FreeToken 不靠显卡型号表拍脑袋。它提供:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ft bench bw</span><br></pre></table></figure><p>这个命令使用真实的专家搬运和 CPU MoE kernel 测量带宽,并把结果写到:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.cache/freetoken/benchbw.json</span><br></pre></table></figure><p><code>ft serve --moe-backend auto</code> 会根据这份 profile 决定继续使用 offload,还是升级为 CPU/GPU 并行的 hybrid。<h2 id=为什么-LRU-专家缓存会有效><a title="为什么 LRU 专家缓存会有效" class=headerlink href=#为什么-LRU-专家缓存会有效></a>为什么 LRU 专家缓存会有效</h2><p>如果每个 token 选中的专家完全随机,刚放进 GPU 的专家下一步就再也不用,那么缓存没有太大价值。<p>实际路由通常存在短期局部性。相邻 token 处在同一段语义、同一种语言或同一段代码中,路由到的专家经常重叠:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>token t: [17, 42, 58, 91, 103, 144, 201, 233]</span><br><span class=line>token t + 1: [17, 42, 61, 91, 103, 144, 205, 233]</span><br><span class=line> ↑ ↑ ↑ ↑ ↑ ↑</span><br></pre></table></figure><p>第二个 token 中就有多个专家可以直接命中。<p>FreeToken 使用全局 LRU:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>命中:更新 usage</span><br><span class=line>未命中:找最久没有使用的 slot</span><br><span class=line>换入:更新正向表和反向表</span><br><span class=line>执行:把逻辑 expert id 改写为 slot id</span><br></pre></table></figure><p>源码里的实现还多做了一步:命中判断、miss 去重、<code>q</code> 的计算、victim 选择和 ID 改写都在 GPU kernel 中完成。<p>原因是 CUDA Graph 喜欢固定的控制流和固定形状。如果每一层都把 expert id 拷回 CPU,让 Python 决定淘汰谁,再通知 GPU,光同步就会非常昂贵。<p>FreeToken 的做法是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>控制流保持固定</span><br><span class=line>变化的命中数、miss 数和 victim 都作为 GPU 上的数据</span><br></pre></table></figure><p>工作缓冲区大小固定,再用一个有效数量屏蔽没有使用的部分。这样动态 LRU 仍然可以被放进静态捕获的 CUDA Graph。<p>论文给出的路由回放结果也能看到这个差别。在相同缓存容量下,Qwen3.6 的 Decode miss rate:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>FreeToken 全局 LRU:16%</span><br><span class=line>KTransformers 预填充时更新的静态放置:41%</span><br><span class=line>llama.cpp 路由无关的静态切分:62%</span><br></pre></table></figure><p>这个数字不是说 LRU 永远都会命中 84%,而是说明在论文的四类 agent workload 和指定缓存容量下,token 之间确实存在可以利用的路由局部性。<h2 id=Prefill-为什么不能也按需加载><a title="Prefill 为什么不能也按需加载" class=headerlink href=#Prefill-为什么不能也按需加载></a>Prefill 为什么不能也按需加载</h2><p>看完 Decode 很容易产生一个想法:Prefill 也让选中的专家按需进入缓存不就行了?<p>问题在于 Prefill 不是一次只处理一个 token,而是一次处理几千个 token。<p>对单个 token 来说:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>256 个专家中只选 8 个</span><br></pre></table></figure><p>但是一批 8192 个 token 各选 8 个,取所有路由结果的并集后,几乎每个专家都会被碰到:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>token 1 -> 8 个专家</span><br><span class=line>token 2 -> 另外一些专家</span><br><span class=line>...</span><br><span class=line>token 8192 -> 另外一些专家</span><br><span class=line></span><br><span class=line>所有 token 的并集 -> 接近完整 256 个专家</span><br></pre></table></figure><p>也就是说,MoE 在 Decode 阶段是稀疏工作集,到了长 Prompt 的 Prefill 阶段却接近稠密工作集。<p>如果还逐个 miss、逐个搬运,会产生很多细碎传输,GPU 也会不断等 PCIe。<p>FreeToken 因此给 Prefill 设计了另一条路径:完整层双缓冲。<p><img alt="FreeToken 的 Prefill 双缓冲" data-src=/uploads/freetoken/prefill-double-buffer.svg><p>它从全局专家 slot pool 中借出能容纳两层完整专家的空间:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>Buffer A:GPU 正在计算第 l 层</span><br><span class=line>Buffer B:PCIe 同时传输第 l + 1 层</span><br></pre></table></figure><p>第 <code>l</code> 层算完后,两块 buffer 交换角色:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>Buffer B:计算第 l + 1 层</span><br><span class=line>Buffer A:传输第 l + 2 层</span><br></pre></table></figure><p>因为搬的是下一层完整专家,甚至不需要等下一层路由结果出来就可以提前开始传输。理想情况下,一层的计算时间被下一层的搬运时间完全覆盖。<p>这也解释了为什么至少要有两层专家大小的缓存才能开启 overlap。源码中 <code>OffloadMoeCache</code> 会直接检查:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>cache_size >= 2 × num_experts</span><br></pre></table></figure><p>空间不足时不会硬撑,而是回退到按需 Prefill。<p>论文在 RTX 5090 上关闭双缓冲后,4K、8K、16K Prompt 的 Prefill 吞吐分别下降了 19%、25% 和 26%。Prompt 越长,完整专家池的搬运越无法忽略。<h2 id=Agent-场景里还有一种重复:上下文被改写><a title="Agent 场景里还有一种重复:上下文被改写" class=headerlink href=#Agent-场景里还有一种重复:上下文被改写></a>Agent 场景里还有一种重复:上下文被改写</h2><p>普通聊天通常只是在历史后面追加内容,前缀 KV Cache 很容易复用。<p>Coding Agent 不一样。一次工具调用之后,框架可能会:<ul><li>删除旧的 thinking<li>用占位符替换太老的工具输出<li>截断历史 observation<li>保留前半段,再改写中间某一块</ul><p>Qwen3.6 这类混合架构不仅有完整注意力,还有 Gated DeltaNet 这样的递归状态。KV Cache 可以按 token 保存,递归层却把前缀压缩成一个不断演进的 state。<p>如果中间一块上下文被改了,修改点后面的 state 都失效。检查点又很少的话,只能从很早的位置重新 Prefill。<p>FreeToken 的做法是把有限的 recurrent-state checkpoint 放在更可能存活的语义边界:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>一轮对话结束</span><br><span class=line>thinking 结束</span><br><span class=line>tool call 开始/结束</span><br><span class=line>tool output 开始/结束</span><br></pre></table></figure><p>Agent 框架通常也是按这些完整区块删除内容,所以边界之前的前缀更可能保持不变。恢复时:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>找到仍然有效的最深语义锚点</span><br><span class=line>恢复对应的 KV / recurrent state</span><br><span class=line>只重新计算变化后的后缀</span><br></pre></table></figure><p>这部分跟专家缓存解决的不是同一个问题:<table><thead><tr><th>缓存<th>缓存的东西<th>主要减少什么<tbody><tr><td>专家缓存<td>MoE 权重<td>Decode 时的 PCIe 搬运和 CPU 计算<tr><td>KV / 状态缓存<td>Prompt 的计算结果<td>多轮 Agent 中重复 Prefill</table><p>FreeToken 把它们放进同一个 serving engine,是因为本地 Agent 的总等待时间同时受两边影响。只有 Decode 跑得快,但每次工具调用后重新 Prefill 一分钟,体验仍然不行。<h2 id=显存还要在专家缓存和-KV-Cache-之间动态分配><a title="显存还要在专家缓存和 KV Cache 之间动态分配" class=headerlink href=#显存还要在专家缓存和-KV-Cache-之间动态分配></a>显存还要在专家缓存和 KV Cache 之间动态分配</h2><p>刚启动模型时上下文很短,可以把更多剩余显存用作专家缓存。对话越来越长后,KV Cache 会持续增长,而专家工作集没有同比增长。<p>静态切分很容易出现:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>专家缓存很大,但 KV Cache 不够</span><br><span class=line>或者</span><br><span class=line>KV 预留太多,专家缓存命中率很低</span><br></pre></table></figure><p>FreeToken 把这两块都看成运行时资源。在 scheduler 没有待处理 Prefill、没有进行中的 Decode 时,到达一个 safe point,才执行 cache rebuild:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>暂停接收新的计算批次</span><br><span class=line>释放旧 CUDA Graph</span><br><span class=line>重新分配专家 slot 和 KV page</span><br><span class=line>重新捕获 CUDA Graph</span><br><span class=line>继续服务</span><br></pre></table></figure><p>Host RAM 中的完整专家池不需要重新加载,所以不必重启进程或者再次读取整个模型。<p>可以通过:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>ft ctl cache</span><br><span class=line>ft ctl cache --moe 512 --kv 16k</span><br></pre></table></figure><p>查看和调整缓存。调整不是发生在某个 token 计算到一半的时候,而是排队等到 scheduler 完全空闲的安全点。<h2 id=FTW-为什么能缩短启动时间><a title="FTW 为什么能缩短启动时间" class=headerlink href=#FTW-为什么能缩短启动时间></a>FTW 为什么能缩短启动时间</h2><p>模型文件里的专家权重未必正好是运行时最方便的排列。FreeToken 会把不同模型的专家整理成少数几种 expert bank,并把:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>flat_id = layer_id × num_experts + expert_id</span><br></pre></table></figure><p>作为每个 bank 的第一维。<p>同一个 <code>flat_id</code> 在 <code>gate_up</code>、<code>down</code>、scale 等 bank 中对应同一个完整专家。这样 GPU slot cache 和 CPU executor 共用一套逻辑 ID,不需要理解每种模型原始 checkpoint 的布局。<p>FTW 是 FreeToken 的 fast weight format。它不是新的量化算法,主要作用是预先把权重转换成运行时最终布局。<p>传统加载过程可能是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>申请巨大空缓冲区</span><br><span class=line>把内存页 pin 住并清零</span><br><span class=line>读取 checkpoint</span><br><span class=line>重新排列专家</span><br><span class=line>复制到最终位置</span><br></pre></table></figure><p>FreeToken 的路径是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>从磁盘直接读入最终 Host 布局</span><br><span class=line>数据填充完成后再 pin</span><br></pre></table></figure><p>Pinned memory,也就是 page-locked memory,不会被操作系统随便换出,CUDA 可以对它进行更稳定的异步 DMA。但提前 pin 一个巨大的空缓冲区会触发页面分配和清零,随后又马上被模型数据覆盖,白做了一遍内存写入。<p>FTW 避免了这部分启动开销。GPU 专家缓存也不需要专门 warmup,第一次请求从冷缓存开始,沿用普通 miss 路径自然升温。<p><code>ft checkpoint</code> 是可选步骤,FreeToken 也能直接读取支持的 Hugging Face checkpoint:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>ft checkpoint \</span><br><span class=line> --model ~/models/Qwen3.6-35B-A3B \</span><br><span class=line> --out ~/models/Qwen3.6-35B-A3B-FTW \</span><br><span class=line> --moe-backend offload</span><br></pre></table></figure><h2 id=从源码看这些模块怎么对应><a class=headerlink href=#从源码看这些模块怎么对应 title=从源码看这些模块怎么对应></a>从源码看这些模块怎么对应</h2><p>把论文和代码对照起来,大致是下面这些位置:<table><thead><tr><th>源码<th>主要职责<tbody><tr><td><code>python/freetoken/moe/offload_cache.py</code><td>全局专家 slot、LRU 映射、专家 bank、Prefill 双缓冲<tr><td><code>python/freetoken/moe/offload_kernels.py</code><td>GPU 上做 hit/miss 分类、<code>q</code> 计算、victim 选择和 ID 改写<tr><td><code>python/freetoken/layers/moe.py</code><td>串起路由、CPU 提交、PCIe copy、GPU GEMM 和结果合并<tr><td><code>python/freetoken/moe/cpu_executor.py</code><td>固定 CPU worker、SIMD 和量化专家计算<tr><td><code>python/freetoken/moe/benchbw.py</code><td>测量真实 CPU MoE 与 PCIe 搬运带宽<tr><td><code>python/freetoken/engine/cache_budget.py</code><td>在专家 slot 和 KV page 之间计算显存预算<tr><td><code>python/freetoken/scheduler/scheduler.py</code><td>在 idle safe point 执行运行时 cache rebuild</table><p>其中最核心的 Decode 伪代码其实不复杂:<figure class="highlight python"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br></pre><td class=code><pre><span class=line>routes = router(hidden_states)</span><br><span class=line></span><br><span class=line>hits, misses = gpu_cache.lookup(routes)</span><br><span class=line>to_gpu, to_cpu = split_by_bandwidth(misses)</span><br><span class=line></span><br><span class=line>cpu_task = cpu_executor.submit(to_cpu)</span><br><span class=line>gpu_cache.fill(to_gpu)</span><br><span class=line></span><br><span class=line>gpu_output = gpu_experts(hits + to_gpu)</span><br><span class=line>cpu_output = cpu_task.wait()</span><br><span class=line></span><br><span class=line><span class=keyword>return</span> gpu_output + cpu_output</span><br></pre></table></figure><p>真正难的是让 lookup、动态 miss 数、异步拷贝、CPU worker 和 CUDA Graph 同时成立,而且不能每层都把控制权交回 Python。<h2 id=跟-llama-cpp-的部分卸载有什么区别><a title="跟 llama.cpp 的部分卸载有什么区别" class=headerlink href=#跟-llama-cpp-的部分卸载有什么区别></a>跟 llama.cpp 的部分卸载有什么区别</h2><p>两者最表面的相同点都是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>显存不够,就让一部分权重留在内存</span><br></pre></table></figure><p>但“哪一部分留在内存”和“运行时怎么使用它”差别很大。<p><img alt="llama.cpp 与 FreeToken 的卸载方式" data-src=/uploads/freetoken/llamacpp-vs-freetoken.svg><h3 id=llama-cpp-的-ngl-是静态层卸载><a title="llama.cpp 的 -ngl 是静态层卸载" class=headerlink href=#llama-cpp-的-ngl-是静态层卸载></a>llama.cpp 的 -ngl 是静态层卸载</h3><p>以一个 64 层 Dense 模型为例:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>第 0~39 层:CPU</span><br><span class=line>第 40~63 层:GPU</span><br></pre></table></figure><p>模型加载完成后,层的位置基本就固定了。每生成一个 token,仍然要依次经过全部 64 层:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>CPU 计算前 40 层</span><br><span class=line>传递中间激活</span><br><span class=line>GPU 计算后 24 层</span><br></pre></table></figure><p>这里通常移动的是层与层之间较小的激活,不是每个 token 都把完整层权重搬来搬去。<p>这种方式非常适合 Dense 模型,因为 Dense 没有路由器,任何一层的 FFN 权重每个 token 都会使用。也就谈不上“缓存最近用到的专家”。<p>当前 llama.cpp 的参数是:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>-ngl N</span><br><span class=line>--gpu-layers N</span><br><span class=line>--n-gpu-layers N</span><br></pre></table></figure><p>现在也可以使用 <code>auto</code> 或 <code>all</code>,让运行时自动拟合显存。<h3 id=llama-cpp-也有专门的-MoE-CPU-放置><a title="llama.cpp 也有专门的 MoE CPU 放置" class=headerlink href=#llama-cpp-也有专门的-MoE-CPU-放置></a>llama.cpp 也有专门的 MoE CPU 放置</h3><p>如果模型是 MoE,当前 llama.cpp 还支持:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>-cmoe</span><br><span class=line>--cpu-moe</span><br></pre></table></figure><p>把全部 MoE 专家权重留在 CPU;或者:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>-ncmoe N</span><br><span class=line>--n-cpu-moe N</span><br></pre></table></figure><p>只把前 N 个 MoE 层的专家留在 CPU。<p>这已经比单纯的 <code>-ngl</code> 更接近 FreeToken:注意力等非专家部分可以继续在 GPU,而巨大的专家权重留在内存。<p>llama.cpp 当前的 scheduler 还能根据路由 ID,只处理被选中的 expert sub-row,而不是无条件复制所有专家。不过截至 2026 年 8 月 23 日,官方 master 的这条路径没有 FreeToken 那样跨 token 持久存在的 <code>expert -> slot</code> 映射。被选中的专家可以在一次 forward 中搬到 GPU tensor 的原始偏移位置,但下一次相同专家仍可能再次搬运。<p>llama.cpp 社区已经有 persistent expert cache 的 RFC 和 PR,例如 <a href=https://github.com/ggml-org/llama.cpp/issues/20757 rel=noopener target=_blank>#20757</a> 和 <a href=https://github.com/ggml-org/llama.cpp/pull/26824 rel=noopener target=_blank>#26824</a>,说明这个方向也正在快速演进。这里对比的是本文日期的官方 CLI 和 master 行为,不应该理解成 llama.cpp 永远不会有专家缓存。<h3 id=FreeToken-优化的是动态专家工作集><a title="FreeToken 优化的是动态专家工作集" class=headerlink href=#FreeToken-优化的是动态专家工作集></a>FreeToken 优化的是动态专家工作集</h3><p>FreeToken 的主要单位不是“第几层放在哪”,而是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>当前这个 (layer, expert) 应该在哪里执行</span><br></pre></table></figure><table><thead><tr><th>对比项<th>llama.cpp <code>-ngl</code><th>llama.cpp <code>--cpu-moe</code><th>FreeToken hybrid<tbody><tr><td>主要对象<td>完整层<td>MoE 专家 tensor<td>单个 <code>(layer, expert)</code><tr><td>放置时机<td>加载时静态决定<td>加载时决定专家在 CPU<td>每个 Decode step 动态决定<tr><td>GPU 专家驻留<td>不适用<td>官方 master 无跨 step slot cache<td>全局 LRU 持久缓存<tr><td>Cache miss<td>不适用<td>CPU 计算或按路由搬运<td>一部分搬 GPU,一部分 CPU 计算<tr><td>CPU/GPU 比例<td>由层数决定<td>由 CPU MoE 层数等配置决定<td><code>q*</code> 根据实测带宽决定<tr><td>Prefill<td>CPU/GPU 按层执行<td>后端相关的专家处理<td>完整层双缓冲隐藏 PCIe<tr><td>Agent 状态复用<td>KV/Context checkpoint<td>KV/Context checkpoint<td>KV + 语义 recurrent-state anchor<tr><td>格式和平台<td>GGUF,CPU/CUDA/Metal/Vulkan 等<td>同左<td>当前开源 CLI 主要是 Linux x86_64 + NVIDIA CUDA</table><p>所以它们不是简单的“一个能卸载,一个不能卸载”。<p>llama.cpp 是通用、本地部署范围很广的推理引擎,Dense 模型、Apple Silicon 和各种后端都是它的优势。FreeToken 则把目标收得更窄,专门围绕“显存装不下完整 MoE 专家池”设计整个 serving stack。<h2 id=那么-Qwen3-8-27B-在-4060-上应该用哪一种><a title="那么 Qwen3.8-27B 在 4060 上应该用哪一种" class=headerlink href=#那么-Qwen3-8-27B-在-4060-上应该用哪一种></a>那么 Qwen3.8-27B 在 4060 上应该用哪一种</h2><p>回到最开始容易串在一起的问题。<p>Qwen3.8-27B 是 Dense 模型:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>没有 256 选 8 的路由专家</span><br><span class=line>每个 token 都要经过每层完整 FFN</span><br></pre></table></figure><p>27B 做 4bit 的理论权重大小约为:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>27B × 0.5 byte = 13.5GB</span><br></pre></table></figure><p>再加量化元数据、KV Cache 和运行时开销,8GB 4060 无法让它全量常驻显存。<p>这时更合适的思路是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>GGUF 低比特量化</span><br><span class=line>+ llama.cpp</span><br><span class=line>+ 尽可能多的静态 GPU layer offload</span><br><span class=line>+ 剩余层在 CPU / RAM 中执行</span><br></pre></table></figure><p>例如:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>llama-server \</span><br><span class=line> -m Qwen3.8-27B-Q3_K_M.gguf \</span><br><span class=line> -ngl auto \</span><br><span class=line> -c 8192</span><br></pre></table></figure><p>具体能放多少层取决于 GGUF 大小、上下文长度、KV 精度、系统当前可用显存和 llama.cpp 版本。<code>-ngl auto</code> 比照搬别人机器上的固定层数更稳妥。<p>FreeToken 当前文档虽然支持 Qwen3.6-27B Dense,但 Dense 模型的 <code>auto</code> 会解析为 <code>fused</code>,也就是要求权重常驻 GPU,并不会启用 MoE offload/hybrid 这套专家机制。当前支持列表也还没有列出刚发布的 Qwen3.8-27B。<p>因此:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>8GB 4060 + Qwen3.6-35B-A3B MoE</span><br><span class=line>-> FreeToken 的专家缓存和 hybrid 很有意义</span><br><span class=line></span><br><span class=line>8GB 4060 + Qwen3.8-27B Dense</span><br><span class=line>-> 更适合 llama.cpp 的量化和静态层卸载</span><br></pre></table></figure><p>两个模型参数接近,但运行路径不能混着解释。<h2 id=实际怎么跑-FreeToken><a title="实际怎么跑 FreeToken" class=headerlink href=#实际怎么跑-FreeToken></a>实际怎么跑 FreeToken</h2><p>当前开源 CLI 的要求是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>Linux x86_64</span><br><span class=line>NVIDIA GPU</span><br><span class=line>driver r580+</span><br><span class=line>CUDA 13</span><br><span class=line>Python 3.10+</span><br></pre></table></figure><p>所以它目前不是 M3 Pro 上 llama.cpp/Metal 的替代品。Mac 仍然更适合 llama.cpp 或 MLX。<p>安装:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>uv venv</span><br><span class=line><span class=built_in>source</span> .venv/bin/activate</span><br><span class=line>uv pip install <span class=string>"freetoken[accel]"</span></span><br></pre></table></figure><p>先测一次这台机器的 CPU/PCIe 带宽:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ft bench bw --dtype nvfp4</span><br></pre></table></figure><p>然后启动论文中 8GB 机器使用的 NVFP4 模型:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ft serve --model nvidia/Qwen3.6-35B-A3B-NVFP4</span><br></pre></table></figure><p>只提供 <code>--model</code> 也可以,dtype、attention backend、MoE backend、专家缓存和 KV 容量会根据 checkpoint、显存和带宽 profile 自动推导。<p>想明确观察模式,可以分别测试:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br></pre><td class=code><pre><span class=line><span class=comment># 专家在内存,miss 全部搬到 GPU</span></span><br><span class=line>ft serve \</span><br><span class=line> --model nvidia/Qwen3.6-35B-A3B-NVFP4 \</span><br><span class=line> --moe-backend offload</span><br><span class=line></span><br><span class=line><span class=comment># 专家在内存,miss 由 CPU 处理</span></span><br><span class=line>ft serve \</span><br><span class=line> --model nvidia/Qwen3.6-35B-A3B-NVFP4 \</span><br><span class=line> --moe-backend cpu</span><br><span class=line></span><br><span class=line><span class=comment># GPU cache hit + PCIe fill + CPU overflow 并行</span></span><br><span class=line>ft serve \</span><br><span class=line> --model nvidia/Qwen3.6-35B-A3B-NVFP4 \</span><br><span class=line> --moe-backend hybrid</span><br></pre></table></figure><p>运行后可以查看:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>ft ctl stats</span><br><span class=line>ft ctl cache</span><br></pre></table></figure><p>对比时不要只测一句 <code>hello</code>。至少分别看:<ul><li>冷启动后的第一轮<li>长 Prompt 的 TTFT<li>连续生成几百 token 后的 Decode<li>多轮工具调用后是否反复 Prefill</ul><p>短 Prompt、冷缓存和长时间 Agent 会话测到的是三种不同状态。<h2 id=这套方案的限制><a class=headerlink href=#这套方案的限制 title=这套方案的限制></a>这套方案的限制</h2><p>看到 39.3 tok/s 很容易只记住“8GB 显存”,但这套方案还有几个前提。<h3 id=内存仍然要装得下完整专家池><a class=headerlink href=#内存仍然要装得下完整专家池 title=内存仍然要装得下完整专家池></a>内存仍然要装得下完整专家池</h3><p>显存需求降低不等于总内存需求消失。论文里的 4060 Laptop 有 32GiB 内存,并且使用 NVFP4。只有 16GB 系统内存时,同一模型可能会进入 swap,速度会急剧下降,甚至无法稳定加载。<h3 id=主要适用于-MoE><a title="主要适用于 MoE" class=headerlink href=#主要适用于-MoE></a>主要适用于 MoE</h3><p>Dense 模型每个 token 都要读完整 FFN,没有小而稳定的专家工作集。FreeToken 的核心专家缓存不会把 27B Dense 变成 3B active。<h3 id=性能受整机而不是只受-GPU-决定><a title="性能受整机而不是只受 GPU 决定" class=headerlink href=#性能受整机而不是只受-GPU-决定></a>性能受整机而不是只受 GPU 决定</h3><p>需要同时看:<ul><li>Host RAM 带宽<li>PCIe 代际和通道数<li>CPU SIMD 能力<li>物理核心数<li>显存能留下多少专家 slot<li>实际路由的局部性</ul><p>同一张 4060 放在不同笔记本里,内存和 PCIe 配置不同,<code>q*</code> 和速度也会不同。<h3 id=39-3-tok-s-不是所有请求的固定速度><a title="39.3 tok/s 不是所有请求的固定速度" class=headerlink href=#39-3-tok-s-不是所有请求的固定速度></a>39.3 tok/s 不是所有请求的固定速度</h3><p>这是论文在指定 4060 Laptop、NVFP4 模型和 coding-agent workload 下测得的 Decode 吞吐。它不等于:<ul><li>冷启动速度<li>Prefill 速度<li>超长上下文下的平均响应速度<li>任意量化、任意机器都能得到的速度</ul><h3 id=缓存也有冷启动和工作集突变><a class=headerlink href=#缓存也有冷启动和工作集突变 title=缓存也有冷启动和工作集突变></a>缓存也有冷启动和工作集突变</h3><p>刚启动时专家缓存是空的。如果话题、语言或者任务突然变化,路由热点也可能变化,短时间 miss 会增加。LRU 利用的是局部性,不是消灭所有 miss。<h2 id=最后总结><a class=headerlink href=#最后总结 title=最后总结></a>最后总结</h2><p>现在再看“8GB 显存运行 35B”,可以把它拆成五步:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>1. Qwen3.6-35B-A3B 是 MoE,每个 token 只激活约 3B</span><br><span class=line>2. 完整的量化专家池放在 32GiB Host RAM,不是硬塞进 8GB VRAM</span><br><span class=line>3. 剩余显存作为跨层共享的 LRU 专家 slot cache</span><br><span class=line>4. Decode miss 按 q* 分给 PCIe+GPU 和 CPU,两条路径并发并精确合并</span><br><span class=line>5. Prefill 改用完整层双缓冲,并通过语义状态缓存减少 Agent 重算</span><br></pre></table></figure><p>llama.cpp 的 <code>-ngl</code> 主要解决的是“完整层怎样静态分布到 CPU 和 GPU”,<code>--cpu-moe</code> 则进一步允许专家留在 CPU。FreeToken 在这个基础问题上继续往前走,把专家当成随 token 路由变化的动态工作集,并围绕它设计缓存、带宽分流、Prefill 和运行时显存管理。<p>所以 FreeToken 真正有意思的地方并不是一个神奇的“8GB 跑 35B”数字,而是它换了一个看待本地机器的方式:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>GPU 不是唯一计算设备</span><br><span class=line>显存也不是唯一模型内存</span><br><span class=line>CPU、RAM、PCIe 和 GPU 可以组成一台统一的推理机器</span><br></pre></table></figure><p>但这条路成立的关键仍然是 MoE 的稀疏性。换成 Qwen3.8-27B 这样的 Dense 模型,还是要老老实实回到量化、层卸载和内存带宽这几个问题上。<h2 id=参考资料><a class=headerlink href=#参考资料 title=参考资料></a>参考资料</h2><ul><li><a href=https://arxiv.org/abs/2608.16157 rel=noopener target=_blank>FreeToken 论文:Efficient Edge-Native MoE Serving with Bandwidth-Adaptive Execution</a><li><a href=https://github.com/FlashML-org/FreeToken rel=noopener target=_blank>FreeToken 源码</a><li><a href=https://github.com/FlashML-org/FreeToken/blob/main/docs/cli.md rel=noopener target=_blank>FreeToken CLI 文档</a><li><a href=https://huggingface.co/Qwen/Qwen3.6-35B-A3B rel=noopener target=_blank>Qwen3.6-35B-A3B 官方模型卡</a><li><a href=https://huggingface.co/Qwen/Qwen3.8-27B rel=noopener target=_blank>Qwen3.8-27B 官方模型卡</a><li><a href=https://github.com/ggml-org/llama.cpp/blob/master/tools/cli/README.md rel=noopener target=_blank>llama.cpp CLI 参数</a><li><a href=https://github.com/ggml-org/llama.cpp/issues/20757 rel=noopener target=_blank>llama.cpp persistent expert cache 讨论</a></ul></div><footer class=post-footer><div class=post-eof></div></footer></article></div><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/08/16/%E8%AE%A9chatgpt%E6%9D%A5%E7%BB%99%E6%88%91%E8%AE%B2%E8%A7%A3%E4%B8%8Bdeepseek-harness%E7%9A%84%E6%8F%92%E4%BB%B6%E4%BD%93%E7%B3%BB/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/08/16/%E8%AE%A9chatgpt%E6%9D%A5%E7%BB%99%E6%88%91%E8%AE%B2%E8%A7%A3%E4%B8%8Bdeepseek-harness%E7%9A%84%E6%8F%92%E4%BB%B6%E4%BD%93%E7%B3%BB/ itemprop=url>让chatgpt来给我讲解下deepseek harness的插件体系</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-08-16 23:07:34 / 修改时间:23:08:30" datetime=2026-08-16T23:07:34+08:00>2026-08-16</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/LLM/ itemprop=url rel=index><span itemprop=name>LLM</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-title="让chatgpt来给我讲解下deepseek harness的插件体系" data-increment=false data-path=/2026/08/16/%E8%AE%A9chatgpt%E6%9D%A5%E7%BB%99%E6%88%91%E8%AE%B2%E8%A7%A3%E4%B8%8Bdeepseek-harness%E7%9A%84%E6%8F%92%E4%BB%B6%E4%BD%93%E7%B3%BB/ title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/08/16/%E8%AE%A9chatgpt%E6%9D%A5%E7%BB%99%E6%88%91%E8%AE%B2%E8%A7%A3%E4%B8%8Bdeepseek-harness%E7%9A%84%E6%8F%92%E4%BB%B6%E4%BD%93%E7%B3%BB/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/08/16/让chatgpt来给我讲解下deepseek-harness的插件体系/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><p>最近deepseek发布了harness,试用了一下结合deepseek-4-flash,用它尝试写一个php的现代版phpmyadmin,因为也是一个比较重要且有一点复杂度的应用<br>试了下除了数据展示有点问题,修了一轮就完成了,整体表现还是不错的<br>然后看了下它主打的是万物都是插件<br>这个概念还是比较复杂的,尝试让gpt老师帮我讲解下<br>DeepSeek Harness 就像一个“由很多小员工临时组成的公司”。<br>没有一个超级总经理包办所有事情:<ul><li>一个员工负责问大模型;<li>一个员工负责读文件;<li>一个员工负责运行命令;<li>一个员工负责审批危险操作;<li>一个员工负责保存聊天记录;<li>一个员工负责显示网页界面。<br>这些“员工”就是插件。<br>需要什么员工,就把什么员工叫来;不需要了,就让它离开,并自动收拾自己留下的东西。</ul><h2 id=用一个真实请求走一遍><a class=headerlink href=#用一个真实请求走一遍 title=用一个真实请求走一遍></a>用一个真实请求走一遍</h2><p>你对 DeepSeek Harness 说:<blockquote><p>帮我读取 package.json,看看项目用了哪些依赖。<br>系统内部大致发生了这些事:</blockquote><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br></pre><td class=code><pre><span class=line>你输入问题</span><br><span class=line> ↓</span><br><span class=line>Agent Loop 插件:决定下一步做什么</span><br><span class=line> ↓</span><br><span class=line>LLM 插件:把问题发给 DeepSeek</span><br><span class=line> ↓</span><br><span class=line>DeepSeek 返回:我要调用 read_file</span><br><span class=line> ↓</span><br><span class=line>权限插件:这个文件允许读吗?</span><br><span class=line> ↓</span><br><span class=line>文件工具插件:读取 package.json</span><br><span class=line> ↓</span><br><span class=line>日志插件:记录本次工具调用</span><br><span class=line> ↓</span><br><span class=line>Agent Loop 插件:把文件内容再次交给 DeepSeek</span><br><span class=line> ↓</span><br><span class=line>DeepSeek 生成最终回答</span><br><span class=line> ↓</span><br><span class=line>Web UI 插件:把回答显示出来</span><br></pre></table></figure><p>关键点是:<br>上面每一个步骤都可以由不同插件负责。<br>比如你可以:<ul><li>把 DeepSeek 换成其他模型;<li>把本地文件系统换成远程沙箱;<li>给文件读取前增加权限检查;<li>给模型调用增加重试;<li>给工具调用增加日志;<li>换掉整个 Agent Loop。<br>其他部分不需要跟着重写。</ul><h2 id=为什么普通插件系统做不到这么灵活><a class=headerlink href=#为什么普通插件系统做不到这么灵活 title=为什么普通插件系统做不到这么灵活></a>为什么普通插件系统做不到这么灵活</h2><p>普通软件的结构经常是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>固定核心程序</span><br><span class=line>├── 插件位置 A</span><br><span class=line>├── 插件位置 B</span><br><span class=line>└── 插件位置 C</span><br></pre></table></figure><p>核心程序是老板,插件只能填几个预留位置。<br>DeepSeek Harness 更像:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>极小的组装平台</span><br><span class=line>├── Agent Loop 插件</span><br><span class=line>├── 模型插件</span><br><span class=line>├── 工具插件</span><br><span class=line>├── 权限插件</span><br><span class=line>├── 存储插件</span><br><span class=line>└── UI 插件</span><br></pre></table></figure><p>连“核心 Agent 怎么循环运行”本身也是插件。<br>所以它不是:<blockquote><p>一个 Agent,加上一些插件。<br>而是:<br>很多插件组合起来以后,才形成一个 Agent。</blockquote><h2 id=插件之间怎么找到对方><a class=headerlink href=#插件之间怎么找到对方 title=插件之间怎么找到对方></a>插件之间怎么找到对方</h2><p>假设你写了一个天气工具插件。<br>它不能一启动就直接工作,因为它要先找到“工具管理器”。<br>所以它会声明:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>我需要:tools 服务</span><br></pre></table></figure><p>代码里大概是:<figure class="highlight ts"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>export</span> <span class=keyword>const</span> inject = [<span class=string>'tools'</span>]</span><br></pre></table></figure><p>意思就是:<blockquote><p>只有工具管理器存在时,才启动我。<br>系统启动时可能出现这种情况:</blockquote><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>天气插件:我需要 tools</span><br><span class=line>工具管理器:还没启动</span><br><span class=line>天气插件:那我先等着</span><br><span class=line></span><br><span class=line>工具管理器启动</span><br><span class=line>天气插件:条件满足,现在开始工作</span><br></pre></table></figure><p>如果工具管理器后来被替换:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>旧工具管理器卸载</span><br><span class=line> ↓</span><br><span class=line>天气插件也暂时卸载</span><br><span class=line> ↓</span><br><span class=line>新工具管理器启动</span><br><span class=line> ↓</span><br><span class=line>天气插件重新启动,并连接新管理器</span><br></pre></table></figure><p>这就是 DeepSeek Harness 很特别的地方:<blockquote><p>插件依赖不是只在启动时检查一次,而是一直被系统关注。</blockquote><h2 id=ctx-到底是什么><a title="ctx 到底是什么" class=headerlink href=#ctx-到底是什么></a><code>ctx</code> 到底是什么</h2><p>插件启动时会拿到一个 <code>ctx</code>:<figure class="highlight ts"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=keyword>export</span> <span class=keyword>function</span> <span class="title function_">apply</span>(<span class=params>ctx</span>) {</span><br><span class=line> <span class=comment>// 使用 ctx 做事</span></span><br><span class=line>}</span><br></pre></table></figure><p>你可以把 <code>ctx</code> 理解成“公司总机”。<br>插件不知道其他插件住在哪里,只需要问总机:<figure class="highlight ts"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>ctx.<span class=property>tools</span> <span class=comment>// 工具管理器</span></span><br><span class=line>ctx.<span class=property>llm</span> <span class=comment>// 模型管理器</span></span><br><span class=line>ctx.<span class=property>sessions</span> <span class=comment>// 会话管理器</span></span><br><span class=line>ctx.<span class=property>fs</span> <span class=comment>// 文件系统</span></span><br><span class=line>ctx.<span class=property>sandbox</span> <span class=comment>// 沙箱</span></span><br></pre></table></figure><p>比如天气插件说:<figure class="highlight ts"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ctx.<span class=property>tools</span>.<span class="title function_">register</span>(weatherTool)</span><br></pre></table></figure><p>意思就是:<blockquote><p>总机,请把我的天气能力登记到工具管理器里。<br>这样天气插件不需要自己找到 Agent Loop,也不需要直接修改模型提示词。工具管理器会负责把天气工具告诉模型。</blockquote><h2 id=插件卸载时,怎么避免留下垃圾><a class=headerlink href=#插件卸载时,怎么避免留下垃圾 title=插件卸载时,怎么避免留下垃圾></a>插件卸载时,怎么避免留下垃圾</h2><p>这是整个系统最值得理解的设计。<br>假设天气插件启动后做了三件事:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>1. 注册 weather 工具</span><br><span class=line>2. 监听工具调用事件</span><br><span class=line>3. 启动一个定时器</span><br></pre></table></figure><p>如果只是粗暴地删除插件代码,可能留下:<ul><li>一个无法调用的 weather 工具;<li>一个永远存在的事件监听器;<li>一个一直运行的定时器。<br>DeepSeek Harness 要求插件在创建东西时,同时登记“怎么撤销”。<br>可以理解成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br></pre><td class=code><pre><span class=line>创建 weather 工具</span><br><span class=line>同时记下:卸载时删除 weather 工具</span><br><span class=line></span><br><span class=line>添加事件监听器</span><br><span class=line>同时记下:卸载时移除监听器</span><br><span class=line></span><br><span class=line>启动定时器</span><br><span class=line>同时记下:卸载时停止定时器</span><br></pre></table></figure>于是卸载插件时,系统会自动倒着清理:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>停止定时器</span><br><span class=line>移除监听器</span><br><span class=line>删除 weather 工具</span><br></pre></table></figure>所以热更新才比较可靠。<br>修改天气插件代码后,系统可以:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>完整清理旧天气插件</span><br><span class=line> ↓</span><br><span class=line>重新加载新代码</span><br><span class=line> ↓</span><br><span class=line>重新注册新天气工具</span><br></pre></table></figure>而不是让新旧两套东西同时残留。<br>这个“创建时顺便记录撤销方法”的机制,技术上叫 <code>effect</code>。</ul><h2 id=Fiber-又是什么><a title="Fiber 又是什么" class=headerlink href=#Fiber-又是什么></a>Fiber 又是什么</h2><p>现在再引入 Fiber 就很容易了。<br>Fiber 可以理解成:<blockquote><p>某个插件这一次运行的“工作档案”。<br>里面记录着:</blockquote><ul><li>这个插件是谁;<li>它在等哪些服务;<li>它现在有没有启动;<li>它注册过什么东西;<li>卸载时要清理什么;<li>它是否启动失败。<br>一个插件大致会经历<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>等待条件</span><br><span class=line> ↓</span><br><span class=line>正在启动</span><br><span class=line> ↓</span><br><span class=line>正常运行</span><br><span class=line> ↓</span><br><span class=line>正在清理</span><br><span class=line> ↓</span><br><span class=line>已经卸载</span><br></pre></table></figure>对应的技术名称是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>PENDING</span><br><span class=line>→ LOADING</span><br><span class=line>→ ACTIVE</span><br><span class=line>→ UNLOADING</span><br><span class=line>→ DISPOSED</span><br></pre></table></figure>所以 Fiber 不是线程,也不是子进程。<br>它只是插件的一份“生命周期档案”。</ul><h3 id=插件怎么插手系统流程><a class=headerlink href=#插件怎么插手系统流程 title=插件怎么插手系统流程></a>插件怎么插手系统流程</h3><p>有些插件提供能力,比如文件系统。<br>另一些插件不提供新能力,只想在某个流程中插一脚。<br>例如权限插件想在执行 Bash 前检查命令:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>模型请求执行 rm ...</span><br><span class=line> ↓</span><br><span class=line>权限插件检查</span><br><span class=line> ↓</span><br><span class=line>允许:继续执行</span><br><span class=line>拒绝:直接返回错误</span><br></pre></table></figure><p>这个过程很像高速公路收费站:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>请求</span><br><span class=line> → 审批站</span><br><span class=line> → 权限站</span><br><span class=line> → 超时站</span><br><span class=line> → 日志站</span><br><span class=line> → 真正执行</span><br></pre></table></figure><p>每一站都可以:<ul><li>检查请求;<li>修改请求;<li>记录请求;<li>放行;<li>拒绝继续。<br>在代码里,放行通常是调用:<figure class="highlight ts"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class="title function_">next</span>()</span><br></pre></table></figure>不调用 <code>next()</code>,就表示到此为止。<br>这就是它所谓的 <code>waterfall</code>。本质上就是一条可插拔的中间件链。</ul><h2 id=Bundle-和-Profile-又是什么><a title="Bundle 和 Profile 又是什么" class=headerlink href=#Bundle-和-Profile-又是什么></a>Bundle 和 Profile 又是什么</h2><p>这里最容易被名词绕晕。<h3 id=Plugin-是员工><a title="Plugin 是员工" class=headerlink href=#Plugin-是员工></a>Plugin 是员工</h3><p>真正执行工作的代码。<h3 id=Bundle-是部门安装包><a title="Bundle 是部门安装包" class=headerlink href=#Bundle-是部门安装包></a>Bundle 是部门安装包</h3><p>一个 Bundle 可能一次带来很多插件,例如:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>Web Bundle</span><br><span class=line>├── Web 服务器插件</span><br><span class=line>├── 浏览器通信插件</span><br><span class=line>├── 聊天界面插件</span><br><span class=line>├── 设置页面插件</span><br><span class=line>└── Web 启动插件</span><br></pre></table></figure><p>Profile 是公司组织方案<br>比如:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>web Profile</span><br><span class=line>├── 基础 Bundle</span><br><span class=line>└── Web Bundle</span><br></pre></table></figure><p>而:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>headless Profile</span><br><span class=line>├── 基础 Bundle</span><br><span class=line>└── 命令行 Bundle</span><br></pre></table></figure><p>因此:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>dsh --profile web</span><br></pre></table></figure><p>可以理解成:<blockquote><p>按照 Web 这套组织方案,把对应部门和员工全部组装起来。</blockquote><h2 id=安装一个插件时发生什么><a class=headerlink href=#安装一个插件时发生什么 title=安装一个插件时发生什么></a>安装一个插件时发生什么</h2><p>执行:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>dsh plugin --profile web add some-plugin</span><br></pre></table></figure><p>大致等于:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>1. 用 pnpm 下载 npm 包</span><br><span class=line>2. 查看这个包有没有声明自己是 DSH Bundle</span><br><span class=line>3. 找到它携带的插件配置</span><br><span class=line>4. 把它加入 web Profile</span><br><span class=line>5. 下次启动时加载其中的插件</span><br></pre></table></figure><p>大概是这么个逻辑</div><footer class=post-footer><div class=post-eof></div></footer></article></div><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/08/09/%E6%9D%A5%E5%AD%A6%E4%B9%A0%E4%B8%8Bpi-agent%E7%9A%84%E5%8E%9F%E7%90%86/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/08/09/%E6%9D%A5%E5%AD%A6%E4%B9%A0%E4%B8%8Bpi-agent%E7%9A%84%E5%8E%9F%E7%90%86/ itemprop=url>来学习下pi agent的原理</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-08-09 21:23:24 / 修改时间:21:25:36" datetime=2026-08-09T21:23:24+08:00>2026-08-09</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/LLM/ itemprop=url rel=index><span itemprop=name>LLM</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-title="来学习下pi agent的原理" data-increment=false data-path=/2026/08/09/%E6%9D%A5%E5%AD%A6%E4%B9%A0%E4%B8%8Bpi-agent%E7%9A%84%E5%8E%9F%E7%90%86/ title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/08/09/%E6%9D%A5%E5%AD%A6%E4%B9%A0%E4%B8%8Bpi-agent%E7%9A%84%E5%8E%9F%E7%90%86/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/08/09/来学习下pi-agent的原理/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><p>之前也偶尔用户pi agent这个工具,当然这个还是个很庞大的系统,我也只能一点点学<br>首先是工具系统,默认的核心工具主要是四个 <code>read</code>,<code>write</code>,<code>edit</code>,<code>bash</code><h3 id=read-主要是读取文件><a title="read 主要是读取文件" class=headerlink href=#read-主要是读取文件></a>read 主要是读取文件</h3><p>输入结构式这样<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"path"</span><span class=punctuation>:</span> <span class=string>"src/app.ts"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"offset"</span><span class=punctuation>:</span> <span class=number>1</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"limit"</span><span class=punctuation>:</span> <span class=number>200</span></span><br><span class=line><span class=punctuation>}</span></span><br><span class=line>- path:相对或绝对路径</span><br><span class=line>- offset:从第几行开始,<span class=number>1</span> 开始计数</span><br><span class=line>- limit:最多读取多少行</span><br><span class=line>内部的原理大致是</span><br><span class=line>```text</span><br><span class=line>解析路径</span><br><span class=line>→ 检查文件是否可读</span><br><span class=line>→ 判断是文本还是图片</span><br><span class=line>→ 读取内容</span><br><span class=line>→ 按 offset/limit 截取</span><br><span class=line>→ 限制输出大小</span><br><span class=line>→ 返回给模型</span><br></pre></table></figure><p>对于文本,Pi 将文件按照 UTF-8 解码并拆分为行。默认最多返回前 2000 行或 50KB,哪个先达到就按哪个截断,并告诉模型下一次应该使用哪个 offset 继续读取。<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>[Showing lines 1-2000 of 3540. Use offset=2001 to continue.]</span><br></pre></table></figure><h3 id=write-创建文件或完整覆盖><a title="write 创建文件或完整覆盖" class=headerlink href=#write-创建文件或完整覆盖></a>write 创建文件或完整覆盖</h3><p>输入<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"path"</span><span class=punctuation>:</span> <span class=string>"src/Hello.php"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"content"</span><span class=punctuation>:</span> <span class=string>"&LT?php\n\necho \"hello\";\n"</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>主要原理<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>解析目标路径</span><br><span class=line>→ 创建不存在的父目录</span><br><span class=line>→ 进入该文件的 mutation queue</span><br><span class=line>→ 完整写入 content</span><br><span class=line>→ 返回写入结果</span><br></pre></table></figure><p>如果文件已经存在,则完整覆盖。它不是“追加”,也不是“局部修改”。<br>Pi 会针对同一个文件建立 mutation queue,防止多个并行工具同时修改同一个文件导致内容互相覆盖:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>write A ──────┐</span><br><span class=line> ├→ 同一个文件顺序执行</span><br><span class=line>edit A ──────┘</span><br><span class=line></span><br><span class=line>write B ─────────→ 不同文件可以独立处理</span><br></pre></table></figure><h3 id=edit-精确替换已有代码><a title="edit 精确替换已有代码" class=headerlink href=#edit-精确替换已有代码></a>edit 精确替换已有代码</h3><p>这是四个工具中设计最讲究的一个。<br>输入<br>当前 Pi 支持一次提交多个互不重叠的修改:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"path"</span><span class=punctuation>:</span> <span class=string>"src/UserService.php"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"edits"</span><span class=punctuation>:</span> <span class=punctuation>[</span></span><br><span class=line> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"oldText"</span><span class=punctuation>:</span> <span class=string>"public function find($id)"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"newText"</span><span class=punctuation>:</span> <span class=string>"public function find(int $id): ?User"</span></span><br><span class=line> <span class=punctuation>}</span><span class=punctuation>,</span></span><br><span class=line> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"oldText"</span><span class=punctuation>:</span> <span class=string>"return $this->users[$id];"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"newText"</span><span class=punctuation>:</span> <span class=string>"return $this->users[$id] ?? null;"</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>]</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>内部原理<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br></pre><td class=code><pre><span class=line>读取原文件</span><br><span class=line>→ 去掉 BOM</span><br><span class=line>→ 将 CRLF/CR 统一成 LF</span><br><span class=line>→ 在原始文件中寻找每个 oldText</span><br><span class=line>→ 检查每段是否唯一、是否重叠</span><br><span class=line>→ 执行全部替换</span><br><span class=line>→ 恢复原来的换行符和 BOM</span><br><span class=line>→ 写回文件</span><br><span class=line>→ 生成 diff 和 unified patch</span><br></pre></table></figure><p>关键要求是:oldText 必须精确匹配,而且在原文件里唯一。<br>例如文件中有两个:<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>return</span> <span class=literal>null</span>;</span><br></pre></table></figure><p>模型如果只提交:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"oldText"</span><span class=punctuation>:</span> <span class=string>"return null;"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"newText"</span><span class=punctuation>:</span> <span class=string>"throw new RuntimeException();"</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>工具会拒绝,因为不知道应该修改哪一个。<br>模型必须提供足够的上下文:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"oldText"</span><span class=punctuation>:</span> <span class=string>"public function load(): ?User\n{\n return null;\n}"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"newText"</span><span class=punctuation>:</span> <span class=string>"public function load(): ?User\n{\n return $this->repository->first();\n}"</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>为什么采用精确替换<ul><li>它相当于一种轻量级“乐观锁”:<li>模型看到文件版本 A<li>模型根据 A 生成 oldText<li>如果调用工具前文件被改成版本 B<li>oldText 匹配失败<li>工具拒绝覆盖,要求模型重新读取<br>这比让模型直接按行号修改更安全,因为文件新增一行后,行号可能全部偏移。<br>执行成功后,Pi 同时生成:<li>面向终端显示的彩色 diff<li>标准 unified patch<li>第一处变化的行号</ul><h3 id=bash-给模型提供“手脚”><a title="bash 给模型提供“手脚”" class=headerlink href=#bash-给模型提供“手脚”></a>bash 给模型提供“手脚”</h3><p>输入<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"command"</span><span class=punctuation>:</span> <span class=string>"php -l src/UserService.php && php tests/run.php"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"timeout"</span><span class=punctuation>:</span> <span class=number>30</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>原理:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>找到当前平台的 Shell</span><br><span class=line>→ 在工作目录启动子进程</span><br><span class=line>→ 注入环境变量</span><br><span class=line>→ 同时监听 stdout/stderr</span><br><span class=line>→ 实时把输出推送到 TUI</span><br><span class=line>→ 等待退出码</span><br><span class=line>→ 将结果返回给模型</span><br></pre></table></figure><p>它可以完成:<ul><li><code>rg</code>、<code>find</code>、<code>ls</code> 搜索项目<li>运行测试<li>调用编译器<li>执行 Git 命令<li>安装依赖<li>启动构建脚本<li>执行任意系统程序<li>输出处理<br>与 <code>read</code> 保留“开头”不同,<code>bash</code> 更适合保留“结尾”。<br>因为测试、编译和日志输出的错误通常在末尾。因此 Pi 保留最后 2000 行或最后 50KB。如果发生截断,完整输出会另外保存到临时文件。</ul><h3 id=四个工具如何协作><a class=headerlink href=#四个工具如何协作 title=四个工具如何协作></a>四个工具如何协作</h3><p>假设用户说:<br>给项目增加一个 /health 接口并运行测试。<p>模型可能按下面的流程工作:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br></pre><td class=code><pre><span class=line>1. bash</span><br><span class=line> rg "Route|router" src</span><br><span class=line></span><br><span class=line>2. read</span><br><span class=line> 读取找到的路由文件</span><br><span class=line></span><br><span class=line>3. edit</span><br><span class=line> 精确插入 /health 路由</span><br><span class=line></span><br><span class=line>4. write</span><br><span class=line> 创建新的 HealthController.php</span><br><span class=line></span><br><span class=line>5. bash</span><br><span class=line> php -l ... && php tests/run.php</span><br><span class=line></span><br><span class=line>6. read</span><br><span class=line> 如果测试失败,读取相关源文件</span><br><span class=line></span><br><span class=line>7. edit</span><br><span class=line> 修复问题</span><br><span class=line></span><br><span class=line>8. bash</span><br><span class=line> 再次运行测试</span><br></pre></table></figure><p>这就是 Coding Agent 的核心闭环:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>观察(read/bash)</span><br><span class=line>→ 决策(LLM)</span><br><span class=line>→ 修改(edit/write)</span><br><span class=line>→ 验证(bash)</span><br><span class=line>→ 再观察</span><br></pre></table></figure><p>大致学一下这个工具的工具逻辑</div><footer class=post-footer><div class=post-eof></div></footer></article></div><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/08/02/%E5%A6%82%E4%BD%95%E5%9C%A8%E5%91%BD%E4%BB%A4%E8%A1%8C%E4%B8%AD%E8%BE%93%E5%87%BA%E8%BF%9B%E5%BA%A6%E6%9D%A1/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/08/02/%E5%A6%82%E4%BD%95%E5%9C%A8%E5%91%BD%E4%BB%A4%E8%A1%8C%E4%B8%AD%E8%BE%93%E5%87%BA%E8%BF%9B%E5%BA%A6%E6%9D%A1/ itemprop=url>如何在命令行中输出进度条</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-08-02 21:26:43 / 修改时间:21:29:37" datetime=2026-08-02T21:26:43+08:00>2026-08-02</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/%E5%B0%8F%E6%8A%80%E5%B7%A7/ itemprop=url rel=index><span itemprop=name>小技巧</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-increment=false data-path=/2026/08/02/%E5%A6%82%E4%BD%95%E5%9C%A8%E5%91%BD%E4%BB%A4%E8%A1%8C%E4%B8%AD%E8%BE%93%E5%87%BA%E8%BF%9B%E5%BA%A6%E6%9D%A1/ data-title=如何在命令行中输出进度条 title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/08/02/%E5%A6%82%E4%BD%95%E5%9C%A8%E5%91%BD%E4%BB%A4%E8%A1%8C%E4%B8%AD%E8%BE%93%E5%87%BA%E8%BF%9B%E5%BA%A6%E6%9D%A1/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/08/02/如何在命令行中输出进度条/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><p>之前就比较好奇,命令行一般输出都是往后追加,好像不知道怎么做覆盖更新,但是很多比较厉害的shell脚本都有那种进度条,甚至整个重绘<br>所以就来看下,怎么在命令行里输出个会变的进度条,<br>比如我在命令行中输出两行<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=keyword>echo</span> <span class=string>"111\n"</span>;</span><br><span class=line><span class=keyword>echo</span> <span class=string>"222\n"</span>;</span><br><span class=line><span class=keyword>exit</span>;</span><br></pre></table></figure><p>最简单就是这样子,那如果想把111改成222呢<br>我们发现”\n”是换行,那”\r”其实是原地移动到行首<br>可以再看下<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=keyword>echo</span> <span class=string>"111\r"</span>;</span><br><span class=line><span class="title function_ invoke__">usleep</span>(<span class=number>1000</span>*<span class=number>1000</span>);</span><br><span class=line><span class=keyword>echo</span> <span class=string>"222\n"</span>;</span><br><span class=line><span class=keyword>exit</span>;</span><br></pre></table></figure><p>加了sleep是因为闪太快了看不到,这样就能看到这一行就重新刷新了<br>然后就可以看下进度条可以怎么输出了<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line><span class=keyword>for</span> (<span class=variable>$i</span> = <span class=number>0</span>; <span class=variable>$i</span> <= <span class=number>50</span>; <span class=variable>$i</span>++) {</span><br><span class=line> <span class="title function_ invoke__">printf</span>(<span class=string>"progress: [%-50s] %d%%\r"</span>, <span class="title function_ invoke__">str_repeat</span>(<span class=string>'#'</span>,<span class=variable>$i</span>), <span class=variable>$i</span> * <span class=number>2</span>);</span><br><span class=line> <span class="title function_ invoke__">usleep</span>(<span class=number>1000</span> * <span class=number>100</span>);</span><br><span class=line>}</span><br><span class=line><span class=keyword>echo</span> <span class=string>"\n"</span>;</span><br><span class=line><span class=keyword>echo</span> <span class=string>"Done.\n"</span>;</span><br></pre></table></figure><p>首先是解释下这段的含义<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>"progress: [%-50s] %d%%\r"</span><br><span class=line> └──┬──┘ │ │ └─ 回到当前行开头</span><br><span class=line> │ │ └──── 输出一个真正的 %</span><br><span class=line> │ └────── 输出整数</span><br><span class=line> └──────────── 输出宽度为 50 的字符串</span><br></pre></table></figure><p>感觉又变成了一个视觉游戏,以为是逐渐的在往后画<br>其实是一直在刷新这个进度条<br>首先是定义一个长度是50个字符的输出,用空格补全,然后会输出对应百分比,比如2%<br>就对应的是一个井号,然后用”\r”回到行首重绘<br>实际的就是第一次画一个#号,第二次画两个#号<br>这里最大的重点就是要让部分字符看着在变,部分看着不变,才能形成这种视觉假象<br>比如这里的50个字符长度,还有对应的”%”符号<br>否则就会变成一直在跳的了<br>对应的动的部分就是#号的增加,对应百分比数值也在往上涨<br>不过这里有点小问题,不知道有没有发现,它的数字没有定义位数<br>那么在达到100%的时候其实是有一点跳跃的<p>那如果想要改多行怎么办呢<br>就需要用ANSI 控制序列让光标向上移动若干行<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>任务 A:[######## ] 40%</span><br><span class=line>任务 B:[############ ] 60%</span><br><span class=line>总进度:[########## ] 50%</span><br></pre></table></figure><p>光标此时位于这三行下面。下一次刷新前,先让光标向上移动三行:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ESC [ 3 A</span><br></pre></table></figure><p>在php中可以写成<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>echo</span> <span class=string>"\033[3A"</span>;</span><br></pre></table></figure><p>最简单的就是<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line><span class=keyword>echo</span> <span class=string>"111\n"</span>;</span><br><span class=line><span class=keyword>echo</span> <span class=string>"222\n"</span>;</span><br><span class=line><span class=keyword>echo</span> <span class=string>"333\n"</span>;</span><br><span class=line><span class=keyword>echo</span> <span class=string>"\033[3A"</span>;</span><br><span class=line><span class="title function_ invoke__">usleep</span>(<span class=number>1000</span>*<span class=number>1000</span>);</span><br><span class=line><span class=keyword>echo</span> <span class=string>"444\n"</span>;</span><br></pre></table></figure><p>这样就会把前面的111,222,333给干掉,重新输出444<br>来个像样点的脚本就是<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br></pre><td class=code><pre><span class=line><span class=meta>&LT?php</span></span><br><span class=line></span><br><span class=line><span class=function><span class=keyword>function</span> <span class=title>progressBar</span>(<span class=params><span class=keyword>int</span> <span class=variable>$percent</span>, <span class=keyword>int</span> <span class=variable>$width</span> = <span class=number>30</span></span>): <span class=title>string</span></span></span><br><span class=line><span class=function></span>{</span><br><span class=line> <span class=variable>$percent</span> = <span class="title function_ invoke__">max</span>(<span class=number>0</span>, <span class="title function_ invoke__">min</span>(<span class=number>100</span>, <span class=variable>$percent</span>));</span><br><span class=line> <span class=variable>$filled</span> = (<span class=keyword>int</span>) <span class="title function_ invoke__">floor</span>(<span class=variable>$percent</span> * <span class=variable>$width</span> / <span class=number>100</span>);</span><br><span class=line></span><br><span class=line> <span class=keyword>return</span> <span class="title function_ invoke__">sprintf</span>(</span><br><span class=line> <span class=string>"[%-<span class=subst>{$width}</span>s] %3d%%"</span>,</span><br><span class=line> <span class="title function_ invoke__">str_repeat</span>(<span class=string>'#'</span>, <span class=variable>$filled</span>),</span><br><span class=line> <span class=variable>$percent</span></span><br><span class=line> );</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=variable>$lineCount</span> = <span class=number>3</span>;</span><br><span class=line></span><br><span class=line><span class=keyword>for</span> (<span class=variable>$i</span> = <span class=number>0</span>; <span class=variable>$i</span> <= <span class=number>100</span>; <span class=variable>$i</span>++) {</span><br><span class=line> <span class=variable>$taskA</span> = <span class=variable>$i</span>;</span><br><span class=line> <span class=variable>$taskB</span> = <span class="title function_ invoke__">min</span>(<span class=number>100</span>, <span class=variable>$i</span> * <span class=number>2</span>);</span><br><span class=line> <span class=variable>$total</span> = (<span class=keyword>int</span>) ((<span class=variable>$taskA</span> + <span class=variable>$taskB</span>) / <span class=number>2</span>);</span><br><span class=line></span><br><span class=line> <span class=comment>// 第一次输出前不需要移动光标</span></span><br><span class=line> <span class=keyword>if</span> (<span class=variable>$i</span> > <span class=number>0</span>) {</span><br><span class=line> <span class="title function_ invoke__">printf</span>(<span class=string>"\033[%dA"</span>, <span class=variable>$lineCount</span>);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=variable>$lines</span> = [</span><br><span class=line> <span class=string>"任务 A:"</span> . <span class="title function_ invoke__">progressBar</span>(<span class=variable>$taskA</span>),</span><br><span class=line> <span class=string>"任务 B:"</span> . <span class="title function_ invoke__">progressBar</span>(<span class=variable>$taskB</span>),</span><br><span class=line> <span class=string>"总进度:"</span> . <span class="title function_ invoke__">progressBar</span>(<span class=variable>$total</span>),</span><br><span class=line> ];</span><br><span class=line></span><br><span class=line> <span class=keyword>foreach</span> (<span class=variable>$lines</span> <span class=keyword>as</span> <span class=variable>$line</span>) {</span><br><span class=line> <span class=comment>// 回到行首并清除当前整行</span></span><br><span class=line> <span class=keyword>echo</span> <span class=string>"\r\033[2K"</span>;</span><br><span class=line> <span class=keyword>echo</span> <span class=variable>$line</span>;</span><br><span class=line> <span class=keyword>echo</span> PHP_EOL;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class="title function_ invoke__">fflush</span>(STDOUT);</span><br><span class=line> <span class="title function_ invoke__">usleep</span>(<span class=number>100000</span>);</span><br><span class=line>}</span><br></pre></table></figure><p>这里的符号代表的含义<table><thead><tr><th>控制序列<th>作用<tbody><tr><td><code>\r</code><td>回到当前行行首<tr><td><code>\033[2K</code><td>清除当前整行<tr><td><code>\033[3A</code><td>光标向上移动三行<tr><td><code>\033[1A</code><td>光标向上移动一行<tr><td><code>\033[1B</code><td>光标向下移动一行<tr><td>\033 表示 ESC 字符,所以:<td></table><figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>echo</span> <span class=string>"\033[3A"</span>;</span><br></pre></table></figure><p>发送给终端的实际含义是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ESC + [3A</span><br></pre></table></figure><p>也就是“光标向上移动三行”。<br>还是比较神奇的东西<br>以前一直没搞明白,刚好这次想到了<br>现在很多想claude code,包括opencode,pi等都是搞成了命令行的<br>这个大概就是这些命令行工具的基础<br>像opencode是使用的<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>@opentui/core</span><br><span class=line>@opentui/solid</span><br><span class=line>solid-js</span><br></pre></table></figure><p>pi则是使用<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>@earendil-works/pi-tui</span><br><span class=line>其他的框架还有</span><br></pre></table></figure><table><thead><tr><th>语言<th>开源库<th>定位<tbody><tr><td>TypeScript<td><a href=https://github.com/anomalyco/opentui rel=noopener target=_blank>OpenTUI</a><td>OpenCode 使用的完整 TUI<tr><td>TypeScript<td><a href=https://github.com/badlogic/pi-mono/tree/main/packages/tui rel=noopener target=_blank>pi-tui</a><td>Pi 使用的轻量差量渲染<tr><td>JavaScript<td><a href=https://github.com/vadimdemedes/ink rel=noopener target=_blank>Ink</a><td>使用 React 编写 CLI<tr><td>JavaScript<td><code>cli-progress</code><td>单个或多个进度条<tr><td>Go<td><a href=https://github.com/charmbracelet/bubbletea rel=noopener target=_blank>Bubble Tea</a><td>Elm 风格 TUI 框架<tr><td>Rust<td><a href=https://github.com/ratatui/ratatui rel=noopener target=_blank>Ratatui</a><td>Rust 生态常用 TUI<tr><td>PHP<td><a href=https://symfony.com/doc/current/console.html#output-sections rel=noopener target=_blank>Symfony Console</a><td>进度条、表格和多区域刷新<tr><td>PHP 里最省事的是 Symfony Console 的 section()<td><td></table><figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br></pre><td class=code><pre><span class=line><span class=keyword>use</span> <span class=title>Symfony</span>\<span class=title>Component</span>\<span class=title>Console</span>\<span class=title>Output</span>\<span class=title>ConsoleOutput</span>;</span><br><span class=line></span><br><span class=line><span class=variable>$output</span> = <span class=keyword>new</span> <span class="title class_">ConsoleOutput</span>();</span><br><span class=line><span class=variable>$section</span> = <span class=variable>$output</span>-><span class="title function_ invoke__">section</span>();</span><br><span class=line></span><br><span class=line><span class=variable>$section</span>-><span class="title function_ invoke__">writeln</span>(<span class=string>"任务 A:10%\n任务 B:20%\n任务 C:30%"</span>);</span><br><span class=line></span><br><span class=line><span class="title function_ invoke__">sleep</span>(<span class=number>1</span>);</span><br><span class=line></span><br><span class=line><span class=variable>$section</span>-><span class="title function_ invoke__">overwrite</span>(</span><br><span class=line> <span class=string>"任务 A:40%\n任务 B:50%\n任务 C:60%"</span></span><br><span class=line>);</span><br></pre></table></figure></div><footer class=post-footer><div class=post-eof></div></footer></article></div><nav class=pagination><span class="page-number current">1</span><a class=page-number href=/page/2/>2</a><span class=space>…</span><a class=page-number href=/page/78/>78</a><a class="extend next" aria-label=下一页 href=/page/2/ rel=next title=下一页><i class="fa fa-angle-right"></i></a></nav></div></main><footer class=footer><div class=footer-inner><div class=beian><a href=https://beian.miit.gov.cn/ rel=noopener target=_blank>浙ICP备16002580号-2 </a><img alt src=/uploads/beian.png><a href=https://beian.mps.gov.cn/#/query/webSearch?code=33010602011094 rel=noopener target=_blank>浙公网安备 33010602011094号</a></div><div class=copyright>© <span itemprop=copyrightYear>2026</span><span class=with-love><i class="fa fa-heart"></i> </span><span class=author itemprop=copyrightHolder>Nicksxs</span></div><div class=busuanzi-count><span class=post-meta-item id=busuanzi_container_site_uv><span class=post-meta-item-icon><i class="fa fa-user"></i> </span><span class=site-uv title=总访客量><span id=busuanzi_value_site_uv></span> </span></span><span class=post-meta-item id=busuanzi_container_site_pv><span class=post-meta-item-icon><i class="fa fa-eye"></i> </span><span class=site-pv title=总访问量><span id=busuanzi_value_site_pv></span></span></span></div></div></footer><div aria-label=返回顶部 class=back-to-top role=button><i class="fa fa-arrow-up fa-lg"></i><span>0%</span></div><a aria-label="在 GitHub 上关注我" title="在 GitHub 上关注我" class=github-corner href=https://github.com/nicksxs rel=noopener target=_blank><svg viewbox="0 0 250 250" aria-hidden=true height=80 width=80><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" style="transform-origin:130px 106px" class=octo-arm fill=currentColor></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" class=octo-body fill=currentColor></path></svg></a><noscript><div class=noscript-warning>Theme NexT works best with JavaScript enabled</div></noscript><script crossorigin=anonymous integrity=sha256-XL2inqUJaslATFnHdJOi9GfQ60on8Wx1C2H8DYiN1xY= src=https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js></script><script crossorigin=anonymous integrity=sha256-ytMJGN3toR+a84u7g7NuHm91VIR06Q41kMWDr2pq7Zo= src=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/5.0.28/fancybox/fancybox.umd.js></script><script crossorigin=anonymous integrity=sha256-mOFREFhqmHeQbXpK2lp4nA3qooVgACfh88fpJftLBbc= src=https://cdnjs.cloudflare.com/ajax/libs/lozad.js/1.16.0/lozad.min.js></script><script src=/js/comments.js></script><script src=/js/utils.js></script><script src=/js/motion.js></script><script src=/js/next-boot.js></script><script crossorigin=anonymous integrity=sha256-DABVk+hYj0mdUzo+7ViJC6cwLahQIejFvC+my2M/wfM= src=https://cdnjs.cloudflare.com/ajax/libs/algoliasearch/4.20.0/algoliasearch-lite.umd.js></script><script crossorigin=anonymous integrity=sha256-9242vN47QUX50UG5Gf5XDO1YREWCEJRyXHofh5fsl24= src=https://cdnjs.cloudflare.com/ajax/libs/instantsearch.js/4.60.0/instantsearch.production.min.js></script><script src=/js/third-party/search/algolia-search.js></script><script src=/js/third-party/fancybox.js></script><script async src=https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js></script><script crossorigin=anonymous integrity=sha256-yvJQOINiH9fWemHn0vCA5lsHWJaHs6/ZmO+1Ft04SvM= src=https://cdnjs.cloudflare.com/ajax/libs/quicklink/2.3.0/quicklink.umd.js></script><script class=next-config data-name=quicklink type=application/json>{"enable":false,"home":false,"archive":false,"delay":true,"timeout":3000,"priority":true,"url":"https://nicksxs.me/"}</script><script src=/js/third-party/quicklink.js></script><script data-cloudflare-page-counter-config type=application/json>{"enable":true,"endpoint":"https://counter.nicksxs.me","site":"nicksxs","local_cache":true,"timeout":5000,"icon":"far fa-eye","item_text":true}</script><script src=/js/cloudflare-page-counter.js></script><script class=next-config data-name=disqus type=application/json>{"enable":true,"shortname":"nicksxs","count":true,"i18n":{"disqus":"disqus"}}</script><script src=/js/third-party/comments/disqus.js></script>