
简介这是一份面向Java初中级学习者的输入法实现示例以单个Java源文件展现输入法核心流程适合想了解键盘事件处理、字符编码转换与图形界面交互的开发者阅读。资源压缩包体积仅6KB共1个java文件代码紧凑且无额外依赖便于直接导入IDE研读。目前已有900余人学习下载是快速入门输入法原理的轻量参考。示例覆盖了输入法开发中常用的关键点通过键盘监听获取用户按键结合字符编码完成拼音到汉字的转换借助界面组件实现候选字展示与选择同时涉及字库词库的简单组织方式以及多线程在输入响应中的运用。阅读这份代码读者可以直观看到Java事件模型、I/O读取和数据结构如Trie树思路在真实小项目中的协作并可作为扩展为带联想、词频调整功能的输入法雏形。对于正在做Java课程设计或对输入法机制感兴趣的人这份资料能帮助快速建立整体框架减少从零摸索的时间。1. Java输入法为什么值得自己写不是伪命题也不需要逆天改命输入法这名字听着像个系统级黑匣子其实拆开看就三件事接收按键、找候选、把选中文本塞回屏幕。用 Java 做这三件事能完整搭出一个拼音输入法而且还能跑在 Windows、Linux、macOS 三套桌面环境里。跟 C 写的系统级输入法比它确实抢不过原生全局钩子但如果目标是在自己的桌面程序、远程桌面会话、机房终端里嵌一套受控输入方案Java 这条路线反而是主流方案里最好维护的。我见过不少团队在这个标题下翻车多半是上来就想接管整个操作系统的按键流结果卡在 JNI 和线程模型里。正确做法是用 Java 写输入引擎用 Swing 画候选框用系统自带的文本注入能力输出结果把系统钩子这块留给最薄的一层。这样最少只需要一个可执行 JAR 和一个启动脚本就能得到一个能打的输入法。这套路径适合谁适合正在给定制终端、内部管理系统、远程控制台做“壳”的开发者。读完这篇文章你可以搭出一个能拼音选字、能出候选、能配置快捷键的 Java 输入法雏形并清楚哪些地方是后面翻车的重灾区。2. 先画架构图Java负责“想”操作系统负责“搬”输入法不是单一组件是三个松耦合模块的组合输入会话引擎、候选面板、文本注入器。下面先把这三块拆清楚。2.1 输入法的三个职能拆开看引擎、候选框、注入器第一份是输入会话引擎。它接收按键内部维护一个“正在输入的拼音串”。用户敲了 n-i-h-a-o引擎要能算出一组候选词并且能响应上下翻页、数字选字、退格回退。引擎不关心候选词显示在屏幕哪个位置那是候选框的事。第二份是候选框。它是独立于引擎的一个浮层负责把候选词画出来同时响应鼠标点击。候选框跟引擎的通信接口很小这次候选是什么、用户选了哪个。这个接口小到可以抽成一个 interface对 UI 实现完全隔离。第三份是文本注入器。它负责把最终确定的字符串“塞”到当前获得焦点的输入框里。Java 里常见做法是用 Robot 模拟粘贴或者通过 JNA 调用系统输入法 API。如果只在自己的程序里做直接调用目标输入框的替换文本接口比模拟按键稳定得多。把三份拆开后数据流是这样的按键事件先交给引擎引擎更新拼音串并返回候选列表给 UIUI 在浮层里展示用户按下空格或数字UI 把选中词交给注入器注入器把文本送进焦点控件然后通知引擎清空当前会话。整个过程就是一个有限状态机不是玄学。2.2 为什么纯SwingRobot能跑起来取舍和代价一个常见误区是一定要用java.awt.im.spi.InputMethod把自己注册成系统输入法否则不叫输入法。这条路在理论上有 Java 原生扩展点但发布时依赖 JRE 扩展机制而且新版 JDK 已经删掉了不少相关线程和上下文接口。现实项目里几乎没人走这条路我建议直接放弃。如果你只想要一个能用的桌面输入法常见做法是引擎用纯 Java 写UI 用 Swing 的 JWindow 浮层注入用 java.awt.Robot。这么搭配的好处是零第三方依赖一个 JRE 就能跑。代价是你无法全局拦截物理按键只能在能监听到 KeyEvent 的应用内生效。我在模拟项目X里就是这么干的主程序是一个文本编辑器自定义 JFrame 监听 KeyEvent把原始按键转发进引擎候选框用 JWindow 浮在输入框下方。Robot 在拿到确定文本后把文本粘贴到焦点组件。整套代码不到一千行完成了中文输入。你会发现这个模型根本不试图跟系统输入法抢底层按键只是在应用层横插了一刀。这个取舍换来的是跨平台一致性。进程内按键事件、EDT 线程模型、中文字符串处理三件套在所有桌面平台行为一致。跟 JNI 原生钩子比少了“自己机器上好好的到现场就崩”的血泪经历。代价也明显CtrlSpace 这类系统快捷键你拦不到用户在别的程序里切不出这个输入法所以它更适合作为嵌入式输入法存在。如果你一定要做全局输入法那语言选择就值得重新考量。Java 并非不能做但你需要通过 JNI/JNA 注册全局键盘钩子或者在每个目标平台上维护独立的辅助进程。这会把你从“写输入法”带进“写平台适配器”的泥潭和标题要的东西已经不是一回事。2.3 最小可运行骨架状态机与事件循环输入法引擎内部状态只有三态空闲、拼音输入中、候选中。下面用一个最小骨架说清楚状态转移。先看状态定义public enum SessionState { IDLE, // 空闲没有输入等待字母键 INPUTTING, // 输入中已经积累拼音串但还没出候选 CANDIDATE // 候选中有拼音串且有候选结果 }接着是 InputSession 的核心逻辑。这里故意不接词库先让状态机跑起来public class InputSession { private final StringBuilder pinyin new StringBuilder(); private SessionState state SessionState.IDLE; public void onKey(int code, char keyChar) { if (code KeyEvent.VK_BACK_SPACE) { handleBackspace(); return; } if (code KeyEvent.VK_SPACE state SessionState.CANDIDATE) { // 真实实现这里会选中第一个候选这里先留空 return; } if (Character.isLetter(keyChar)) { pinyin.append(Character.toLowerCase(keyChar)); state SessionState.INPUTTING; refreshCandidates(); } } private void handleBackspace() { if (pinyin.length() 0) { pinyin.deleteCharAt(pinyin.length() - 1); refreshCandidates(); } else if (state ! SessionState.IDLE) { state SessionState.IDLE; } } private void refreshCandidates() { if (pinyin.length() 0) { state SessionState.IDLE; return; } state SessionState.CANDIDATE; // 这里在后续章节会接词典查询把结果交给 UI } }这段代码的逻辑很直白字母进来就进入输入中状态空格和数字选字退格删掉末尾字母或者退出会话。参数上有一个点必须注意Swing 的 KeyEvent 在字母键上提供的大小写字符可能不一致所以进拼音串前统一转小写否则后面 Trie 查询会漏词。这个骨架看着简单但我建议从一开始就把状态转移锁在同一个类里所有 UI 都不准直接改 pinyin 内容只能调用 onKey。否则后面加候选、加撤销、加用户自造词时状态会乱得连自己都看不懂。这一步做对了后面的坑能少三分之一。关于事件循环还有一个容易被忽略的原则所有按键事件和 UI 更新都发生在 EDT 上如果在 onKey 里做耗时操作界面会卡。这个骨架里 onKey 是同步方法只是一个起点。下一章把字典查询接入后必须把查询挪到后台线程EDT 只做状态变更和 UI 刷新。3. 拼音转汉字Trie索引和候选排序的落地实现3.1 词库数据长什么样一个TXT词表撑起MVP客户端输入法本质是离线查表。最小可用词库只需要一个 UTF-8 编码的文本文件每行一条“拼音 词 词频”。例如nihao 你好 32800 nihao 泥好 2200 xiexie 谢谢 41200 jianyi 建议 35600 jianyi 简易 8010拼音约定俗成用不带声调的全拼。因为键盘产生的原始输入就没有声调候选排序靠词频和上下文靠声调做首要过滤会自缚手脚。词频字段是候选排序的主键来源可以是现代汉语词频统计也可以是项目自己的日志。如果你不想找第三方词频可以先用“在语料里出现的次数”代替比如从新闻语料里统计效果也能接受。词库解析代码很简单难的是别写错编码。我习惯这样加载public record DictEntry(String pinyin, String word, int frequency) {} public void loadDictFromFile(String path) throws IOException { Files.lines(Path.of(path), StandardCharsets.UTF_8) .filter(line - !line.isBlank()) .map(line - line.trim().split(\\s)) .filter(parts - parts.length 3) .forEach(parts - dict.addEntry( new DictEntry(parts[0], parts[1], Integer.parseInt(parts[2])))); }这里最需要看的是StandardCharsets.UTF_8。如果你用 BufferedReader.readLine 再手动转字节在 Windows 上很容易踩到默认编码问题中文词条变成乱码。另外\\s切分能容忍词频前后有空格但要注意词本身绝不能带空格否则行结构会碎。词频解析失败时我建议直接跳过该行并打 warning而不是让整个加载崩掉因为词库文件难免有一两行是脏数据。加载完可以去重。同一个拼音下可能出现同一词的重复记录如果不处理候选里会重复出现“你好”。简单做法是在 addEntry 里用 Map 检查或者先读进 List 再按pinyin word去重。我跳过这一步时在模拟项目X里翻过一次车候选列表连续出现两个“谢谢”看起来非常业余。3.2 用Trie处理“nihao”到“你好”有了词库现在做索引。拼音输入的核心操作是给定前缀串 nih快速找出所有以它开头的词。这个操作用 Trie 最直观每个节点对应一个字母节点挂一个词条列表。构建和查询都按字母走。下面这个实现保留了最小可用路径去掉了一些防御代码方便看清主流程public class PinyinTrie { private static class TrieNode { final MapCharacter, TrieNode children new HashMap(); final ListWordHit hits new ArrayList(); } public record WordHit(String word, int frequency) {} private final TrieNode root new TrieNode(); public void insert(String pinyin, String word, int frequency) { TrieNode node root; for (char c : pinyin.toCharArray()) { node node.children.computeIfAbsent(c, k - new TrieNode()); } node.hits.add(new WordHit(word, frequency)); } public ListWordHit search(String prefix) { TrieNode node root; for (char c : prefix.toCharArray()) { node node.children.get(c); if (node null) { return List.of(); } } ListWordHit result new ArrayList(); collect(node, result); return result; } private void collect(TrieNode node, ListWordHit out) { out.addAll(node.hits); for (TrieNode child : node.children.values()) { collect(child, out); } } }这里的关键逻辑是insert 沿着拼音字母一路创建节点在最终节点上挂词search 先顺着前缀走到节点再深度遍历子树捞词。由于拼音字母表很小HashMap 的方向数不会超过 26这里用 Map 完全没问题。两个参数值得说一下。第一search 的递归在拼音短时很安全但前缀长度只有 1 时会拉出成千上万词所以在调用方最好做保护拼音串长度小于 2 时直接不刷新候选。第二如果你要返回 topN不要先 collect 全部再排队那样浪费很大建议在 collect 过程中维护一个小顶堆或者在节点设计上额外存每层最大频率做剪枝。MVP 阶段直接 collect 后排序也能接受但要在前缀长度上设限。3.3 词频和用户自造词让候选排序不那么蠢Trie 只负责捞出候选排序必须交给独立的排序器。排序规则一般分三档精确匹配整个拼音串的词优先第二看词频第三看用户最近选择。MVP 阶段可以先只按词频降序然后把用户选择过的词放到单独的 userLexicon 里选一次词频加一分并且写回磁盘持久化。用户词表的最小实现public class UserLexicon { private final MapString, Integer extraFrequency new HashMap(); public void boost(String word) { extraFrequency.merge(word, 1, Integer::sum); } public int frequencyOf(String word) { return extraFrequency.getOrDefault(word, 0); } }排序时把字典词频和用户词频相加得到最终权重。这里有一个我反复掉的坑不要一开始就允许用户把任意字符串硬编码进字典。允许用户把“nihaoma”存成“你好吗”当然可以但如果不给低默认权重它会在“ni”前缀下冲到最前面。因为只要拼音匹配用户词权重默认很高。建议给用户自造词一个基础权重比如 50只有反复被选中的词才慢慢涨上去。一个好的调参习惯是把“基础用户词权重”“每选中一次加分”做成两个可配置参数。默认 50 / 1 意味着一个词要被选中五次才追上一个词频 100 的普通词。每个项目对常用词和自造词的敏感度不同这个参数最终要靠回归测试来收敛不能拍脑袋。3.4 多音字和双拼先别做但要知道边界多音字不需要在 Trie 里做特殊结构因为用户输入的是不带声调的拼音多音字的另一个读音也会命中同一个词。比如“银行”的“行”有人打 hang有人打 xing词库里“yinhang 银行 50000”和“yinxing 银行 8000”都存在要靠词频把常用读音顶到前面这是排序层的问题不是索引层的问题。理解了这一点你的 Trie 结构就不用为多音字改版。双拼则是另一个话题。双拼本质是把按键序列映射成全拼再走现有 Trie。映射关系可以做成一张配置表比如l - sh、v - ü。Java 实现双拼没有任何障碍麻烦的是每种双拼方案小鹤、微软、自然码都有不同映射表需要维护。我建议第一个版本只做全拼把拼音到拼音的映射接口预留出来即可。等全拼词库和候选排序稳定了再花半天时间加一张双拼映射表收益很大且不会伤到核心结构。4. 候选面板与输出注入把按键序列变成屏幕上的字4.1 JWindow候选面板跟随光标、失焦隐藏候选面板的本质是无边框 JWindow不抢焦点。它跟着文本输入框的光标走所以需要一个锚点坐标。在应用内输入框场景可以用当前焦点组件的插入符位置来定位JTextComponent target (JTextComponent) KeyboardFocusManager.getCurrentKeyboardFocusOwner(); if (target null) { return; } Point locationOnScreen target.getLocationOnScreen(); Rectangle caretRect target.getCaret().getMagicCaretPosition(); if (caretRect null) { return; // 输入框刚获得焦点时这个值可能是 null } candidateWindow.setLocation( locationOnScreen.x caretRect.x, locationOnScreen.y caretRect.y caretRect.height 2);这段代码有两个必须处理的边界getMagicCaretPosition()可能为 null特别是在输入框刚挂上焦点状态时getLocationOnScreen()在窗口未显示时也会抛异常。输入法这种和焦点强绑定的模块任何一步都不值得赌全部判空后再设置位置。候选框的 JWindow 在创建时要设置setFocusableWindowState(false)内部 JList 也设setFocusable(false)。否则用户点击候选框会带走焦点正在输入的拼音串会因为焦点丢失被引擎清空。设置不抢焦点后鼠标点击 JList 无法让 JList 获得焦点你需要把鼠标点击事件转成选词命令直接调用引擎 commit 而不是点击组件本身。失焦隐藏的逻辑也要有。在主窗口的 WindowFocusListener 里窗口失去焦点时调用candidateWindow.setVisible(false)并且session.reset()。注意顺序先重置会话再隐藏 UI否则会有一瞬间的候选框残留。4.2 Robot输出模拟回车的副作用与替代方案确认候选后怎么输出文本是另一个大坑。最省事的通用方案是用 Robot 模拟 CtrlVpublic static void injectText(String text) throws AWTException { Toolkit toolkit Toolkit.getDefaultToolkit(); Clipboard clipboard toolkit.getSystemClipboard(); clipboard.setContents(new StringSelection(text), (clip, contents) - {}); Robot robot new Robot(); robot.keyPress(KeyEvent.VK_CONTROL); robot.keyPress(KeyEvent.VK_V); robot.keyRelease(KeyEvent.VK_V); robot.keyRelease(KeyEvent.VK_CONTROL); }副作用很明显系统剪贴板被覆盖。如果不做恢复用户的复制历史就被打断了。常见做法是在 setContents 之前先通过clipboard.getContents(null)保存旧内容输出完成后延迟几百毫秒再恢复。恢复操作也有讲究延迟太短在某些编辑器里文本还没接受完就被覆盖我个人经验是 300ms 比较稳定但要在你目标平台上测一遍。Robot 方案更适合纯文本框在网页、IDE、富文本编辑器里会触发各自的粘贴副作用。文本框如果本身是 JTextComponent可以直接调用replaceSelection(text)完全不需要剪贴板这是最干净的方式。如果一定要跨应用另一个方向是 JNA 调用系统输入法 API把字符串直接注入焦点控件。这个方案平台差异很大第一个版本不建议碰先把候选和选词跑通注入方式留一个接口替换即可。4.3 四个值得调的参数候选数、超时、词频阈值、线程池输入法好不好用到最后就是参数调优。下面是我会最先调的四个参数和起始值参数起始值调参方向候选词数量9数字键 1-9 正好覆盖太大选字难太小依赖翻页拼音前缀最小长度2设为 1 时候选膨胀设为 4 后简拼失效用户词基础权重50太高会冲击系统词频太低自造词很难浮上来字典查询线程池4 线程主要为了避免百万级词库在低端机上卡 EDT候选数量 9 对应传统输入法数字键也正好是 JList 一屏显示不滚动的高度。我把“拼音前缀最小长度”单独拿出来做参数是因为它直接影响“简拼”。简拼模式下用户只输入声母比如 nh 出“你好”但这会把“你”“好”相关的长词全部拉出来所以很多输入法默认把它关掉或要求至少两个字母。这两个参数如果不配合用户敲一个字母就弹一屏幕候选属于新手必翻车现场。线程池参数看着多余其实很重要。Trie 查询在百万级词库时一次 collect 会产生大量临时对象。如果所有查询都在 EDT 上执行输入会变得一顿一顿。我的做法是让查询在独立线程池上执行再切回 EDT 更新 UICompletableFuture.supplyAsync(() - engine.topCandidates(pinyin, 9), queryPool) .thenAcceptAsync(cands - candidateList.setListData(cands.toArray(new String[0])), SwingUtilities::invokeLater);代码里的thenAcceptAsync的第二个参数指定了一个执行线程这里直接用SwingUtilities::invokeLater可以保证候选 JList 的更新发生在 EDT这是 Swing 不可违背的规则。另外查询结果要在回调里做一次版本号校验确认这次返回的拼音串仍然是最新的否则快速按键时会出现“旧拼音显示新候选”的串台现象。这个串台问题我后面会在避坑章节里展开。4.4 候选翻页与EDT刷新两个隐藏细节候选超过一页时需要上下翻页。PageUp/PageDown 在引擎里只改 pageIndex并不改变拼音串。选中逻辑是pageIndex * pageSize digit。状态机里建议把 pageIndex 也纳入会话状态退格回退时把 pageIndex 清零这个细节很容易漏漏掉的后果是翻页后删一个字母候选列表还停留在第二页。JWindow 还有一个隐藏性能问题透明背景和重绘会触发整层合成。如果候选面板在每次刷新时都修改背景图片低端机器上会有肉眼可见的闪烁。解决方法是把候选列表放在一个固定尺寸面板里通过repaint(rect)局部刷新而不是每次都revalidate()。这一条在 Linux 桌面上尤其是血泪经验合成器切换窗口时很容易掉帧。5. Java输入法避坑清单五个反复翻车的地方这一章的坑是在模拟项目X的输入法模块里反复撞出来的。每一条按现象、原因、解决三步写可以直接对照排错。5.1 中文标点混入英文串现象、原因、解决现象输入法明明确认了中文候选屏幕上的句子却夹着英文标点。更常见的是用户打中文时按了句号键英文句号直接穿透输入法原样进到文本里。原因onKey 里只处理了Character.isLetter标点按键没有进入引擎。Java 的 KeyEvent 中字母和标点是两条通道如果不在引擎做标点映射标点就会绕过会话状态直接落到焦点控件。解决在引擎里加一张全角标点映射表逗号 - 、句号 - 。。同时要区分当前是否正在拼音输入拼音串为空时输出半角非空时输出全角。我建议把这张表做成静态映射因为绝大多数标点行为在项目生命周期内不需要动态调整静态表够用且最快。5.2 候选面板黑匣子明明调用了repaint却不刷新现象词库更新后JList 的 setListData 被调用了屏幕上的候选却还是旧的一组。有人会怀疑 Swing 线程模型于是加 repaint()、revalidate() 全部无效。原因多半是在非 EDT 线程改了 JList 的 modelSwing 没有捕获线程冲突面板也不会主动重绘。另一种可能是候选 JWindow 的尺寸没有随新内容变长词被裁剪后看起来像没刷新。解决所有 model 的修改统一走SwingUtilities.invokeLater。如果已经这样做了还不刷新调用candidateWindow.pack()强制重新计算尺寸。还有一个隐蔽问题如果你用自定义 ListModel修改时要发fireContentsChanged否则 JList 拿不到变化事件。这套组合拳下来所谓“黑匣子”基本都能解决。5.3 焦点丢失导致输入串被截断现象正在输入拼音用户点击候选框拼音串即刻清空或者按 Tab 切到另一个控件再回来发现输入状态丢了。原因候选 JWindow 默认可能可聚焦点击会把焦点从原输入框抢走同时主窗口失去焦点后 KeyEvent 不再送到监听器但引擎状态还停留在 INPUTTING没有主动复位。用户再输入时前一个会话就半路截断。解决第一candidateWindow.setFocusableWindowState(false)JList 也setFocusable(false)这样点击面板不会抢焦点。第二监听主窗口WINDOW_LOST_FOCUS后决定是恢复还是复位推荐做成配置项。注意不要两个都做否则在某些桌面环境下会变成一个“永远无法失焦”的行为导致窗口切不出去。5.4 快捷键冲突输入法宁可不用CtrlSpace现象宿主程序里 CtrlSpace 是代码补全输入法也把中英文切换绑到 CtrlSpace结果补全功能失效输入法也切不出来。原因Java 应用内注册的 KeyEvent 监听和系统快捷键是叠加的。同一个快捷键被两个功能抢系统层面会先吃掉或者两个功能轮流触发行为完全不可预期。解决输入法自身不要注册系统级中英文切换键改为一个可配置的 KeyStroke。默认值选 CtrlShiftF 这类跟编辑器少冲突的组合。如果你一定要注册全局热键用 JNA 注册时要检查返回值注册失败则提示用户换键。这个原则不只在输入法里有用凡是做内嵌输入模块都应该把快捷键让给宿主程序自己的键做成可改。5.5 内存里的词库越滚越大缓存策略现象程序运行几十小时后内存从 100MB 涨到 400MBGC 越来越频繁界面开始一顿一顿。不断按钮堆区直方图里大量字符串数组。原因用户自造词表无限增长每次按键查询都构造新的ListString在低端机上产生明显压力。Trie 本身是常驻内存不会无限涨但查询结果和排序中间对象会让老年代迅速堆积。解决给用户词表设置硬上限比如 20000 条超过后用 LRU 淘汰低频词。查询结果可以复用固定大小数组用一个 generation 计数器做失效标记避免反复分配大对象。还有一条立竿见影的经验前缀长度小于 3 的查询结果缓存起来因为这些结果稳定且数量少。缓存命中后即使用户词频变化也可以等缓存过期再重建。6. 进阶把引擎做成无头服务用断言脚本给输入法上回归测试输入法最麻烦的不是写功能而是改了一个排序规则后你不知道上一版体验有没有被破坏。我把引擎部分做成无头服务之后才真正告别了“瞎调参数”的状态。做法很简单把 Trie 查询和候选排序逻辑保留去掉 UI 和注入只暴露一个纯 Java 接口输入拼音串返回前 N 个候选。这样所有测试都不需要启动 Swing毫秒级跑完。基础回归测试可以写成这样。这里拿 JUnit 做示例如果你不想引测试框架用普通 main 加断言效果一样Test void nihaoShouldReturnTopWordFirst() { PinyinEngine engine new PinyinEngine(); ListString candidates engine.candidates(nihao, 3); assertEquals(你好, candidates.get(0)); }当词库更新后这类断言会报很多排序变化。为了避免每次更新词库都要人工确认可以把断言从“完全相等”改成“必须包含且第一个元素一定是主词”这样既保证常用词在前又不会因为新词进入候选而误伤。用这个粒度跑完整个词库样例大约几分钟比人工点界面快得多。再进一步可以做一个“回放测试”。把用户真实按键序列录下来存成 JSON 文件测试时逐段喂给无头引擎断言最终长文本的每次 commit 跟用户当时一致。这段测试价值高于一切单元测试因为它覆盖了先输拼音、切中文标点、退格、翻页、再选字这种复杂操作组合。我现在的习惯是每次调整词频、加自造词、换字典都先把回放测试跑一遍确保没有引入新的翻车点。这个习惯挡掉了至少三次明显的候选排序回退。等你把输入法引擎改成这种可回放、可断言的结构后续想再叠加双拼、模糊音、自定义短语都会变得非常踏实。这大概是我在这个项目里留下的最好的一条经验。希望帮到你。本文还有配套的精品资源点击获取