上一篇文章探究了用户点击输入框,系统自动拉起IME软键盘的整个工作流程。在这一篇文章中,将从另外一个角度出发,看看在软键盘按下字母时,IME是如何将它转换为中文字符,当选择某个中文字符时,系统又是如何将字符填充到输入框中的。
Q1:软键盘是如何将拼音转换成汉字的?
当用户在界面上敲下hong这个拼音时,输入法是如何将它转换成可选的中文汉字的?其根本在于中文字典的维护和字典索引逻辑的设计。虽然各个输入法的细节逻辑上都有所不同,但核心的设计思想都是类似的。
以开源软件RIME为例,它的中文字典如下所示:

可以看到,字典中一般由三部分内容组成,分别是 文字------编码------词频,除了文字是必选项,其它的都属于可选的内容。比如在其它的某些输入法的英文字典中,单纯以以下形式表现也是完全OK的:
animator
apple
可以看到,由于英文的特殊性,它可能并不需要编码;词频也是可以省略的,就单纯按照从上到下的顺序作为词频。
字典索引逻辑也是八仙过海各显神通,实际上每种语言编好字典以后,问题就转换成了 如何从字典中找出给定编码的所有文字,并按定义的频次由高到低排列。这个编码问题就不难了吧,90%以上的工程师都能完成这个功能。
但实际上这只是完成了一个最简单的输入,现代输入法为了更好地提高输入效率,在能有的基础上加了很多花活,来让它变得好用,举例来说:
在输入法上键入天天两个字后,它会自动推荐补全开心两个字。其实这还是基于现有字典的应用,IME在字典的基础上开发了一个自动补全器,从字典里索引能和天天两个字搭配起来的词组并作为推荐内容显示。
更进一步的操作,还有根据用户习惯动态调整词频,甚至使用语言模型优化输出结果等做法。因此,IME的门槛并不在于其本身的输入技术,而在于如何让它变得更智能上。
Q2:软键盘的文字是如何传输到输入框中的?
输入法与EditText之间能够沟通,它们之间必然存在一个跨进程桥梁,这个桥梁就是IInputContext
。为了了解这个过程,我们先得回顾一下上篇文章调起输入法过程的内容。
在调起输入法的过程中,会调用到InputMethodManager
的startInputInner
方法,其中有如下片段:
less
InputMethodManager.java
boolean startInputInner(@StartInputReason int startInputReason,
@Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
@SoftInputModeFlags int softInputMode, int windowFlags) {
...
//Attention !!!
InputConnection ic = view.onCreateInputConnection(tba);
if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);
synchronized (mH) {
...
if (ic != null) {
mCursorSelStart = tba.initialSelStart;
mCursorSelEnd = tba.initialSelEnd;
mCursorCandStart = -1;
mCursorCandEnd = -1;
mCursorRect.setEmpty();
mCursorAnchorInfo = null;
final Handler icHandler;
missingMethodFlags = InputConnectionInspector.getMissingMethodFlags(ic);
if ((missingMethodFlags & InputConnectionInspector.MissingMethodFlags.GET_HANDLER)
!= 0) {
// InputConnection#getHandler() is not implemented.
icHandler = null;
} else {
icHandler = ic.getHandler();
}
//Attention!!!
servedContext = new ControlledInputConnectionWrapper(
icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view);
} else {
servedContext = null;
missingMethodFlags = 0;
}
mServedInputConnectionWrapper = servedContext;
...
return true;
}
请注意这句话, InputConnection ic = view.onCreateInputConnection(tba);
,它创建了一个InputConnection
对象,用于将IInputContext
接收到的事件转发到相应的EditText
上。
默认情况下,View的onCreateInputConnection
方法是一个空实现,即:
typescript
View.java
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return null;
}
TextView这个方法对它进行了重载,变成了:
ini
TextView.java
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (onCheckIsTextEditor() && isEnabled()) {
...
if (mText instanceof Editable) {
InputConnection ic = new EditableInputConnection(this);
outAttrs.initialSelStart = getSelectionStart();
outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
outAttrs.setInitialSurroundingText(mText);
return ic;
}
}
return null;
}
当满足条件mText instanceof Editable
时,即控件为EditText
时,才会创建EditableInputConnection
对象。
再回到InputMethodManager
中,继续往下看,可以发现已经满足ic != null
的条件了,紧接着创建了ControlledInputConnectionWrapper
对象,并把EditText
提供的EditableInputConnection
封装了进去。
在这里创建的ControlledInputConnectionWrapper
对象可是大有讲究,来扒一扒它的继承关系,可以看到以下的链路------ControlledInputConnectionWrapper->IInputConnectionWrapper->IInputContext.Stub
,最终它继承的是IInputContext.Stub
。因此可以大胆地推测,ControlledInputConnectionWrapper
最终会通过Binder传递到IMS中,作为IMMS
与IMS
通讯的桥梁。
事实确实如此,具体的追踪细节这里不在阐述了,简单地写一下调用链吧: InputMethodManagerService#startInputOrWindowGainedFocus->InputMethodManagerService#startInputOrWindowGainedFocusInternalLocked->InputMethodManagerService#startInputUncheckedLocked->InputMethodManagerService#executeOrSendMessage->IInputMethodWrapper#startInput->>IInputMethodWrapper#executeMessage
。
现在来看,软键盘输入一段文字后,发生了什么事情。软键盘向EditText
提交文字的代码如下:
getCurrentInputConnection().commitText(String.valueOf(charCode), 1);
。
继续跟踪后如下:
arduino
InputConnectionWrapper.java
public boolean commitText(CharSequence text, int newCursorPosition) {
try {
mIInputContext.commitText(text, newCursorPosition);
notifyUserActionIfNecessary();
return true;
} catch (RemoteException e) {
return false;
}
}
可以很清楚地看到,这里发生了IPC通讯,根据之前的分析,可以直接去IPC的客户端ControlledInputConnectionWrapper
中追踪后续的内容:
arduino
IInputConnectionWrapper.java
public void commitText(CharSequence text, int newCursorPosition) {
dispatchMessage(obtainMessageIO(DO_COMMIT_TEXT, newCursorPosition, text));
}
...
case DO_COMMIT_TEXT: {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "commitText on inactive InputConnection");
return;
}
ic.commitText((CharSequence)msg.obj, msg.arg1);
return;
}
这里调用到了InputConnection
的commitText
方法里了。还记得前文提到的InputConnection
作用吗,它负责将IInputContext
接收到的事件转发到相应的EditText
上。而在之前的分析中可以发现,获取到焦点的EditText
每次都会创建一个新的EditableInputConnection
对象。
java
EditableInputConnection.java
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (mTextView == null) {
return super.commitText(text, newCursorPosition);
}
mTextView.resetErrorChangedFlag();
boolean success = super.commitText(text, newCursorPosition);
mTextView.hideErrorIfUnchanged();
return success;
}
继续往下看:
scss
BaseInputConnection.java
public boolean commitText(CharSequence text, int newCursorPosition) {
if (DEBUG) Log.v(TAG, "commitText " + text);
replaceText(text, newCursorPosition, false);
sendCurrentText();
return true;
}
private void sendCurrentText() {
...
// Otherwise, revert to the special key event containing
// the actual characters.
KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(),
content.toString(), KeyCharacterMap.VIRTUAL_KEYBOARD, 0);
sendKeyEvent(event);
content.clear();
}
}
sendCurrentText
里有一个非常关键的操作,就是把输入法输入的文字封装成了KeyEvent,然后调用sendKeyEvent
开启了事件分发流程。
android事件分发机制这里便不再阐述了,相信大家都比较熟悉了。我们只需要关注TextView
对这个特殊的KeyEvent处理方式即可。
来看一下TextView
的onKeyDown
方法,代码如下:
java
TextView.java
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
final int which = doKeyDown(keyCode, event, null);
if (which == KEY_EVENT_NOT_HANDLED) {
return super.onKeyDown(keyCode, event);
}
return true;
}
继续看doKeyDown
方法:
scss
TextView.java
private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
...
if (mEditor != null && mEditor.mKeyListener != null) {
boolean doDown = true;
if (otherEvent != null) {
try {
beginBatchEdit();
//Attention here;
final boolean handled = mEditor.mKeyListener.onKeyOther(this, (Editable) mText,
otherEvent);
hideErrorIfUnchanged();
doDown = false;
if (handled) {
return KEY_EVENT_HANDLED;
}
} catch (AbstractMethodError e) {
// onKeyOther was added after 1.0, so if it isn't
// implemented we need to try to dispatch as a regular down.
} finally {
endBatchEdit();
}
}
if (doDown) {
beginBatchEdit();
final boolean handled = mEditor.mKeyListener.onKeyDown(this, (Editable) mText,
keyCode, event);
endBatchEdit();
hideErrorIfUnchanged();
if (handled) return KEY_DOWN_HANDLED_BY_KEY_LISTENER;
}
}
...
return mPreventDefaultMovement && !KeyEvent.isModifierKey(keyCode)
? KEY_EVENT_HANDLED : KEY_EVENT_NOT_HANDLED;
}
关键在onKeyOther
这个监听,根据INPUT_TYPE类型的差别,keyListener也各不相同,可能存在的Listener有TextKeyListener
、DialerKeyListener
、DigitsKeyListener
等,但它们都存在一个共同的父类,即BaseKeyListener
。为了方便接下来的分析,本文就以BaseKeyListener
的onKeyOther
方法来继续追踪。
vbnet
BaseKeyListener.java
public boolean onKeyOther(View view, Editable content, KeyEvent event) {
...
CharSequence text = event.getCharacters();
if (text == null) {
return false;
}
//Key Sentence
content.replace(selectionStart, selectionEnd, text);
return true;
}
arduino
SpannableStringBuilder.java
public SpannableStringBuilder replace(final int start, final int end,
CharSequence tb, int tbstart, int tbend) {
...
sendTextChanged(textWatchers, start, origLen, newLen);
sendAfterTextChanged(textWatchers);
// Span watchers need to be called after text watchers, which may update the layout
sendToSpanWatchers(start, end, newLen - origLen);
return this;
}
这里修改了TextView
关联的SpannableStringBuilder
的文字内容,并通过sendTextChanged
通知TextView
控件文本内容发生了改变。
scss
TextView.java
public void onTextChanged(CharSequence buffer, int start, int before, int after) {
TextView.this.handleTextChanged(buffer, start, before, after);
if (AccessibilityManager.getInstance(mContext).isEnabled()
&& (isFocused() || isSelected() && isShown())) {
sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
mBeforeText = null;
}
}
关键语句handleTextChanged
:
arduino
TextView.java
void handleTextChanged(CharSequence buffer, int start, int before, int after) {
...
if (ims == null || ims.mBatchEditNesting == 0) {
updateAfterEdit();
}
...
}
关键语句updateAfterEdit
,在这个方法里调用了invalidate
方法,开始刷新TextView
控件,将修改后的文本显示到界面上。 至此,将软键盘输入的文字传输到文本框控件的整个流程已执行完毕。