简介:面向想在安卓开发环境中快速创建输入法应用的开发者,这份压缩包提供了一个可直接运行的最简输入法工程,覆盖输入法服务、键盘界面和按键事件处理三大核心部分,有效降低入门门槛。资源共1982个文件,压缩后约26.56MB,其中以XML界面布局、PNG按钮图标、JSON配置、JAVA源码,以及编译生成的class、jar和可直接安装的apk文件为主,既有完整工程代码也有调试产物,便于边看边跑。通过该项目能够掌握输入法服务InputMethodService的生命周期、自定义键盘的构建方式、按键字符向输入框提交的完整流程,以及输入法在系统设置中的启用和切换方法。工程内附有构建产物与源码目录,结构直观,便于按模块阅读和修改。已有1063人学习下载,适合初次接触输入法开发、希望获得可运行模板并继续二次定制功能的开发者。
1. 先盘清楚成本:最简单的输入法在 Android Studio 里能跑成什么样
在 Android Studio 里新建一个空工程,把它做成输入法(IME),也就是标题里说的“可直接运行的最简单输入法”,这件事我已经做过很多次。它不依赖任何三方库,也不需要词库、手势、悬浮窗这些花活,核心只有一个继承 InputMethodService 的类、一张软键盘布局、一份 Manifest 声明。十几分钟就能跑出第一个版本,点任意输入框,系统会把你写的键盘弹出来。这篇文章从建工程一路拆到真机验证,再给你一份我踩出来的高频坑清单。适合想碰输入法、又不想一开始就被系统服务细节劝退的开发者,也适合那些想拿输入法做内部工具的人。
2. 搭骨架:InputMethodService 加 AndroidManifest,缺一个都弹不出来
2.1 为什么是 InputMethodService:输入法在 Android 里的运行方式
在 Android 里,输入法不是一个 Activity,也不是普通悬浮窗,而是一个由系统进程主动启动并绑定的 Service。它的生命周期和时机都不由你控制:用户在任何可编辑控件上点一下,系统会找到当前启用的输入法应用,调用它的 InputMethodService,然后把软键盘窗口展示出来。A同学第一次接触输入法项目时,把这个 Service 当普通 View 塞进 Activity 里调试,结果怎么都弹不出来,因为他根本没让系统把这个 Service 识别成“输入法”。
要让一个 Service 被系统当作输入法,三件事必须同时成立:第一,类继承 InputMethodService;第二,AndroidManifest 里声明了系统绑定权限和对应 action;第三,res/xml 里有一份给系统读的输入法元数据配置。三样缺一,设置页里要么找不到这个输入法,要么找到了也切不过去。下面按顺序把这三样配齐,配完就是第一个可运行的版本。
2.2 写一个能上屏的输入法 Service:最小 Java 类
在空工程里新建一个类,名字叫 SimpIme,继承 InputMethodService。先写一个最简版本,它已经能弹出一个按钮,按下去往当前输入框输入一个大写 A:
public class SimpIme extends InputMethodService { @Override // 系统准备展示键盘时调用,返回值就是软键盘的根 View public View onCreateInputView() { Button btn = new Button(this); btn.setText("按我输入 A"); // 关键:键盘上的按钮不能抢输入框的焦点,否则会收不到输入连接 btn.setFocusable(false); btn.setTextSize(18); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 拿到当前输入框的连接,为空说明输入法没真正接上输入框 InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.commitText("A", 1); } } }); LinearLayout root = new LinearLayout(this); root.setOrientation(LinearLayout.VERTICAL); root.setGravity(Gravity.CENTER); root.addView(btn); return root; // 返回这个 View,系统会把它放进输入法窗口 } }这段代码的逻辑很直白,但两处细节值得记一下。btn.setFocusable(false) 不是顺手写的,而是输入法开发里最常见的坑:Button 默认可聚焦,点击它时目标输入框会失焦,getCurrentInputConnection() 可能返回 null,字符永远弹不进去。另一个细节是返回值交给系统后,这个 View 的高度会直接影响软键盘的显示高度,后面第 3 章单独讲。
这个类用 Java 写,是因为对“最容易读”来说 Java 的继承和方法重写比 Kotlin 的扩展风格更直观。你项目里如果建的是 Kotlin 工程,写法完全一样,类名换成你的 Kotlin 类名即可,Manifest 里的 android:name 要同步改。输入法不关心你的 Activity 是 Java 还是 Kotlin,它只认 Service 的完整类名。
2.3 在 Manifest 里把 Service 声明成“输入法”:权限、action、meta-data
光有这个类还不够,系统不会扫描你所有的 Service。必须在 AndroidManifest.xml 的 内加一个固定声明格式的 service 节点:
<service android:name=".SimpIme" android:label="最简单输入法" android:exported="false" android:permission="android.permission.BIND_INPUT_METHOD"> <intent-filter> <action android:name="android.view.InputMethod" /> </intent-filter> <meta-data android:name="android.view.im" android:resource="@xml/ime_message" /> </service>这里四个关键词分别承担不同职责。android:permission 是安全门槛:只有持有 BIND_INPUT_METHOD 权限的调用方才能绑定这个 Service,普通 App 不能随便 bind 你的输入法。intent-filter 里的 action 是系统识别输入法身份的入口,少了这一行,设置页会直接把你忽略。meta-data 指向 xml 里关于输入法的元数据。android:exported 在 targetSdk 31 之后必须显式声明,否则编译直接报错;我习惯写 false,系统仍能通过权限绑定,如果遇到切换列表不出现,再把这块单独排查。
2.4 没有这一份 xml,系统不认识你的输入法:res/xml 配置与 subtype
在 res/xml 目录下新建 ime_message.xml,文件名要和 meta-data 中的 @xml/ime_message 保持一致。内容如下:
<input-method xmlns:android="http://schemas.android.com/apk/res/android"> <subtype android:label="简体中文" android:imeSubtypeLocale="zh_CN" android:imeSubtypeMode="keyboard" /> </input-method>input-method 是根节点,subtype 描述这个输入法支持的类型。locale 写成 zh_CN 表示它是中文键盘;imeSubtypeMode 固定写 keyboard。别小看这份文件,不少 Android 版本在切换输入法时,如果当前 IME 一个 subtype 都没声明,切换列表里就是空白,设置页里明明启用了却切不过去。第一次写可以先保留一个 subtype,后面想加英文键盘、双拼继续加就行。
2.5 第一个可运行版本:编译、安装并在系统里启用
输入法的功能在后台 Service 里,不像普通应用,点 Android Studio 的 Run 之后不会弹出一个界面。默认工程里的 MainActivity 是 App 的宿主,不影响输入法识别,但你要知道真正的入口不是它。正确验证流程是这样:先把 APK 装到手机或模拟器;在系统设置里进入“语言与输入法”,找到“最简单输入法”并启用;再点开任意一个输入框,从通知栏或输入法切换器选到它。
如果这三个步骤里任何一步找不到入口,就回到前面三份文件检查:Java 类是否继承 InputMethodService、Manifest 的权限和 action 是否齐全、xml 的 subtype 是否写对。到这里,一个最小输入法已经真正可运行了。后面要做的,就是把这个只有单个按钮的“键盘”换成看起来像键盘的样子。
3. 画键盘:用 LinearLayout 替代 KeyboardView,九宫格半小时改完
3.1 为什么别用 KeyboardView:弃用与资源绑定
网上不少输入法教程还在用 android.inputmethodservice.KeyboardView 和 Keyboard,这两者在新 API 里已经被标记废弃,而且两者绑定很深:KeyboardView 需要一份 keyboard_keys.xml 来定义键位和按键样式,触控反馈、按键音都写在它自己的逻辑里。你真想换成自己的键面布局时,得重写它的很多回调,对“最简单输入法”这个目标来说,学习成本完全不划算。
我一般会直接忽略 KeyboardView,用 LinearLayout 加 Button 搭键盘。原因很简单:中文和数字键盘的默认按键就是横向一排排排列,LinearLayout 足够表达;点击事件就是普通 View 的点击事件,调试思路和写页面完全一致。你甚至可以先用 XML 排好界面,在 Activity 预览里看效果,再挂到输入法上,开发效率比折腾 KeyboardView 高很多。
3.2 XML 里写一个三行三列再加一行的键盘布局
新建 res/layout/ime_keyboard.xml,用四个横向 LinearLayout 拼成四行。行高直接给 48dp,不要用 0dp 加 weight 去撑高度,原因放在 3.4 讲。先看前两行的写法:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/key_root" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#EEEEEE" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="48dp"> <Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:focusable="false" android:tag="1" android:text="1" /> <Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:focusable="false" android:tag="2" android:text="2" /> <Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:focusable="false" android:tag="3" android:text="3" /> </LinearLayout> <!-- 第二行、第三行结构完全相同,分别放 4 5 6 和 7 8 9 --> <!-- 第四行放退格、0、确定,功能键的 tag 这里先不定义 --> </LinearLayout>横向排列的按钮用 layout_width="0dp" 加 layout_weight="1" 平分整行宽度,这是最省心的方式:无论屏幕多宽,三个键都均匀铺满。每一行固定 48dp,四行就是 192dp,键盘整体高度是稳定的。每个 Button 都设 focusable="false",避免点击键盘时抢走目标输入框的焦点。tag 属性保存了真正要提交的字符,text 只负责显示,这样显示和提交可以灵活分开。
第四行的“退格”“0”“确定”中,0 可以正常给 tag="0",退格和确定属于功能键,它们的点击行为不是简单 commitText,先留着不给 tag,下一章接上逻辑。
3.3 把 XML 挂到 onCreateInputView,用同一个点击事件分发字符
回到 SimpIme,把 onCreateInputView 里手动 new View 的方式换成 inflate 布局。为了让多个按钮不各自写一套监听,用一个统一 listener 处理,通过 tag 判断要提交的内容:
@Override public View onCreateInputView() { View root = getLayoutInflater().inflate(R.layout.ime_keyboard, null); OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { InputConnection ic = getCurrentInputConnection(); if (ic == null) { return; } CharSequence tag = (CharSequence) v.getTag(); if (tag != null) { ic.commitText(tag, 1); } } }; // key_root 是根布局,遍历它的每个子行,再遍历行内按钮 LinearLayout keyRoot = root.findViewById(R.id.key_root); for (int i = 0; i < keyRoot.getChildCount(); i++) { LinearLayout row = (LinearLayout) keyRoot.getChildAt(i); for (int j = 0; j < row.getChildCount(); j++) { View child = row.getChildAt(j); child.setOnClickListener(listener); } } return root; }遍历方式依赖一个前提:键盘区里的直接子 View 全是横向 LinearLayout,行内全是 Button。只要你不往里面塞分隔条或提示文字,这段代码不会出问题。tag 为空的功能键点击后会被 if (tag != null) 挡住,暂时不做任何事。后续要在退格和确定上加逻辑,给它们设置对应 tag 后,在 listener 里加两个判断分支就行。
3.4 键盘高度为什么是 wrap_content:输入法窗口的尺寸规则
很多人写到这里会疑惑,为什么根布局高度不写 match_parent,也不写固定值,而是 wrap_content。原因在输入法窗口的尺寸规则:软键盘窗口高度由系统按屏幕剩余空间和当前输入框的 insets 动态计算,你写在布局里的固定高度会和系统计算打架。
如果你把根布局改成 match_parent,输入法窗口会顶到屏幕上沿,看起来像全屏盖住了内容;如果你把每行用 0dp 加 weight 去占满父布局,而父布局高度又是 wrap_content,键盘高度会被测量成 0,出现按键挤在一起的怪样子。所以这里最稳的做法是:根布局 wrap_content,每一行用固定 48dp,行内用 weight 均分宽度。这是输入法开发里少数我建议“别那么灵活”的地方。想进一步自定义高度,以后重写 onComputeInsets 和 onMeasure 也不迟。
3.5 先跑起来的验证路径:打开 App 不等于弹出键盘
写完布局后重新安装,点桌面上的应用图标,你会看到 MainActivity 空界面,键盘根本不会弹出来——这是正常的,因为输入法由系统调度,不是由应用界面调度。验证方法是去任意输入框点击,比如在 MainActivity 里放一个 EditText,编译后再点它。如果 EditText 聚焦后弹出来的是系统键盘,说明系统还没把输入法切到你写的这个;这时去输入法切换列表里手动选一下“最简单输入法”,大概率就在里面。
如果切换列表里找不到,最可能的问题还是 Manifest 声明有遗漏,或者 subtype 没写。这些坑很大概率会在你第一次运行时报出来,我已经在第 5 章把它们集中整理了。
4. 把字符送进输入框:InputConnection 的提交、删除与动作键
4.1 commitText 的 newCursorPosition 参数到底怎么设
上一章的点击事件里,commitText(tag, 1) 的第二个参数是 1,很多新手会忽略它。newCursorPosition 表示提交文本后光标相对新文本的位置:传 1 表示把光标移到刚输入的文本后面,传 0 表示光标保持在文本开头,传负数表示光标往新文本前移动偏移。对普通打字来说,传 1 才合理:输完一个字符,光标应该在它后面,紧接着输下一个。
如果这里传 0,每次输入完光标都在前面,下一次按键会把新字符插在之前字符的前面,表现就是输入顺序全部反过来。这是输入法里最隐蔽的“玄学”问题之一。不管你要提交的是字母、数字还是整句话,newCursorPosition 都建议固定用 1,除非你想实现“在光标前插入”的特定效果,才去动这个参数。
4.2 退格不是删一个字符,是删一个“区间”:deleteSurroundingText
软键盘上的退格按钮,点击时要调用的不是 commitText,而是 InputConnection 的 deleteSurroundingText 方法。用法如下:
// 删除光标前 1 个字符,相当于普通键盘的退格 ic.deleteSurroundingText(1, 0);第一个参数是光标前的删除数量,第二个参数是光标后的删除数量。传 (2, 0) 一次删两个字符;想把光标后面还未确认的字符删掉,就传 (0, 1)。这个方法的名字容易让人误解成“删除附近文本”,它实际会先删掉指定数量的字符,再把光标放回删除区间的开头。在只做基础输入时,退格就是这一行代码,且不需要额外判空:光标已经在文本最前头时,Android 不会崩溃,只是什么都不删。
| 方法 | 作用 | 常用参数 | 常见误用 |
|---|---|---|---|
| commitText | 提交一段文本到输入框 | text、newCursorPosition=1 光标放末尾 | 传 0 导致输入顺序反向 |
| deleteSurroundingText | 删除光标前后字符 | before=1、after=0 表示退格 | 想删选中文本时却只删固定字符数 |
| performEditorAction | 把动作键事件发给输入框 | editorInfo.imeOptions | 传 0 当成普通回车,导致搜索按钮失效 |
输入框有选中区域时,deleteSurroundingText 的 before 和 after 并不直接等价于“删除选中内容”,因为删除是围绕光标进行的。如果项目以后要做“删除选中的一串字”,那得先读取 Selection 的 start 和 end,再用 commitText 替换成空字符串,这里不再展开。
4.3 读取 EditorInfo,让回车键变成“搜索”或“完成”
输入框的 android:imeOptions 属性会透传到一个叫 EditorInfo 的对象里。你可以通过它判断当前输入框想要什么操作按钮,比如搜索、发送、完成、下一步。在 SimpIme 里重写 onStartInputView,每次输入法切换到新输入框时读取一份数据:
private int currentImeOptions = 0; @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { super.onStartInputView(attribute, restarting); // 系统传进来的是目标输入框的信息,imeOptions 标识右下角动作键的用途 if (attribute != null) { currentImeOptions = attribute.imeOptions; } }键盘上那个“确定”功能键点击时,不提交字符,而是调用 performEditorAction,把动作交给系统处理:
if ("action".equals(tag)) { InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.performEditorAction(currentImeOptions); } }currentImeOptions 是 EditorInfo 里的原值,不需要自己判断“完成”还是“搜索”,系统知道目标输入框期望什么。如果你的键盘按钮文案想跟着动作键变化,也可以在 onStartInputView 里读 attribute.actionLabel 或 actionId 去改按钮文字,但对当前这个版本不是必须的。
4.4 给键盘加上下档字符:大小写切换的最小实现
如果键盘上有字母,你多半想要大小写切换。最简单的方式是维护一个 boolean caps 状态,点击切换键翻转它,然后遍历所有按钮,把单字符的 text 和 tag 一起转换:
private boolean caps = true; private void toggleCaps(LinearLayout root) { caps = !caps; for (int i = 0; i < root.getChildCount(); i++) { LinearLayout row = (LinearLayout) root.getChildAt(i); for (int j = 0; j < row.getChildCount(); j++) { Button btn = (Button) row.getChildAt(j); CharSequence tag = (CharSequence) btn.getTag(); if (tag != null && tag.length() == 1) { String converted = caps ? tag.toString().toUpperCase() : tag.toString().toLowerCase(); btn.setText(converted); btn.setTag(converted); } } } }大小写切换的逻辑就这么多,但有一个细节必须强调:text 和 tag 必须一起换,否则会出现界面上显示大写、实际提交小写的错位。这个错位在临时真机测试时特别坑,因为肉眼看到键盘上明明是大写 A,输入框里却是小写 a,排查半天才发现是 tag 没同步。养成“提交内容跟着显示内容走”的习惯,后面做符号页切换时能少踩一个坑。
4.5 验证上屏行为:用一个自带输入框的 Activity
空工程的 MainActivity 里放一个 EditText 就够了,不需要做任何绑定;模拟器里直接点 EditText,就会唤起当前启用的输入法。你还可以顺手测一下 windowSoftInputMode:输入法本身以 IME 方式工作,Activity 的软键盘模式不会挡住它,不用特意配置。若想边测边看日志,在点击事件的 ic 判空处加一行 Log 输出,比反复打断点更快。这里我一般会专门用一个带不同 imeOptions 的输入框来测动作键,比如一个输入框设置 android:imeOptions="actionSearch",另一个设置 actionDone,看右下角按钮的表现是否和普通键盘一致。
4.6 为什么不建议用 sendDownUpKeyEvents 模拟按键
有些旧教程会让你用 sendDownUpKeyEvents 模拟物理按键输入,比如 KEYCODE_A 对应字母 A。这个方法在部分模拟器上有效,但我建议你别在正式项目里用。sendDownUpKeyEvents 会把事件当作用户物理键盘敲击,可能触发联想、输入法快捷键等额外逻辑;不同设备对硬件键事件的处理也不统一,字符丢失不是罕见事。更麻烦的是,它没法直接表达中文、标点和自定义短语,每次都要做 keycode 映射。因此这个最简单输入法只用 InputConnection 的 commitText 和 deleteSurroundingText,提交删除都走文本通道,行为可预期得多。
5. 输入法不弹出的排查手册:5 个高频坑的现象、原因与解决
5.1 设置里找不到“最简单输入法”:manifest 权限与 intent-filter 缺失
现象:APK 已经装到手机,设置 → 语言与输入法 → 虚拟键盘里翻遍列表,也看不到自己写的输入法项。
原因:最常见是<service>里漏了 android.permission.BIND_INPUT_METHOD 权限,或 intent-filter 的 action 写错,系统枚举输入法时直接跳过。其次是 res/xml 文件名和 meta-data 的 @xml 引用对不上,系统读取元数据失败。
解决:回到 AndroidManifest 核对 service 节点。我一般会先用命令查看系统是否感知到这个输入法,如果输出里根本没有包名,说明 service 声明有问题,先修声明再重新安装。注意输入法没有独立桌面入口,装完直接去设置页看结果,不用指望 Run 按钮等下会弹键盘。
5.2 在输入法列表里切不出来:subtype 缺失导致的选择器空白
现象:设置页能看到“最简单输入法”并启用了,但随便点开输入框,弹出的还是系统自带键盘,切换列表里找不到这个输入法。
原因:res/xml/ime_message.xml 里没有声明<subtype>。部分 Android 版本获取输入法列表时把 subtype 作为过滤条件,没有 subtype 的 IME 会被视为不可用,于是它不出现在切换候选里。
解决:把 2.4 的 xml 补全,重新安装,同时确认 imeSubtypeLocale 写成系统支持的语言代码,比如 zh_CN 或 en_US。android:label 可以直接写中文字面量,不需要引用字符串资源;如果 locale 乱填一个系统不认识的地区代码,切换列表依然可能把你丢掉。
5.3 软键盘显示了,但按下去不弹字:InputConnection 为 null 与焦点问题
现象:键盘能弹出来,按键上也有点击效果,但输入框里就是不出字,日志打在 getCurrentInputConnection() 处能看到返回 null。
原因:绝大多数不是系统 Bug,而是键盘上的某个 View 抢走了焦点。尤其是键盘里放了 EditText,或者 Button 没设 focusable=false 时,点击按键后焦点从目标输入框转移走,输入法的输入连接也会跟着断开。
解决:先把键盘里所有可聚焦控件都关掉,Button 统一加 focusable="false",键盘里不要放 EditText。第二步检查目标输入框本身是不是 TextView:TextView 默认不可编辑,不会建立输入连接。第三步在点击事件里加判空日志,让问题在日志层面显形,而不是对着屏幕干猜。
5.4 键盘布局被系统拉伸、悬浮或截断:根布局高度写错的三种表现
现象:同一个键盘,在部分手机上高度占了半屏,在横屏时下面留一条黑缝,还有可能被底部导航条盖住底部一行。
原因:根布局 android:layout_height 写了 match_parent,或者内部行用了 0dp 加 weight 去撑高度。输入法窗口高度由系统根据 insets 动态给出,写在布局里的固定全屏高度会跟系统计算打架;而 wrap_content 下用 weight 撑行高,键盘会被测量成高度 0 或极小值。
解决:统一用“根布局 wrap_content + 每行固定 48dp”的方案。行内宽度可以用 weight,因为宽度由输入法窗口宽度决定,不会出现测量歧义。如果发现键盘还剩一条缝,先查是不是有人给根布局加了 margin 或给行加了 divider。
5.5 targetSdk 31 之后编译报 exported 错:API 检查与强迫声明
现象:升级 compileSdk 或 targetSdk 后,Manifest 编辑器直接标红,Build 报错提示带 intent-filter 的组件必须声明 android:exported。
原因:Android 12(API 31)增强了组件可见性,所有带 intent-filter 的组件必须明确 exported,输入法的 service 带 intent-filter,逃不掉。
解决:在<service>上补 android:exported="false"。按我的经验,false 不影响系统识别和绑定;如果你在某个 ROM 上发现设置了却无法启用,可以临时改成 true 重新安装对比,但不要通过在intent-filter里补包名的方式绕过检查,那会引入安全问题。升 targetSdk 之前,把这类历史隐患一次性排查掉,比打包发布被商店检测到后再补救省事得多。
6. 用 adb 一条命令切换输入法:一个让验证过程少走弯路的技巧
输入法没有桌面图标,每次改代码后都要在真机上点设置、进语言与输入法、切键盘,点错一步就白等。我习惯把三个命令串起来用:先查当前系统里有哪些输入法,再启用自己那个,最后切换过去。终端里执行:
# 列出系统里所有输入法,确认包名和 service 类名完整 adb shell ime list -s # 启用你的输入法,包名/类名要和 Manifest 里完全一致 adb shell ime enable com.yourpackage/.SimpIme # 立即切到你的输入法,效果等同于在设置里手动点选 adb shell ime set com.yourpackage/.SimpIme其中 ime set 是即时生效的,不需要重启模拟器。配合 Android Studio 的安装结果,一条命令就能完成“装新包 → 启用 → 切换”,省掉设置页里来回点的过程。如果 ime set 时报 unknown id,多半是包名写错了;直接复制第一条 ime list -s 的输出最稳。再配合日志过滤,只要给 Log 加一个 “SimpIme” 常量,就可以用 adb logcat -s SimpIme 单独看输入法进程的输出,比在一堆 tag 里找自己那一条省心很多。这组命令还可以写进脚本,做成运行配置,以后每次调试只要点一次。我最早做输入法 Demo 时不知道有这些命令,改一行代码就在真机上点六七次设置,后面才明白:输入法开发的调试路径和普通应用不是一回事,提前掌握这几条命令,相当于吃了后悔药。希望帮到你。
本文还有配套的精品资源,点击获取