查看原文
其他

Android输入法IMS流程详解

鸿洋
2024-08-24

The following article is from 躬行之 Author jzman

PS:尝试从稀缺的角度看问题。

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


  1. IMF 框架
  2. 输入法拉起流程
  3. 输入法管理服务(IMMS)
  4. 输入法(IMS)
  5. 客户端(IMM)
  6. IMS、IMMS和IMM之间的交互

1IMF 框架


Android 输入放框架主要包括三部分内容:
  1. 客户端,当前输入内容的 App,主要是 InputMethodManager和 EditText/TextView。
  2. 输入法管理服务,主要是 InputMethodManagerService
  3. 输入法:主要是 InputMethodService。
可知最关键的就是 InputMethodManagerInputMethodManagerServiceInputMethodService 三者之间的关系,后文中出现可将其简称为 IMM、IMMS和IMS,其关系如下:
上述三个模块分别对应不同的进程,客户端 IMM 属于 App 应用进程,输入法 IMS 属于输入法自己的进程,输入法管理服务 IMMS 属于系统进程,三者之间需要通过 IPC 机制来进行通信, Androd中最主要的 IPC 通信方式都就是 Binder 机制。

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

2输入法拉起流程


当点击一个输入框 EditText 将获得焦点,系统默认的输入法将会被拉起,EditText 继承自 TextView,关键实现在 TextView 中,拉起输入法的关键流程如下:
可知,输入法 IMS 的拉起可由按键和触摸事件触发,经由 IMM、IMMS 通过一系列 IPC 调用拉起系统默认输入法 IMS,上述流程中省略了一些操作,实际流程要更复杂,重点关注如下返回值:
// InputBindResult.java
// 请求输入法成功
ResultCode.SUCCESS_WITH_IME_SESSION,
// 输入法已经启动,等待IME创建session
ResultCode.SUCCESS_WAITING_IME_SESSION,
// 输入法还未启动,等待输入法绑定
ResultCode.SUCCESS_WAITING_IME_BINDING,


当没有绑定输入法和 session 没创建的时候都在等待输入法绑定和 session 创建,当完成这两步之后客户端会再次请求输入法,为便于理解上图中没有体现这个过程,拉起 IMS 的关键代码如下:
// 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, 0new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0));
    // 绑定输入法
    if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
        // ...
        // 绑定成功
        return new InputBindResult(
                InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
                nullnull, 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 中定义的相关接口了,如下:
@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 显示输入法。

3输入法管理服务(IMMS)


InputMethodManagerService 是用来管理输入法的系统服务,IMMS 属于系统服务的一部分,随着系统的启动而启动,IMMS 的初始化流程如下图所示:
如上图 IMMS 的初始化流程,从 SystemServer 到 IMMS 再到 ServiceManager,最后到 ServiceManagerProxy 这里完成 Java 层的系统服务注册流程,BinderProxy 是 native 层 IBinder 在 Java 层的代理对象,其初始化只能通过 Native 层的 javaObjectforIBinder 函数来初始化,不能直接创建,到此系统服务 IMMS 在 Java 层的初始化结束,Native 层的暂时不关注,下面附上 BinderProxy 的定义及注释供参考:
/**
 * 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,关键流程如下:
看下 IMMS 的 onSessionCreated 方法实现:
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 的关键流程结束。

4输入法(IMS)


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


自定义输入法要在 AndroidManifest.xml 中进行声明,参考如下:
<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 的生命周期如下:
其关键生命周期如下:
  • onBindInput:客户端 IMM 绑定该输入法 IMS 的时候调用。
  • onStartInput:用户在编辑框开始输入文字的时候调用。
  • onCreateInputView:创建并返回输入区域的视图,第一次显示输入区域视图的时候调用,默认实现返回 null,可实现 onEvaluateInputViewShown 来控制候显示输入区域视图,更改输入区域视图使用 setInputView 进行更改。
  • onCreateCandidatesView:创建并返回显示候选词的视图,第一次显示候选词视图的时候调用,默认实现返回 null,可使用 setCandidatesViewShown 方法控制候选词视图的显示,更改候选视图使用 setCandidatesView 进行更改。
  • onStartInputView:当前页面的输入框获取了焦点时调用。

5客户端(IMM)


InputMethodManager 主要用来处理当前应用程序与输入法之间的交互,每次只能运行一个输入法,多个应用程序使用输入法,确保获得焦点并保证只有一个应用程序在使用输入法,应用程序使用 Textview 及其子类默认实现了这种交互,也可以自定义文本编辑器来实现这种交互。
IMM 的核心实现是 IInputMethodClient.aidl IInputContext.aidl,如下:
  • IInputMethodClient.aidl:简单理解为 IMMS 绑定 IMS 的时候通知 IMM 输入法已经准备好了可以进行绑定了,具体是由 IMM 内部实现,对应 mClient。
  • IInputContext.aidl:定义操作输入法编辑器 IME 的方法,默认实现是 EditableInputConnection
下面看看 IMM 的初始化流程:
可知,ViewRootImp 创建的时候会获取 IWindowSession,如果 IWindowSession 为空的时候,会通过 WindowManagerService 创建一个 IWindowSession ,其实现为上图中的 Session,在 Session 创建的时候调用 IMMS 的 addClient 将客户端 IMM 的关键实现 IInputMethodClientIInputContext 添加到 IMMS 由 IMMS 统一维护。
  • IMM 与 IMMS 的交互主要是 IInputMethodManager.aidl,使用时通过 getSystemService 获取 input_method 系统服务,也就是 IMMS,之后就可以调用输入法管理器 IMMS 提供的公共接口了。
  • IMM 与 IMS 的交互主要是 IInputMethodSession.aidl,定义供客户端操作输入法的方法,IMMS 请求 IInputMethod 创建 IInputMethodSession,IMMS 绑定客户端 IMM 的时候将 IInputMethodSession 传入 IMM。

6IMS、IMMS和IMM之间的交互


输入法(IMS)、输入法管理服务(IMMS)和客户端(IMM)之间的交互主要是通过一系列 aidl 进行交互的,关键 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 机制下的输入法框架。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

binder传递大数据:AIDL 如何分片传输大量 Parcelable 数据
Android打造丝滑的Activity recreate重建(主题切换)过渡动画
值得一看的Android广播分析好文


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

    继续滑动看下一个
    鸿洋
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存