Android 输入法详解

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

官方文档

IMF 框架

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

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

可知最关键的就是 InputMethodManagerInputMethodManagerServiceInputMethodService 三者之间的关系,后文中出现可将其简称为 IMMIMMSIMS,其关系如下:

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 的拉起可由按键和触摸事件触发,经由 IMMIMMS 通过一系列 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 方法,后续 IMMIMMSIMS 三者之间的交互关系都在其中,最终调用 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 的初始化流程,从 SystemServerIMMS 再到 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

看下 IMMSonSessionCreated 方法实现:

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 创建的时候调用 IMMSaddClient 将客户端 IMM 的关键实现 IInputMethodClientIInputContext 添加到 IMMSIMMS 统一维护。

  • IMMIMMS 的交互主要是 IInputMethodManager.aidl,使用时通过 getSystemService 获取 input_method 系统服务,也就是 IMMS,之后就可以调用输入法管理器 IMMS 提供的公共接口了。
  • IMMIMS 的交互主要是 IInputMethodSession.aidl,定义供客户端操作输入法的方法,IMMS 请求 IInputMethod 创建 IInputMethodSessionIMMS 绑定客户端 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. IMMIMS 通过 IInputMethodSessionIInputContext 交互。

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