android输入法机制包含三部分:
- 输入法服务(InputMethodService),简称IMS;
- 输入法系统服务(InputMethodManagerService),简称IMMS;
- 客户端app(即当前要输入内容的app);
android中的四大组件,其中经常用的包含Activity和Service。它们就像是系统和app通信的接口一样。通过Activity中可以展示UI,业务处理等。Service也同样可以做到。输入法就是靠Service来展示UI,业务处理的。
抛出几个问题
- 输入法Service是如何启动的呢?
- 输入法Service是如何展示UI(键盘)的呢?
- 第三方app如何向输入法service发信息的呢?比如发起弹键盘的请求。
- 该Service如何向第三方app发信息的呢?比如把按键信息传给第三方app的EditText。
整体概括
输入法服务的启动以及和第三方app的关系的搭建,离不开IMMS(InputMethodManagerService)和其它系统服务。而启动、关系搭建、通信过程,离不开binder。
先把图奉上
图里边用红色数字标注的地方是用binder进行通信的,而蓝色标注的A,B处也是用binder通信,蓝色部分标注的是和普通app的按键触屏通信机制一样,在这里先不分析了。
首先简洁的一笔带过IMMS的启动,它的启动是在SystemServer运行起来时,会调用代码startOtherServices,代码如下。
1 | // Start services. |
在startOtherServices中,有这段代码
1 | // Bring up services needed for UI. |
这里就是启动了InputMethodManagerService。具体代码在SystemServer.java中。而SystemServer属于什么进程?被谁启动?在这里不分析。
输入法Service的启动
那么输入法服务是被谁启动的呢?没错,是它是它就是它,我们的朋友IMMS。摘一段IMMS的代码(来自于InputMethodManagerService.java),删去了一些代码,简化如下。
1 | InputBindResult startInputInnerLocked() { |
通过代码我们看到了这个方法里,绑定了当前输入法服务(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 | try { |
注意,注意!第三方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 | void showWindowInner(boolean showInput) { |
这里说的净是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 | InputMethodManager(Looper looper) throws ServiceNotFoundException { |
这个IInputMethodManager对应的就是InputMethodManagerService,我们看IMMS的类的定义
1 | public class InputMethodManagerService extends IInputMethodManager.Stub |
IInputMethodManager对应的aidl文件是IInputMethodManager.aidl:
1 | interface IInputMethodManager { |
IMMS持有输入法Service的binder
IMMS在调用bindCurrentInputMethodService时,传入了一个ServiceConnection(其实就是IMMS自己实现了这个接口)。
在onServiceConnected方法中获取到类型是IInputMethod的binder,并赋值给mCurMethod。
1 | @Override |
在输入法Service这一端,上边提到到AbstractInputMethodService开始发挥自己的功能了,它实现了onBind方法,返回IInputMethodWrapper。IInputMethodWrapper继承了一个Stub,实现了IInputMethod接口。
1 | @Override |
IInputMethod.aidl如下所示。
1 | oneway interface IInputMethod { |
现在知道了:
- 第三方app持有IMMS的binder IInputMethodManager,这个IInputMethodManager实现类就是IMMS(InputMethodManagerService);
- 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 | case DO_START_INPUT: { |
总结一下。
- 客户端app通过IMMS,把IInputContext这个binder传递给了IMS,这个过程都使用了binder机制。
- IMS端靠这个IInputContext向客户端app发送指令(文字、符号等)。
IInputContext.aidl是:
1 | oneway interface IInputContext { |
草稿
IMS一端
类的关系
我先把类列出来
- InputMethodService;
- IInputMethodWrapper;
- InputMethod <|– AbstractInputMethodImpl <|– InputMethodImpl;
- InputConnectionWrapper(它实现了InputConnection接口);
- 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定义的。
客户端
- IInputConnectionWrapper;
- ControlledInputConnectionWrapper;
IInputConnectionWrapper相关的类的关系如下
在客户端的InputMethodManager的startInputInner方法中,创建了ControlledInputConnectionWrapper对象,并把它作为参数调用IInputMethodManager的startInputOrWindowGainedFocus。IInputMethodManager正是IMMS对应的binder,这样就通过IMMS把ControlledInputConnectionWrapper对应的binder传递给了IMS端。
不难想象,这里的ControlledInputConnectionWrapper所对应的service端的正是InputConnectionWrapper里边的IInputContext。
系统服务端
InputMethodManagerService
IMS端和客户端共用:InputConnection
需要用到的aidl
- IInputContextCallback.aidl
- IInputContext.aidl
- InputBinding.aidl
- IInputMethod.aidl
- IInputMethodManager.aidl
- IInputMethodClient.aidl