android输入法机制的学习总结

android输入法机制包含三部分:

  1. 输入法服务(InputMethodService),简称IMS;
  2. 输入法系统服务(InputMethodManagerService),简称IMMS;
  3. 客户端app(即当前要输入内容的app);

android中的四大组件,其中经常用的包含Activity和Service。它们就像是系统和app通信的接口一样。通过Activity中可以展示UI,业务处理等。Service也同样可以做到。输入法就是靠Service来展示UI,业务处理的。

抛出几个问题

  1. 输入法Service是如何启动的呢?
  2. 输入法Service是如何展示UI(键盘)的呢?
  3. 第三方app如何向输入法service发信息的呢?比如发起弹键盘的请求。
  4. 该Service如何向第三方app发信息的呢?比如把按键信息传给第三方app的EditText。

整体概括

输入法服务的启动以及和第三方app的关系的搭建,离不开IMMS(InputMethodManagerService)和其它系统服务。而启动、关系搭建、通信过程,离不开binder。

先把图奉上

图里边用红色数字标注的地方是用binder进行通信的,而蓝色标注的A,B处也是用binder通信,蓝色部分标注的是和普通app的按键触屏通信机制一样,在这里先不分析了。

首先简洁的一笔带过IMMS的启动,它的启动是在SystemServer运行起来时,会调用代码startOtherServices,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
// Start services.
try {
traceBeginAndSlog("StartServices");
startBootstrapServices();
startCoreServices();
startOtherServices();
SystemServerInitThreadPool.shutdown();
} catch (Throwable ex) {
throw ex;
} finally {
traceEnd();
}

在startOtherServices中,有这段代码

1
2
3
4
5
// Bring up services needed for UI.
if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);
// ... ...
}

这里就是启动了InputMethodManagerService。具体代码在SystemServer.java中。而SystemServer属于什么进程?被谁启动?在这里不分析。

输入法Service的启动

那么输入法服务是被谁启动的呢?没错,是它是它就是它,我们的朋友IMMS。摘一段IMMS的代码(来自于InputMethodManagerService.java),删去了一些代码,简化如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
InputBindResult startInputInnerLocked() {
InputMethodInfo info = mMethodMap.get(mCurMethodId);
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 (bindCurrentInputMethodService(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
mCurToken = new Binder();
mIWindowManager.addWindowToken(mCurToken, TYPE_INPUT_METHOD, DEFAULT_DISPLAY);
return new InputBindResult(null, null, mCurId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
} else {
mCurIntent = null;
}
return null;
}

通过代码我们看到了这个方法里,绑定了当前输入法服务(bindCurrentInputMethodService)。为什么当前输入法呢?看代码这里InputMethodInfo info = mMethodMap.get(mCurMethodId)。map这里存储了百度、搜狗、讯飞、KK等一系列输入法的信息。至于mIWindowManager.addWindowToken和InputBindResult,先不考虑。只需要简要知道,这个方法启动了输入法服务。

这个方法是被谁调用的呢?它是被IMMS中的startInputOrWindowGainedFocus方法调用,而startInputOrWindowGainedFocus是被第三方app请求弹起输入法时通过binder机制调用。(startInputOrWindowGainedFocus是在IInputMethodManager.aidl声明的)。

第三方app请求弹起输入法时是如何通过binder机制调用到这里startInputOrWindowGainedFocus的?这里要看第三方app进程中的InputMethodManager。

InputMethodManager

InputMethodManager是第三方app所在进程的一个对象,它有startInputInner这个方法,方法内部有一段这样的代码

1
2
3
4
5
6
7
8
try {
// ...
final InputBindResult res = mService.startInputOrWindowGainedFocus(
startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode,
windowFlags, tba, servedContext,missingMethodFlags);
} catch (RemoteException e) {
// ...
}

注意,注意!第三方app这里也调用了startInputOrWindowGainedFocus方法,它和IMMS中的startInputOrWindowGainedFocus是通过binder机制通信的。

InputMethodManager的startInputInner方法会在编辑框获取焦点时被调用。

输入法服务如何展现UI键盘

先上个图,展示输入法服务的类继承关系。

在去了解AbstractInputMethodService、InputMethodService前,先抛出一个认知。

通常我们要展示一个界面时,除了用activity之外,我们也可以获取WindowManager,然后调用它的addView。也可以通过popupwindow、dialog展示界面。

输入法的界面就是靠最后一种方式(dialog)展示出来的,而调用dialog的地方,必然是在AbstractInputMethodService、InputMethodService这2个类中某一个地方。

然后,我们去分析一下InputMethodService。发现该类中有个私有字段

1
SoftInputWindow mWindow;

而这个SoftInputWindow正是继承了Dialog。那么说,输入法的UI所需要的view,必然是添加到mWindow中,然后靠mWindow的show方法来显示界面。照这个思路去分析相应的代码。

在InputMethodService重写的onCreate方法中,创建了SoftInputWindow实例,该实例赋值给mWindow,然后调用方法initViews()。在initViews方法中,创建了一个mRootView,然后把该mRootView作为参数传入到mWindow的setContentView方法中。

InputMethodService提供了一个方法public View onCreateInputView(),该方法返回的view是挂接到mRootView的树结构的某一个节点上的,然后我们就可以继承InputMethodService来实现这个onCreateInputView(),这样就可以自定义键盘的外观了。

回头总结下,原来输入法service中有个mWindow,类型是继承了Dialog的SoftInputWindow,它的contentView是mRootView,然后输入法的view是通过onCreateInputView()方法创建出来后,挂接到mRootView中的。

而什么时候调用mWindow的show方法呢?我们看到InputMethodService有个方法showWindowInner,在这个方法尾部调用了mWindow.show()。那么showWindowInner是干什么的?谁调用了它?看名字就知道它是要弹起输入法,一定是弹起输入法时,某个系统回调中调用了它。

我把showWindowInner方法的代码展示出来入下。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void showWindowInner(boolean showInput) {
boolean doShowInput = false;
final int previousImeWindowStatus =
(mWindowVisible ? IME_ACTIVE : 0) | (isInputViewShown() ? IME_VISIBLE : 0);
mWindowVisible = true;
if (!mShowInputRequested && mInputStarted && showInput) {
doShowInput = true;
mShowInputRequested = true;
}

if (DEBUG) Log.v(TAG, "showWindow: updating UI");
initialize();
updateFullscreenMode();
updateInputViewShown();

if (!mWindowAdded || !mWindowCreated) {
mWindowAdded = true;
mWindowCreated = true;
initialize();
if (DEBUG) Log.v(TAG, "CALL: onCreateCandidatesView");
View v = onCreateCandidatesView();
if (DEBUG) Log.v(TAG, "showWindow: candidates=" + v);
if (v != null) {
setCandidatesView(v);
}
}
if (mShowInputRequested) {
if (!mInputViewStarted) {
if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
mInputViewStarted = true;
onStartInputView(mInputEditorInfo, false);
}
} else if (!mCandidatesViewStarted) {
if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");
mCandidatesViewStarted = true;
onStartCandidatesView(mInputEditorInfo, false);
}

if (doShowInput) {
startExtractingText(false);
}

final int nextImeWindowStatus = IME_ACTIVE | (isInputViewShown() ? IME_VISIBLE : 0);
if (previousImeWindowStatus != nextImeWindowStatus) {
mImm.setImeWindowStatus(mToken, mStartInputToken, nextImeWindowStatus,
mBackDisposition);
}
if ((previousImeWindowStatus & IME_ACTIVE) == 0) {
if (DEBUG) Log.v(TAG, "showWindow: showing!");
onWindowShown();
mWindow.show();
// Put here rather than in onWindowShown() in case people forget to call
// super.onWindowShown().
mShouldClearInsetOfPreviousIme = false;
}

这里说的净是InputMethodService内部的东西,AbstractInputMethodService到底干了什么?以后会分析。

第三方app和输入法service如何通信

第三方app可以控制输入法弹出和收起等。我们都知道进程间通信靠binder。这里也不例外。but!!!还记得这个输入法Service是在IMMS中bind的吧(不记得了就查IMMS中的这个方法bindCurrentInputMethodService),而不是在第三方app中bind的。所以这个输入法service所对应的binder对象应该是存在于IMMS,而现在是要第三方app通过持有的binder向输入法service通信,该怎么办?

最初始的binder关联是这样的。

紧接着第三方app通过IMMS和IMS进行通信,于是就变成了这样。

这样就可以实现,客户端app去通知输入法Service弹出键盘,在通知时,把自己的一个binder作为参数最终传给了IMS,于是就变成了这样。IMS可以通过这个binder向客户端app通信。

此时大家会发出质疑,“是这样吗?没代码你说个河蟹啊。上代码!”

代码分析

第三方app持有IMMS的binder

第三方app是如何持有IMMS的binder的呢?第三方app运行起来后,会持有一个InputMethodManager对象(简称IMM),平时调用context.getSystemService(Context.INPUT_METHOD_SERVICE)就是获取的这个IMM,IMM的构造方式是

1
2
3
4
InputMethodManager(Looper looper) throws ServiceNotFoundException {
this(IInputMethodManager.Stub.asInterface(
ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)), looper);
}

这个IInputMethodManager对应的就是InputMethodManagerService,我们看IMMS的类的定义

1
2
3
public class InputMethodManagerService extends IInputMethodManager.Stub
implements ServiceConnection, Handler.Callback {
...

IInputMethodManager对应的aidl文件是IInputMethodManager.aidl:

1
2
3
4
5
6
7
8
9
10
11
interface IInputMethodManager {
void addClient(in IInputMethodClient client,
in IInputContext inputContext, int uid, int pid);
boolean showSoftInput(in IInputMethodClient client, int flags,
in ResultReceiver resultReceiver);
boolean hideSoftInput(in IInputMethodClient client, int flags,
in ResultReceiver resultReceiver);
InputBindResult startInputOrWindowGainedFocus(int startInputReason,
in IInputMethodClient client, in IBinder windowToken, int controlFlags,int softInputMode,int windowFlags, in EditorInfo attribute, IInputContext inputContext,int missingMethodFlags,int unverifiedTargetSdkVersion);
// ...
}

IMMS持有输入法Service的binder

IMMS在调用bindCurrentInputMethodService时,传入了一个ServiceConnection(其实就是IMMS自己实现了这个接口)。

在onServiceConnected方法中获取到类型是IInputMethod的binder,并赋值给mCurMethod。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mMethodMap) {
if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
mCurMethod = IInputMethod.Stub.asInterface(service);
if (mCurToken == null) {
Slog.w(TAG, "Service connected without a token!");
unbindCurrentMethodLocked(false);
return;
}
executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
MSG_ATTACH_TOKEN, mCurMethod, mCurToken));
if (mCurClient != null) {
clearClientSessionLocked(mCurClient);
requestClientSessionLocked(mCurClient);
}
}
}
}

在输入法Service这一端,上边提到到AbstractInputMethodService开始发挥自己的功能了,它实现了onBind方法,返回IInputMethodWrapper。IInputMethodWrapper继承了一个Stub,实现了IInputMethod接口。

1
2
3
4
5
6
7
@Override
final public IBinder onBind(Intent intent) {
if (mInputMethod == null) {
mInputMethod = onCreateInputMethodInterface();
}
return new IInputMethodWrapper(this, mInputMethod);
}

IInputMethod.aidl如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
oneway interface IInputMethod {
void attachToken(IBinder token);
void bindInput(in InputBinding binding);
void unbindInput();
void startInput(in IBinder startInputToken, in IInputContext inputContext, int missingMethods,
in EditorInfo attribute, boolean restarting);
void createSession(in InputChannel channel, IInputSessionCallback callback);
void setSessionEnabled(IInputMethodSession session, boolean enabled);
void revokeSession(IInputMethodSession session);
void showSoftInput(int flags, in ResultReceiver resultReceiver);
void hideSoftInput(int flags, in ResultReceiver resultReceiver);
void changeInputMethodSubtype(in InputMethodSubtype subtype);
}

现在知道了:

  1. 第三方app持有IMMS的binder IInputMethodManager,这个IInputMethodManager实现类就是IMMS(InputMethodManagerService);
  2. IMMS持有输入法Service的binder IInputMethod,这个IInputMethod的实现类是IInputMethodWrapper;

app和输入法关联

app向IMMS发起可输入请求,是靠调用IInputMethodManager的startInputOrWindowGainedFocus,IMMS收到消息后,向输入法service发起请求,是靠调用IInputMethod的startInput。

注意!注意!这两个aidl的startInput都有个参数IInputContext,它也是个binder。这个参数最终传给了输入法Service。具体代码如下。

第三方app端,InputMethodManager的方法startInputInner中有一段代码是


这个ControlledInputConnectionWrapper既是IInputContext的binder。

IMMS收到消息后如何发送消息startInput给IMS的,IMMS这里边逻辑太多,这里不再细说。

IMS端IInputMethodWrapper.java中,类型是IInputContext的inputContext作为参数传给了InputConnectionWrapper,InputConnectionWrapper对象赋值给了InputConnection。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case DO_START_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
final int missingMethods = msg.arg1;
final boolean restarting = msg.arg2 != 0;
final IBinder startInputToken = (IBinder) args.arg1;
final IInputContext inputContext = (IInputContext) args.arg2;
final EditorInfo info = (EditorInfo) args.arg3;
final InputConnection ic = inputContext != null
? new InputConnectionWrapper(mTarget, inputContext, missingMethods) : null;
info.makeCompatible(mTargetSdkVersion);
inputMethod.dispatchStartInputWithToken(ic, info, restarting /* restarting */,
startInputToken);
args.recycle();
return;
}

总结一下。

  1. 客户端app通过IMMS,把IInputContext这个binder传递给了IMS,这个过程都使用了binder机制。
  2. IMS端靠这个IInputContext向客户端app发送指令(文字、符号等)。

IInputContext.aidl是:

1
2
3
4
5
6
oneway interface IInputContext {
void deleteSurroundingText(int leftLength, int rightLength);
void commitText(CharSequence text, int newCursorPosition);
void commitCompletion(in CompletionInfo completion);
// ...
}






草稿

IMS一端

类的关系

我先把类列出来

  1. InputMethodService;
  2. IInputMethodWrapper;
  3. InputMethod <|– AbstractInputMethodImpl <|– InputMethodImpl;
  4. InputConnectionWrapper(它实现了InputConnection接口);
  5. InputContextCallback;

这些类的关系是什么呢,于是根据代码画了一张UML图

概括说,service通过onBind()返回一个继承了Stub的IInputMethodWrapper对象,IInputMethodWrapper内部弱引用了一个InputMethodImpl对象。那么InputConnectionWrapper对象是如何被最终传递给service的呢。于是我画了一个时序图,如下。

远程IPC调用IInputMethodWrapper的startInput方法,把IInputContext引用的对象传递过来,通过时序图可以看到InputMethodService是如何得到InputConnectionWrapper对象的,从而间接地可以得到IInputContext引用的对象(InputConnectionWrapper内聚了IInputContext)。这就赋予了servie远程和第三方app客户端通信的能力。这个IInputContext提供了什么接口,service就可以和客户端做什么通信。比如它有个void commitText(CharSequence text, int newCursorPosition);接口,可以使得service提交文字到客户端相应的EditText中。

注意,这里的IInputMethodWrapper是继承了IInputMethod.Stub,所以它 is-a Binder,实现了aidl定义的接口IInputMethod,而不是InputMethod。 这个容易迷惑人,InputMethod不是aidl定义的。

客户端

  1. IInputConnectionWrapper;
  2. ControlledInputConnectionWrapper;

IInputConnectionWrapper相关的类的关系如下

在客户端的InputMethodManager的startInputInner方法中,创建了ControlledInputConnectionWrapper对象,并把它作为参数调用IInputMethodManager的startInputOrWindowGainedFocus。IInputMethodManager正是IMMS对应的binder,这样就通过IMMS把ControlledInputConnectionWrapper对应的binder传递给了IMS端。

不难想象,这里的ControlledInputConnectionWrapper所对应的service端的正是InputConnectionWrapper里边的IInputContext。

系统服务端

InputMethodManagerService

IMS端和客户端共用:InputConnection

需要用到的aidl

  1. IInputContextCallback.aidl
  2. IInputContext.aidl
  3. InputBinding.aidl
  4. IInputMethod.aidl
  5. IInputMethodManager.aidl
  6. IInputMethodClient.aidl