Android输入法框架(Input Method Framework,IMF)是Android中非常重要的模块,它分布于三个部分(确切的说,是三个进程),
- 包含编辑框的客户(Client)app,表示普通的使用输入法的app进程。当点击编辑框时,会切换出当前选中的输入法;当用户在输入法输入字符,提交候选词,则会更新到编辑框中。为了完成这些行为,它需要跟下面的两个输入法相关服务进行交互。对于普通app开发者而言,他们一般使用系统提供的EditText,该类和其父类TextView已经很好的封装了跟输入法服务之间的交互;如果是自定义的编辑框,则需要自己处理这种交互。
- 输入法(input method,IME)服务(service),是具体的输入法进程,例如自带的拉丁输入法或者谷歌,搜狗等拼音输入法。它们一般提供一个输入窗口,可以根据用户的要求打开或者关闭;可以把用户输入的字符和提交的候选词更新给client等等。这是一个用户级别的Service。为了方便开发者编写新的输入法,IMF提供了抽象基类InputMethodService供输入法开发者扩展。
- 输入法管理者(Input method manager,IMM)服务(Service),这是一个Android系统级的服务,用于管理多个输入法以及同其他系统服务(例如window manager service)进行交互。这部分代码是app开发者和IME开发者都不需要关心的。
为了方便描述,后文分别称该三个组件为:client,IME和IMM。
这三个部分需用共同合作才能完成输入法的工作。例如打开一个app,并且一个edit框获取了focus焦点。此时client会通知IMM打开输入法,然后IMM查看当前选中的IME,并调用该IME的start操作。这个简单的开始操作需要三个组件的配合。再比如用户提交了候选词,此时IME需要将候选词告诉client。这里须要IME和client的合作。
因为这三个部分是三个进程,所以它们之间必须通过IPC进行通讯。在Android中,IPC机制是通过binder机制和aidl接口进行通信的。
- 对于Client而言,它提供了两个接口IInputMethodClient.aidl和IInputContext.aidl。前者是供IMM调用的,后者是供IME调用的。
- IMM提供了接口IInputMethodManager.aidl供其他两个组件调用。
- IME提供了两个接口IInputMethod.aidl和IInputMethodSession.aidl,前者供IMM调用,后者供client直接调用。
这些调用关系可以参考下图:
这些接口定义都在java/com/android/internal/view目录下。那这些接口是如何实现的呢?
先看client提供的接口。IInputContext是由同一目录下的IInputConnectionWrapper实现的。正如名字所说,它只是一个wrapper,它把接收到的IPC消息委托给你InputConnection的一个实现。例如对于EditText而言,实现是EditableInputConneciton。
在调用方,IME也不是直接操作IInputContext接口。它会调用实现了InputConnection接口的InputConnectionWrapper(也在前面目录下)。该对象封装了从client传过来的IInputContext实例。
对于IME对client的调用操作,它会经历下面流程(以调用commitText为例,它表示提交候选词):
- InputConnectionWrapper.commitText被IME进程中其他代码调用。
- 委托给IInputContext stub对象。
- 通过IPC跨进程传输
- IInputConnectionWrapper接受到该消息并调用其commitText处理。
- 如果当前在主UI线程,则直接嗲用InputConnection的实现(例如EditableInputConnection)的commitText方法;否则通过handler进行线程间通信。
在IME看来,接口是InputConnection;在client上,实现的也是InputConnection。IInputContext完全被隐藏起来了。所以Android官方文档说IME通过InputConnection接口来操作client。
再看client提供的另外一个接口IInputMethodClient,IMM是直接调用的。IMM的代码就是InputMethodManagerService。在client端,InputMethodManager类中有一个对IInputMethodClient.stub的实现。
对IMM提供的IInputMethodManager接口而言,它是由InputMethodManagerService来实现的。在client端,InputMethodManager的getInstance(是个singleton)会调用ServiceManager.getService(Context.INPUT_METHOD_SERVICE)获取该接口,然后创建InputMethodManager。所以对于client而言,它跟IMM的交互都是通过InputMethodManager来封装完成的,并不需要关心IInputMethodManager接口。对于IME,如果它想操作IMM,也同样通过InputMethodManager。
下面是IME提供的接口。类似于使用InputConnection封装IInputContext,有两个接口InputMethod和InputMethodSession分别对应着了IInputMethod和IInputSession。
对于InputMethod,IInputMethodWrapper实现了IInputMethod.stub。对于收的的IPC请求,都转发给InputMethod实例。一般而言,这个实例是InputMethodService中定义的InputMethodImpl。该实例是InputMethodService的内部类,所以可以操作InputMethodService。对于其客户IMM,InputMethodManagerService会直接调用IInputMethod的方法发起IPC请求。
对于InputMethodSessoin,非常类似,IInputMethodSessionWrapper实现了IInputMethodSession.stub。同样在InputMethodService中有InputMethodSessionImpl实现了InputMethodSession接口,有一个该类型的对象在IInputMethodSessionWrapper中,负责具体处理过来的IPC消息。在client端,InputMethodManager有一个IInputMethodSession mCurMethod对象。开发者只需要调用InputMethodManager,而由InputMethodManager调用IInputMethodSession的IPC操作。
总结一下,无论是client还是IME的开发者,都不需要直接操作aidl接口。在client端,对于IMM和IME的操作都是通过InputMethodManager发起的,用户甚至不用关心这些IPC操作是发给谁的;在IME端,开发者通过InputConnection给client发IPC消息,通过InputMethodManager给IMM发。而在IMM端,虽然是直接操作aidl接口的stub对象,但因为一般开发者不需要改写它,所以也无关紧要。通过这种方式,简化了开发者的跨进程操作。
最后再总结下代码位置:
- 所有的aidl接口都在java/com/android/internal/view目录下。它只是internal可见,所以普通开发者无法直接访问它。
- 用户可以访问的接口或类,包括InputConnection,InputMethodManager,InputMethod,InputMethodSession都在java/android/view/inputmethod目录下。
- java/com/android/internal/view还包含IInputConnecitonWrapper,InputConnectionWrapper。
- java/android/view/inputmethod还包含BaseInputConnection
- java/android/inputmethodservice包含IME端相关代码,例如IInputMethodWrapper,IInputMethodSessionWrapper,InputMethodImpl,InputMethodSessionImpl,InputMethodService。
- IMM的InputMethodManagerService在services/java/com/android/server下。
代码主要就是这四个目录。