概述  


首先我们了解输入法框架(InputMethodFramework, 下简称IMF)整体的UML图,基于Android10,不同Android版本之间会有少许差异,不过不影响整体结构。


android 覆盖 输入法设置 android输入法框架_android 覆盖 输入法设置


从UML图可以看出IMF涉及到三个主要部分:

  • InputMethodManager(下简称IMM)是整个输入法框架的核心,运行于客户端进程,客户端可以使用IMM对输入焦点和输入法状态进行控制,但是同一时间只有一个客户端处于激活状态。
  • InputMethodService(下简称IMS),是输入法IME(InputMethodEditor)的具体实现,运行于输入法进程。同一时间只有一个IME运行。
  • InputMethodManagerService(下简称IMMS)运行于系统进程,是一个系统级服务,是IMM和IMS沟通的桥梁。


整个I MF是就是围绕IMM,IMS和IMMS三个核心部分进行工作的。以下是对UML图中出现的重要接口的简单介绍:

  • IInputMethod: 这个是输入法进程向系统进程暴露的接口。用于系统进程和输入法进程通信。
  • IInputMethodSession:  这个是输入法进程向外暴露的第二个接口,用于客户端进程和输入法进程通信。
  • IInputContext:这个是客户端进程向输入法进程暴露的接口。用于输入法进程和客户端进程通信。
  • IInputMethodClient:这个是客户端进程向系统进程暴露的接口。用于系统进程和客户端进程通信。
  • IInputMethodManager:  这个是系统进程向客户端进程暴露的接口。


以上接口都属于aidl接口,IMF围绕以上五个接口来实现整体的交互逻辑。 需要说明的是Android版本在后期的迭代中对前三个接口又做了进一步封装。

以IInputMethod为例,谷歌又定义了一个新的接口InputMethod,包含了和IInputMethod相同的接口。具体的实现逻辑从IInputMethod的实现类移动到InputMethod实现类,大概是为了尽量屏蔽aidl接口对于上层实现的影响。

Note:输入法进程代表IMS所在的进程,客户端进程代表IMM所在的进程,这两者可以是同一个进程,但是即使是同一个进程仍然需要通过aidl的方式进行通信。

下图是对以上接口所涉及到的部分流程进行举例


android 覆盖 输入法设置 android输入法框架_android service 权限_02

  • 客户端点击输入框,IMM通过IInputMethodManager接口向IMMS请求显示输入法,IMMS收到请求通过IInputMethod接口向IMS进程转发请求,使输入法展现。
  • IMM进程当光标发生位置发生改变时,IMM通过IInputMethodSession接口,通知IMS光标位置发生变化。
  • IMS进程通过IInputContext将字符上屏到客户端的编辑框。

后续会围绕上述三个部分以及关键流程进行逐步分析,以点到面的方式完成对整个IMF的理解。

  初识IMMS   分析IMF我们从IMMS开始,因为它相对于IMM和IMS,较简单。同时IMMS也是整个IMF的开始的地方。 IMMS是android的一个系统级服务,运行于system_process进程中。主要作用是管理输入法,绑定输入法,管理客户端以及和其他系统级服务交互。IMMS的创建同其他系统服务,这里不再过多介绍。 IMMS在初始化时会创建一个List和Map,用来管理输入法。

static void queryInputMethodServicesInternal(Context context,            @UserIdInt int userId, ArrayMap> additionalSubtypeMap,            ArrayMap methodMap, ArrayList methodList) {        methodList.clear();        methodMap.clear();         // 1. 获取所有包含action为android.view.InputMethod的IntentFilter的Service        final List services = context.getPackageManager().queryIntentServicesAsUser(                new Intent(InputMethod.SERVICE_INTERFACE),                PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS,                userId);         methodList.ensureCapacity(services.size());        methodMap.ensureCapacity(services.size());         // 2. 遍历获取的Service列表        for (int i = 0; i < services.size(); ++i) {            ResolveInfo ri = services.get(i);            ServiceInfo si = ri.serviceInfo;            final String imeId = InputMethodInfo.computeId(ri);             // 3. 对每一个Service检查是否有android.permission.BIND_INPUT_METHOD权限            if (!android.Manifest.permission.BIND_INPUT_METHOD.equals(si.permission)) {                Slog.w(TAG, "Skipping input method " + imeId                        + ": it does not require the permission "                        + android.Manifest.permission.BIND_INPUT_METHOD);                continue;            }             if (DEBUG) Slog.d(TAG, "Checking " + imeId);             try {                // 4. 创建InputMethodInfo对象,并放入List和Map中                final InputMethodInfo imi = new InputMethodInfo(context, ri,                        additionalSubtypeMap.get(imeId));                if (imi.isVrOnly()) {                    continue;  // Skip VR-only IME, which isn't supported for now.                }                methodList.add(imi);                methodMap.put(imi.getId(), imi);                if (DEBUG) {                    Slog.d(TAG, "Found an input method " + imi);                }            } catch (Exception e) {                Slog.wtf(TAG, "Unable to load input method " + imeId, e);            }        }    }
            @UserIdInt int userId, ArrayMap> additionalSubtypeMap,
            ArrayMap methodMap, ArrayList methodList) {
        methodList.clear();
        methodMap.clear();
 
        // 1. 获取所有包含action为android.view.InputMethod的IntentFilter的Service
        final List services = context.getPackageManager().queryIntentServicesAsUser(
                new Intent(InputMethod.SERVICE_INTERFACE),
                PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS,
                userId);
 
        methodList.ensureCapacity(services.size());
        methodMap.ensureCapacity(services.size());
 
        // 2. 遍历获取的Service列表
        for (int i = 0; i < services.size(); ++i) {
            ResolveInfo ri = services.get(i);
            ServiceInfo si = ri.serviceInfo;
            final String imeId = InputMethodInfo.computeId(ri);
 
            // 3. 对每一个Service检查是否有android.permission.BIND_INPUT_METHOD权限
            if (!android.Manifest.permission.BIND_INPUT_METHOD.equals(si.permission)) {
                Slog.w(TAG, "Skipping input method " + imeId
                        + ": it does not require the permission "
                        + android.Manifest.permission.BIND_INPUT_METHOD);
                continue;
            }
 
            if (DEBUG) Slog.d(TAG, "Checking " + imeId);
 
            try {
                // 4. 创建InputMethodInfo对象,并放入List和Map中
                final InputMethodInfo imi = new InputMethodInfo(context, ri,
                        additionalSubtypeMap.get(imeId));
                if (imi.isVrOnly()) {
                    continue;  // Skip VR-only IME, which isn't supported for now.
                }
                methodList.add(imi);
                methodMap.put(imi.getId(), imi);
                if (DEBUG) {
                    Slog.d(TAG, "Found an input method " + imi);
                }
            } catch (Exception e) {
                Slog.wtf(TAG, "Unable to load input method " + imeId, e);
            }
        }
    }

所以一个组件是不是输入法是可以通过以下条件判断:


  1. 是否是Service
  2. Service是否包含action为android.view.InputMethod的IntentFilter
  3. Service是否拥有android.permission.BIND_INPUT_METHOD权限

若条件都满足,会创建一个InputMethodInfo对象,每一个输入法Service(即IMS)都对应一个InputMethodInfo对象。InputMethodInfo主要存储了以下信息:


  1. 输入法的id
    id是IMS在IMMS中的唯一标识,也是输入法Map的key。如果一个输入法应用包名是com.demo.ime,对应IMS的全路径是com.demo.ime.DemonIME,那么它的id为com.demo.ime/.DemoIME
  2. 输入法支持的subType
    IMS在Manifest声明的同时也必须提供meta-data,包含了一个XML。XML里声明了输入法支持的subType,允许不支持subType或者支持多个subType。
  3. 输入法设置的Activity组件名
    XML里也可以声明输入法的设置Actvity,允许用户从系统设置界面直接进入输入法设置 界面。

subType指的是输入法向系统声明所支持的语言和类型(仅仅是声明,和实际支持的语言无关)。于是就会出现这样一种场景,当系统设置了多个语言,声明的subType包含了两个或以上的系统语言时,就会出现多个选项。


android 覆盖 输入法设置 android输入法框架_android xml解析框架_03

同一个输入法,会显示出多个选项,这无疑会对用户产生干扰。所以要么在xml不声明subType,要么在其中一个subType中加入


android:overridesImplicitlyEnabledSubtype="true"

这个属性默认是false,显示设置成true后,当前的subType会覆盖其他的subType, 保证最后出现在列表中的只有一个选项。


Note:如果开发系统预装输入法,建议声明subType,否则首次开机启动可能会出现找不到输入法的情况。

  绑定输入法  

首先要明确输入法(IMS)实际是一个Service,绑定输入法实际就是IMMS绑定IMS的过程,就需要遵循Service的绑定流程。


android 覆盖 输入法设置 android输入法框架_android xml解析框架_04

注意:Service被非正常方式解绑才会回调onServiceDisconnected,unbindService不会回调该方法。

@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {    synchronized (mMethodMap) {        if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {            // 1. 获取输入法进程的一个远程代理对象            mCurMethod = IInputMethod.Stub.asInterface(service);            if (mCurToken == null) {                Slog.w(TAG, "Service connected without a token!");                unbindCurrentMethodLocked();                return;            }            if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);            // Dispatch display id for InputMethodService to update context display.            // 2. 发送初始化消息            executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(                    MSG_INITIALIZE_IME, mCurTokenDisplayId, mCurMethod, mCurToken));            if (mCurClient != null) {                // 3. 发送消息获取Session的消息                clearClientSessionLocked(mCurClient);                requestClientSessionLocked(mCurClient);            }        }    }} @Overridepublic void onServiceDisconnected(ComponentName name) {    // Note that mContext.unbindService(this) does not trigger this.    synchronized (mMethodMap) {        if (DEBUG) Slog.v(TAG, "Service disconnected: " + name                + " mCurIntent=" + mCurIntent);        if (mCurMethod != null && mCurIntent != null                && name.equals(mCurIntent.getComponent())) {            // 1. mCurMethod置为null            clearCurMethodLocked();            // 2. 更新上次绑定时间            mLastBindTime = SystemClock.uptimeMillis();            mShowRequested = mInputShown;            mInputShown = false;            // 3.解除客户端和输入法之间的关联            unbindCurrentClientLocked(InputMethodClient.UNBIND_REASON_DISCONNECT_IME);        }    }} static class SessionState {    final ClientState client;    final IInputMethod method;     IInputMethodSession session;    InputChannel channel;}

在onServiceConnected中主要做了三件事


  1. 通过回调的参数IBinder对象,获得IInputMethod接口的实现并赋值给mCurMethod。IMMS通过它去调用IMS的相关接口。
  2. 通过消息调用IMS的初始化方法,后续流程大多在IMS里,这里先不作详细展开。
  3. 通过一系列复杂调用和回调,从输入法进程获得IInputMethodSession接口的实现,同时创建一个SessionState的实例保存这个对象。

SessionState表示当前输入法的会话状态,保存了输入法向外暴露的两个接口,和ClientState互相引用。在onServiceDisconnected中则包含了以下逻辑:


  1.  将mCurMethod置为null。
  2.  更新mLastBindTime为当前时间,这个变量记录了上次绑定的时间。
  3.  解除客户端和输入法之间的联系。

在未绑定输入法时,

触发绑定流程一般有如下两种场景:焦点变化,点击编辑框。当焦点变化时(包括窗口焦点变化和View焦点变化),当前应用会通过IMM跨进程调用IMMS的startInputOrWindowGainFocus方法,最终会走到startInputUncheckdLocked方法。

InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,        @MissingMethodFlags int missingMethods, @NonNull EditorInfo attribute ......) {          ......      // 1. 这里会检查默认输入法是否有变化    // Check if the input method is changing.    // We expect the caller has already verified that the client is allowed to access this    // display ID.    if (mCurId != null && mCurId.equals(mCurMethodId)            && displayIdToShowIme == mCurTokenDisplayId) {        .......                  // 2. 如果没有变化,这里会return,不会走下面逻辑    }      InputMethodInfo info = mMethodMap.get(mCurMethodId);      // 3. 这里先解绑当前输入法    unbindCurrentMethodLocked();      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));      // 4. 绑定新的输入法    if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {        // 5. 记录一个绑定时间,并更新mHaveConnection标记位        mLastBindTime = SystemClock.uptimeMillis();        mHaveConnection = true;        ......        return new InputBindResult(                InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,                null, null, mCurId, mCurSeq, null);    }    mCurIntent = null;    Slog.w(TAG, "Failure connecting to input method service: " + mCurIntent);    return InputBindResult.IME_NOT_CONNECTED;}

和绑定流程相关的逻辑如下:

  1. 如果默认输入法有变化或者默认输入法是未绑定(mCurMethod为null),会向下执行,否则直接return。
  2. 解绑并重新输入法。
  3. 赋值上次绑定时间mLastBindTime为当前时间,赋值标志位mHaveConnection为true。

如果编辑框已经获得焦点(未获得焦点会走上面的流程),点击编辑框,最终会调用到showCurrentInputLocked方法。

boolean showCurrentInputLocked(int flags, ResultReceiver resultReceiver) {    ......    boolean res = false;    if (mCurMethod != null) {        // 1. 如果已经绑定则走之后显示输入法的流程        .......    } else if (mHaveConnection && SystemClock.uptimeMillis()            >= (mLastBindTime+TIME_TO_RECONNECT)) {        // 2. 如果没有绑定且标志位为true,同时距离上次绑定已经超过设定的阈值,通常为三秒,则解绑再绑定。        Slog.w(TAG, "Force disconnect/connect to the IME in showCurrentInputLocked()");        mContext.unbindService(this);        bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS);    } else {        if (DEBUG) {            Slog.d(TAG, "Can't show input: connection = " + mHaveConnection + ", time = "                    + ((mLastBindTime+TIME_TO_RECONNECT) - SystemClock.uptimeMillis()));        }    }      return res;}

当同时满足以下条件时会走进绑定流程

  1. 当前没有绑定输入法(mCurMethod没有被赋值)
  2. mHaveConnection是true
  3. 距离上次绑定已超过阈值3秒

所以当输入法绑定时间超过了3秒,也就是在3秒内没有回调onServicConnected的场景下会走重新绑定的流程。

  IMMS的BUG  

那么问题来了,我们曾多次接到线上用户反馈,键盘调不起来。通过抓取线上用户的Log,发现了某些系统Log在短时间内大量打印,类似下图。

android 覆盖 输入法设置 android输入法框架_android service 权限_05

根据之前我们的分析,上述Log表明IMMS短时间内多次绑定和解绑输入法,当然键盘就一直调不起来了。

分析具体原因,于是就有了下面的复现路径:

  1. 进程意外死亡会触发onServiceDisconnected,mCurMethod设置为null,同时更新一次mLastBindTime。
  2. 接着会调用IMMS的startInputOrWindowGainFocus,绑定输入法和再次更新mLastBindTime。
  3. 点击编辑框,触发showCurrentInputLocked
  4. 如果已经绑定成功则执行后续键盘显示流程并返回,不会执行下面的流程 
  5. 如果未绑定成功但是时间未超过3s,则打印一行log
  6. 如果时间已超过3s仍未绑定成功,则先解绑, IMS执行onDestroy流程,再绑定,重新执行onCreate→ onBind流程,但此时mLastBindTime不会再更新。

如果首次绑定时间超过3秒(进程启动时间+IMS绑定时间),那么重复点击就会重复执行步骤3 ~ 6,如果在两次点击之间无法绑定成功,而又因为mLastBindTime不再更新的缘故,每次都会判断超过3s,不断重新走解绑和绑定流程。

这显然是一个IMMS的BUG且存在已久,对于应用开发来说,尽可能把首次绑定时间缩短到3秒以内则可以有效避免这个问题。

  管理客户端进程  

IMM是一个单例(不考虑在多屏情况下),在进程中被创建的时候会在IMMS内部生成一个对应的ClientState对象,放在一个Map中缓存起来。当进程死亡后,会从Map中移除。


@Overridepublic void addClient(IInputMethodClient client, IInputContext inputContext,        int selfReportedDisplayId) {    ......    synchronized (mMethodMap) {        ......        final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);        try {            // 1. 注册进程死亡监听,方便进程死亡后从map中移除            client.asBinder().linkToDeath(deathRecipient, 0);        } catch (RemoteException e) {            throw new IllegalStateException(e);        }        // 2. 创建ClientState对象,把参数client和inputContext缓存起来        mClients.put(client.asBinder(), new ClientState(client, inputContext, callerUid,                callerPid, selfReportedDisplayId, deathRecipient));    }}

ClientState负责管理当前客户端的状态,包含了以下成员变量


  1. client: IInputMethodClient接口的实现,作为参数由IMM传递给IMMS,IMMS可以通过它与对应进程的IMM通信。
  2. inputContext: IInputContext接口的实现,默认的InputConnection,由IMM传过来的,实际我们在使用的不是这个对象。
  3. binding: 封装了当前客户端的uid,pid等,最后会传给IMS,但是很少使用。
  4. curSession: 每一个输入法被系统绑定后会生成一个SessionState,这里缓存的就是当前默认输入法的SessionState。最终curSession会被传递给客户端进程,用于IMM调用IMS的相关接口。
static final class ClientState {    // 1. IMMS通过它与IMM通信    final IInputMethodClient client;    // 2. 默认的inputConnection,实际在使用的时候用的并不是这个对象。    final IInputContext inputContext;    final int uid;    final int pid;    final int selfReportedDisplayId;    // 3. 这个会传给IMS,只是对uid/pid/inputContext做了一层封装,用到的时候不多    final InputBinding binding;    final ClientDeathRecipient clientDeathRecipient;     boolean sessionRequested;    // Determines if IMEs should be pre-rendered.    // DebugFlag can be flipped anytime. This flag is kept per-client to maintain behavior    // through the life of the current client.    boolean shouldPreRenderIme;    // 4. 表示当前默认输入法的会话状态    SessionState curSession;}



  最后  

IMMS是IMS和IMM沟通的桥梁,绑定输入法和管理客户端都会与IMS和IMMS有大量的交互,以上只是单纯从IMMS的角度去分析,后续在分析IMS和IMM的时候会结合到IMMS,对整个流程进行详细的拆解,以点到面去完成对IMF的深入理解。