Android 输入法详解

文本输入是输入法和编辑器协同作用的结果,输入法可以是软键盘、语音等,扩展了 InputMethodService 来实现,编辑器主要是接受文本并显示文本的组件,一般是 EditText,输入法与编辑器的交互一般是当输入框获得焦点,弹出当前系统默认输入法,输入字符,选择候选词,更新输入字符到编辑框,本文将从如下几个方面介绍 Android 输入法机制,如下:

官方文档

IMF 框架

Android 输入放框架主要包括三部分内容:

  1. 客户端,当前输入内容的 App,主要是 InputMethodManager和 EditText/TextView。
  2. 输入法管理服务,主要是 InputMethodManagerService。
  3. 输入法:主要是 InputMethodService。

可知最关键的就是 InputMethodManager、InputMethodManagerService、InputMethodService 三者之间的关系,后文中出现可将其简称为 IMM、IMMS和IMS,其关系如下:

sequenceDiagram
    participant IMM as 客户端(IMM)
    participant IMMS as 输入法管理服务(IMMS)
    participant IMS as 输入法(IMS)
    IMM->>IMMS: 显示、隐藏输入法
    IMMS->>IMS: 显示、隐藏输入法
    IMS-->>IMM: 返回输入信息

上述三个模块分别对应不同的进程,客户端 IMM 属于 App 应用进程,输入法 IMS 属于输入法自己的进程,输入法管理服务 IMMS 属于系统进程,三者之间需要通过 IPC 机制来进行通信, Androd中最根主要的 IPC 通信方式都就是 Binder 机制。

其中客户端 IMM 与 输入法 IMS 之间的交互已经通过 EditText 及其父类 TextView 实现,如果是自定义的编辑框,需要自行实现与输入法的交互。

输入法拉起流程

当点击一个输入框 EditText 将获得焦点,系统默认的输入法将会被拉起,EditText 继承自 TextView,关键实现在 TextView 中,拉起输入法的关键流程如下:

sequenceDiagram  
autonumber
TextView->>TextView:onKeyDown/onTouchEvent
TextView->>TextView:viewClicked
TextView->>IMM:viewClicked
IMM->>IMM: checkFocus
IMM->>IMM: startInputInner
IMM->>IMMS: startInputOrWindowGainedFocus
IMMS->>IMMS: startInput
IMMS->>IMMS: startInputLocked
IMMS->>IMMS: startInputUncheckedLocked
IMMS->>IMMS: startInputInnerLocked
IMMS->>IMMS: bindCurrentInputMethodServiceLocked
IMMS->>IMMS: onServiceConnected
IMMS->>IMS: attachToken
IMMS->>IMS: createSession
IMS-->>IMMS:  onSessionCreated
IMMS->>IMS: bindInput
IMMS->>IMS: startInput
IMMS->>IMS: showSoftInput
IMS->>IMS: showWindow
IMS->>IMS: showWindowInner
IMS->>IMS: onStartInputView

可知,输入法 IMS 的拉起可由按键和触摸事件触发,经由 IMM、IMMS 通过一系列 IPC 调用拉起系统默认输入法 IMS,上述流程中省略了一些操作,实际流程要更复杂,重点关注如下返回值:

1
2
3
4
5
6
7
// InputBindResult.java
// 请求输入法成功
ResultCode.SUCCESS_WITH_IME_SESSION,
// 输入法已经启动,等待IME创建session
ResultCode.SUCCESS_WAITING_IME_SESSION,
// 输入法还未启动,等待输入法绑定
ResultCode.SUCCESS_WAITING_IME_BINDING,

当没有绑定输入法和 session 没创建的时候都在等待输入法绑定和 session 创建,当完成这两步之后客户端会再次请求输入法,为便于理解上图中没有体现这个过程,拉起 IMS 的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// InputMethodManagerService.java
InputBindResult startInputInnerLocked() {
// ...
// 绑定输入法
// InputMethod.SERVICE_INTERFACE(android.view.InputMethod)
mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE);
mCurIntent.setComponent(info.getComponent());
mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.input_method_binding_label);
mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0));
// 绑定输入法
if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
// ...
// 绑定成功
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
null, null, mCurId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
}
// 绑定失败
return InputBindResult.IME_NOT_CONNECTED;
}

private boolean bindCurrentInputMethodServiceLocked(
Intent service, ServiceConnection conn, int flags) {
// ...
// 绑定输入法
return mContext.bindServiceAsUser(service, conn, flags,
new UserHandle(mSettings.getCurrentUserId()));
}

当绑定输入法 IMS 成功后将获去到 IInputMethod 的远程服务对象 mCurMethod,通过 mCurMethod 就可以操作 IInputMethod.aidl 中定义的相关接口了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mMethodMap) {
if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
// 绑定输入法成功后获得操作输入法的接口IInputMethod
mCurMethod = IInputMethod.Stub.asInterface(service);
if (mCurToken == null) {
Slog.w(TAG, "Service connected without a token!");
unbindCurrentMethodLocked(false);
return;
}
if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
// 发送MSG_ATTACH_TOKEN生成与系统服务IMMS会话的token
executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
MSG_ATTACH_TOKEN, mCurMethod, mCurToken));
// 如果当前绑定到给输入法的客户端存在则重新创建session
if (mCurClient != null) {
clearClientSessionLocked(mCurClient);
requestClientSessionLocked(mCurClient);
}
}
}
}

到此,系统输入法从编辑器 EditText 获得焦点开始到输入法 IMS 服务绑定成功的流程结束,这里注意下 startInputUncheckedLocked 方法,后续 IMM、IMMS 和 IMS 三者之间的交互关系都在其中,最终调用 showSoftInput 显示输入法。

输入法管理服务(IMMS)

InputMethodManagerService 是用来管理输入法的系统服务,IMMS 属于系统服务的一部分,随着系统的启动而启动,IMMS 的初始化流程如下图所示:

sequenceDiagram
autonumber
SystemServer->>SystemServer:main
SystemServer->>SystemServer:run
SystemServer->>SystemServer:startOtherServices
SystemServer->>SystemServiceManager: startService
SystemServiceManager->>IMMS:onStart
IMMS->>SystemService:publishBinderService
SystemService->>ServiceManager:  addService  
ServiceManager->>ServiceManagerProxy:addServvice
ServiceManagerProxy->>BinderProxy:transact

如上图 IMMS 的初始化流程,从 SystemServer 到 IMMS 再到 ServiceManager,最后到 ServiceManagerProxy 这里完成 Java 层的系统服务注册流程,BinderProxy 是 native 层 IBinder 在 Java 层的代理对象,其初始化只能通过 Native 层的 javaObjectforIBinder 函数来初始化,不能直接创建,到此系统服务 IMMS 在 Java 层的初始化结束,Native 层的暂时不关注,下面附上 BinderProxy 的定义及注释供参考:

1
2
3
4
5
6
7
8
/**
* Java proxy for a native IBinder object.
* Allocated and constructed by the native javaObjectforIBinder function. Never allocated
* directly from Java code.
*/
final class BinderProxy implements IBinder {
// ...
}


IMMS 的核心实现是 IInputMethodManager.aidl 和 IInputSessionCallback.aidl,前者提供给 IMM 调用,后者提供给 IMS 调用,如下:

  • IInputMethodManager.aidl:定义了操作输入法的公共接口对外提供给客户端 IMM,最常用的比如显示、隐藏、切换软键盘等。
  • IInputSessionCallback.aidl: 用于允许输入法创建会话的时候通知 IMMS,之后 IMMS 就可以绑定输入法 IMS 了。

InputMethodManagerService 实现了 IInputMethodManager.aidl 提供给 IMM 使用,MethodCallback 实现了 IInputSessionCallback.aidl,用来回调 IMS 创建的 IInputMethodSession,关键流程如下:

sequenceDiagram
    autonumber
    IMMS->>IMMS: requestClientSessionLocked
    IMMS->>IInputMethodWrapper: createSession
    IInputMethodWrapper->>InputMethodSessionCallbackWrapper:sessionCreated
    InputMethodSessionCallbackWrapper->>MethodCallback: sessionCreated
    MethodCallback-->>IMMS: onSessionCreated

看下 IMMS 的 onSessionCreated 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void onSessionCreated(IInputMethod method, IInputMethodSession session,
InputChannel channel) {
synchronized (mMethodMap) {
if (mCurMethod != null && method != null
&& mCurMethod.asBinder() == method.asBinder()) {
if (mCurClient != null) {
// 清理客户端的session
clearClientSessionLocked(mCurClient);
// 初始化SessionState
mCurClient.curSession = new SessionState(mCurClient,
method, session, channel);
// 准备显示输入法
InputBindResult res = attachNewInputLocked(
InputMethodClient.START_INPUT_REASON_SESSION_CREATED_BY_IME, true);
if (res.method != null) {
executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO(
MSG_BIND_CLIENT, mCurClient.client, res));
}
return;
}
}
}
// Session abandoned. Close its associated input channel.
channel.dispose();
}

如上 onSessionCreated 会绑定客户端到此 IMMS 的关键流程结束。

输入法(IMS)

InputMethodService 为标准 UI 元素(输入视图、候选视图和全屏模式下运行)提供了一个基本框架,但如何使用它们取决于特定的实现者,其是自定义输入法的关键类,其本身是一个 Service 的派生类,如下:

1
public class PinyinIME extends InputMethodService {

自定义输入法要在 AndroidManifest.xml 中进行声明,参考如下:

1
2
3
4
5
6
7
8
<service android:name=".PinyinIME"
android:label="@string/ime_name"
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/method" />
</service>

其中权限 BIND_INPUT_METHOD 的声明可以使之能够连接到系统,设置了一个与 android.view.InputMethod 操作匹配的 Intent。

IMS 的核心实现是 IInputMethod.aidl 和 IIInputMethodSession.aidl,前者提供给 IMMS 调用,后者提供给 IMM 调用,如下:

  • IInputMethod.aidl:输入法 IMM 是一个 Service,其核心实现就是 IInputMethod.aidl 的实现 IInputMethodWrapper\,提供了操作输入法的核心方法。
  • IInputMethodSession.aidl:提供了客户端 IMM 操作输入法的接口,IMM 中存在 IInputMethodSession 类型的实例 mCurMethod 供其调用。

收到客户端 IMM 输入操作后,输入法 IMS 将会显示输入法布局,可以参考 SoftKeyboardView 的实现,输入法 IMS 的生命周期如下:

image

其关键生命周期如下:

  • onBindInput:客户端 IMM 绑定该输入法 IMS 的时候调用。
  • onStartInput:用户在编辑框开始输入文字的时候调用。
  • onCreateInputView:创建并返回输入区域的视图,第一次显示输入区域视图的时候调用,默认实现返回 null,可实现 onEvaluateInputViewShown 来控制候显示输入区域视图,更改输入区域视图使用 setInputView 进行更改。
  • onCreateCandidatesView:创建并返回显示候选词的视图,第一次显示候选词视图的时候调用,默认实现返回 null,可使用 setCandidatesViewShown 方法控制候选词视图的显示,更改候选视图使用 setCandidatesView 进行更改。
  • onStartInputView:当前页面的输入框获取了焦点时调用。

客户端(IMM)

InputMethodManager 主要用来处理当前应用程序与输入法之间的交互,每次只能运行一个输入法,多个应用程序使用输入法,确保获得焦点并保证只有一个应用程序在使用输入法,应用程序使用 Textview 及其子类默认实现了这种交互,也可以自定义文本编辑器来实现这种交互。

IMM 的核心实现是 IInputMethodClient.aidl 和 IInputContext.aidl,如下:

  • IInputMethodClient.aidl:简单理解为 IMMS 绑定 IMS 的时候通知 IMM 输入法已经准备好了可以进行绑定了,具体是由 IMM 内部实现,对应 mClient。
  • IInputContext.aidl:定义操作输入法编辑器 IME 的方法,默认实现是 EditableInputConnection。

下面看看 IMM 的初始化流程:

sequenceDiagram
    ViewRootImpl->>WindowManagerGlobal: ViewRootImpl(context,display)
    WindowManagerGlobal->>WindowManagerService: getWindowSession
    WindowManagerService->>Session: openSession
    Session->>InputMethodManagerService: addClient

可知,ViewRootImp 创建的时候会获取 IWindowSession,如果 IWindowSession 为空的时候,会通过 WindowManagerService 创建一个 IWindowSession ,其实现为上图中的 Session,在 Session 创建的时候调用 IMMS 的 addClient 将客户端 IMM 的关键实现 IInputMethodClient 和 IInputContext 添加到 IMMS 由 IMMS 统一维护。

  • IMM 与 IMMS 的交互主要是 IInputMethodManager.aidl,使用时通过 getSystemService 获取 input_method 系统服务,也就是 IMMS,之后就可以调用输入法管理器 IMMS 提供的公共接口了。
  • IMM 与 IMS 的交互主要是 IInputMethodSession.aidl,定义供客户端操作输入法的方法,IMMS 请求 IInputMethod 创建 IInputMethodSession,IMMS 绑定客户端 IMM 的时候将 IInputMethodSession 传入 IMM。

IMS、IMMS和IMM之间的交互

输入法(IMS)、输入法管理服务(IMMS)和客户端(IMM)之间的交互主要是通过一系列 aidl 进行交互的,关键 aidl 如下图所示:

sequenceDiagram
    participant IMM as 客户端(IMM)
    participant IMMS as 输入法管理服务(IMMS)
    participant IMS as 输入法(IMS)
    IMM->>IMMS: IInputMethodManager.aidl
    IMMS->>IMM: IInputMethodClient.aidl
    IMMS->>IMS: IInputMethod.aidl
    IMS->>IMMS: IInputSessionCallback.aidl
    IMS->>IMM: IInputContext.aidl
    IMM->>IMS: IInputMethodSession.aidl

IMS、IMMS和IMM之间的交互如下:

  1. IMM 使用 IInputMethodManager 请求 IMMS。
  2. IMMS 绑定 IMS 获得操作输入法的相关接口 IInputMethod。
  3. IMMS 请求 IMS 创建 IInputMethodSession。
  4. IMMS 通过 IInputMethodClient 告知 IMM 当前 IInputMethodSession。
  5. IMM 和 IMS 通过 IInputMethodSession 和 IInputContext 交互。

IMM、IMMS 和 IMS 三者之间的交互都是 IPC 调用,对于使用者来说无需关注三者之间的复杂调用,如上就是 Android 机制下的输入法框架。