概述
首先我们了解输入法框架(InputMethodFramework, 下简称IMF)整体的UML图,基于Android10,不同Android版本之间会有少许差异,不过不影响整体结构。- InputMethodManager(下简称IMM)是整个输入法框架的核心,运行于客户端进程,客户端可以使用IMM对输入焦点和输入法状态进行控制,但是同一时间只有一个客户端处于激活状态。
InputMethodService(下简称IMS),是输入法IME(InputMethodEditor)的具体实现,运行于输入法进程。同一时间只有一个IME运行。
InputMethodManagerService(下简称IMMS)运行于系统进程,是一个系统级服务,是IMM和IMS沟通的桥梁。
IInputMethod: 这个是输入法进程向系统进程暴露的接口。用于系统进程和输入法进程通信。
IInputMethodSession: 这个是输入法进程向外暴露的第二个接口,用于客户端进程和输入法进程通信。
IInputContext:这个是客户端进程向输入法进程暴露的接口。用于输入法进程和客户端进程通信。
IInputMethodClient:这个是客户端进程向系统进程暴露的接口。用于系统进程和客户端进程通信。
IInputMethodManager: 这个是系统进程向客户端进程暴露的接口。
以IInputMethod为例,谷歌又定义了一个新的接口InputMethod,包含了和IInputMethod相同的接口。具体的实现逻辑从IInputMethod的实现类移动到InputMethod实现类,大概是为了尽量屏蔽aidl接口对于上层实现的影响。
Note:输入法进程代表IMS所在的进程,客户端进程代表IMM所在的进程,这两者可以是同一个进程,但是即使是同一个进程仍然需要通过aidl的方式进行通信。下图是对以上接口所涉及到的部分流程进行举例
客户端点击输入框,IMM通过IInputMethodManager接口向IMMS请求显示输入法,IMMS收到请求通过IInputMethod接口向IMS进程转发请求,使输入法展现。
IMM进程当光标发生位置发生改变时,IMM通过IInputMethodSession接口,通知IMS光标位置发生变化。
IMS进程通过IInputContext将字符上屏到客户端的编辑框。
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);
}
}
}
所以一个组件是不是输入法是可以通过以下条件判断:
是否是Service
Service是否包含action为android.view.InputMethod的IntentFilter
Service是否拥有android.permission.BIND_INPUT_METHOD权限
输入法的id
id是IMS在IMMS中的唯一标识,也是输入法Map的key。如果一个输入法应用包名是com.demo.ime,对应IMS的全路径是com.demo.ime.DemonIME,那么它的id为com.demo.ime/.DemoIME
输入法支持的subType
IMS在Manifest声明的同时也必须提供meta-data,包含了一个XML。XML里声明了输入法支持的subType,允许不支持subType或者支持多个subType。
输入法设置的Activity组件名
XML里也可以声明输入法的设置Actvity,允许用户从系统设置界面直接进入输入法设置 界面。
android:overridesImplicitlyEnabledSubtype="true"
这个属性默认是false,显示设置成true后,当前的subType会覆盖其他的subType, 保证最后出现在列表中的只有一个选项。
Note:如果开发系统预装输入法,建议声明subType,否则首次开机启动可能会出现找不到输入法的情况。
绑定输入法
首先要明确输入法(IMS)实际是一个Service,绑定输入法实际就是IMMS绑定IMS的过程,就需要遵循Service的绑定流程。注意: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中主要做了三件事
通过回调的参数IBinder对象,获得IInputMethod接口的实现并赋值给mCurMethod。IMMS通过它去调用IMS的相关接口。
通过消息调用IMS的初始化方法,后续流程大多在IMS里,这里先不作详细展开。
通过一系列复杂调用和回调,从输入法进程获得IInputMethodSession接口的实现,同时创建一个SessionState的实例保存这个对象。
将mCurMethod置为null。
更新mLastBindTime为当前时间,这个变量记录了上次绑定的时间。
解除客户端和输入法之间的联系。
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;}
和绑定流程相关的逻辑如下:
如果默认输入法有变化或者默认输入法是未绑定(mCurMethod为null),会向下执行,否则直接return。
解绑并重新输入法。
赋值上次绑定时间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;}
当同时满足以下条件时会走进绑定流程
当前没有绑定输入法(mCurMethod没有被赋值)
mHaveConnection是true
距离上次绑定已超过阈值3秒
所以当输入法绑定时间超过了3秒,也就是在3秒内没有回调onServicConnected的场景下会走重新绑定的流程。
IMMS的BUG
那么问题来了,我们曾多次接到线上用户反馈,键盘调不起来。通过抓取线上用户的Log,发现了某些系统Log在短时间内大量打印,类似下图。
根据之前我们的分析,上述Log表明IMMS短时间内多次绑定和解绑输入法,当然键盘就一直调不起来了。
分析具体原因,于是就有了下面的复现路径:
进程意外死亡会触发onServiceDisconnected,mCurMethod设置为null,同时更新一次mLastBindTime。
接着会调用IMMS的startInputOrWindowGainFocus,绑定输入法和再次更新mLastBindTime。
点击编辑框,触发showCurrentInputLocked
如果已经绑定成功则执行后续键盘显示流程并返回,不会执行下面的流程
如果未绑定成功但是时间未超过3s,则打印一行log
如果时间已超过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负责管理当前客户端的状态,包含了以下成员变量
client: IInputMethodClient接口的实现,作为参数由IMM传递给IMMS,IMMS可以通过它与对应进程的IMM通信。
inputContext: IInputContext接口的实现,默认的InputConnection,由IMM传过来的,实际我们在使用的不是这个对象。
binding: 封装了当前客户端的uid,pid等,最后会传给IMS,但是很少使用。
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;}