Android 11 InputMethod工作原理(二)

上一篇文章探究了用户点击输入框,系统自动拉起IME软键盘的整个工作流程。在这一篇文章中,将从另外一个角度出发,看看在软键盘按下字母时,IME是如何将它转换为中文字符,当选择某个中文字符时,系统又是如何将字符填充到输入框中的。

Q1:软键盘是如何将拼音转换成汉字的?

当用户在界面上敲下hong这个拼音时,输入法是如何将它转换成可选的中文汉字的?其根本在于中文字典的维护和字典索引逻辑的设计。虽然各个输入法的细节逻辑上都有所不同,但核心的设计思想都是类似的。

以开源软件RIME为例,它的中文字典如下所示:

可以看到,字典中一般由三部分内容组成,分别是 文字------编码------词频,除了文字是必选项,其它的都属于可选的内容。比如在其它的某些输入法的英文字典中,单纯以以下形式表现也是完全OK的:

复制代码
animator
apple

可以看到,由于英文的特殊性,它可能并不需要编码;词频也是可以省略的,就单纯按照从上到下的顺序作为词频。

字典索引逻辑也是八仙过海各显神通,实际上每种语言编好字典以后,问题就转换成了 如何从字典中找出给定编码的所有文字,并按定义的频次由高到低排列。这个编码问题就不难了吧,90%以上的工程师都能完成这个功能。

但实际上这只是完成了一个最简单的输入,现代输入法为了更好地提高输入效率,在能有的基础上加了很多花活,来让它变得好用,举例来说:

在输入法上键入天天两个字后,它会自动推荐补全开心两个字。其实这还是基于现有字典的应用,IME在字典的基础上开发了一个自动补全器,从字典里索引能和天天两个字搭配起来的词组并作为推荐内容显示。

更进一步的操作,还有根据用户习惯动态调整词频,甚至使用语言模型优化输出结果等做法。因此,IME的门槛并不在于其本身的输入技术,而在于如何让它变得更智能上。

Q2:软键盘的文字是如何传输到输入框中的?

输入法与EditText之间能够沟通,它们之间必然存在一个跨进程桥梁,这个桥梁就是IInputContext。为了了解这个过程,我们先得回顾一下上篇文章调起输入法过程的内容。

在调起输入法的过程中,会调用到InputMethodManagerstartInputInner方法,其中有如下片段:

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中,作为IMMSIMS通讯的桥梁。

事实确实如此,具体的追踪细节这里不在阐述了,简单地写一下调用链吧: 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;
            }

这里调用到了InputConnectioncommitText方法里了。还记得前文提到的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处理方式即可。

来看一下TextViewonKeyDown方法,代码如下:

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有TextKeyListenerDialerKeyListenerDigitsKeyListener等,但它们都存在一个共同的父类,即BaseKeyListener。为了方便接下来的分析,本文就以BaseKeyListeneronKeyOther方法来继续追踪。

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控件,将修改后的文本显示到界面上。 至此,将软键盘输入的文字传输到文本框控件的整个流程已执行完毕。

相关推荐
_extraordinary_1 小时前
MySQL 事务(二)
android·数据库·mysql
鸿蒙布道师5 小时前
鸿蒙NEXT开发动画案例5
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
橙子1991101611 小时前
在 Kotlin 中什么是委托属性,简要说说其使用场景和原理
android·开发语言·kotlin
androidwork11 小时前
Kotlin Android LeakCanary内存泄漏检测实战
android·开发语言·kotlin
笨鸭先游12 小时前
Android Studio的jks文件
android·ide·android studio
gys989512 小时前
android studio开发aar插件,并用uniapp开发APP使用这个aar
android·uni-app·android studio
H3091912 小时前
vue3+dhtmlx-gantt实现甘特图展示
android·javascript·甘特图
像风一样自由12 小时前
【001】renPy android端启动流程分析
android·gitee
千里马学框架14 小时前
重学安卓14/15自由窗口freeform企业实战bug-学员作业
android·framework·bug·systrace·安卓framework开发·安卓窗口系统·自由窗口
xianrenli3819 小时前
android特许权限调试
android