一、前言
安卓输入法之间的通信,最重要的类就是这个 InputConnection,之前的各部分初始化流程中最重要的部分就是将IMM的IC传递给IMS。 这篇文章详细的介绍了IC是如何传递的,并且详细地给出了IC是如何被调用的。认真看完一定会有收获。同时,建议你结合源码进行调试,理解会更深刻。
1、传递流程
InputConnection 是输入法中 负责通信 的类。
流程:IC在IMM中创建,通过IMMS传递给IMS。IMS可以通过这个IC来输入字符,并在IMM的View上显示。
人话版: 手机APP 中创建IC,通过 系统服务 传递给 讯飞输入法。讯飞输入法打字时,通过IC传递信息到应用,将文字显示到APP的编辑栏上。
2、相关类
RemoteInputConnectionImpl、EditableInputConnection 和 BaseInputConnection 都是 Android 输入法(IME)框架中的 InputConnection 实现,主要用于应用和输入法之间的数据交互。
1、BaseInputConnection(基础实现)
介绍 : BaseInputConnection 是 InputConnection 的 基础实现,通常用于 EditText 这类 文本输入组件。
提供了基本的文本操作,如 commitText()、deleteSurroundingText() 等。
2、EditableInputConnection(优化的 InputConnection)
介绍: EditableInputConnection 继承自 BaseInputConnection优化了输入体验,支持 更灵活的文本操作。
3、RemoteInputConnectionImpl(远程 InputConnection,跨进程使用)
核心逻辑:RemoteInputConnectionImpl不会直接操作 EditText 的文本,而是通过 IPC 让系统或其他应用代理输入。
继承 IRemoteInputConnection.Stub,而不是 BaseInputConnection,本质上是 一个 AIDL IPC 远程服务。
接下来,我会从InputConnection的创建、传递、使用三个方面来介绍InputConnection。
二、创建
IMM.createRealInstance
//frameworks/core/java/android/view/inputmethod/InputMethodManager.java
private static InputMethodManager createRealInstance(int displayId, Looper looper) {
final IInputMethodManager service = IInputMethodManagerGlobalInvoker.getService();
if (service == null) {
throw new IllegalStateException("IInputMethodManager is not available");
}
//我们看看IMM实例里面有什么
final InputMethodManager imm = new InputMethodManager(service, displayId, looper);
...
}
private InputMethodManager(@NonNull IInputMethodManager service, int displayId, Looper looper) {
mService = service; // For @UnsupportedAppUsage
mMainLooper = looper;
mH = new H(looper);
mDisplayId = displayId;
//RemoteInputConnectionImpl 会传递给 IMS,然后IMS 来远程调用
//BaseInputConnection 是应用自身内部调用,将文字显示在编辑框中
mFallbackInputConnection = new RemoteInputConnectionImpl(looper,
new BaseInputConnection(this, false), this, null);
}
三、传递
看这部分的时候,搭配我之前的文档:安卓输入法的弹出与隐藏 本文的1.4就是原文中的1.4
1.4 IMM.startInputInner
@RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
private boolean startInputInner(@StartInputReason int startInputReason,
@Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
@SoftInputModeFlags int softInputMode, int windowFlags) {
....
final Pair<InputConnection, EditorInfo> connectionPair = createInputConnection(view);
//这个ic EditableInputConnection 对象,创建的时候就已经和 编辑框View 绑定了
final InputConnection ic = connectionPair.first;
final EditorInfo editorInfo = connectionPair.second;
final RemoteInputConnectionImpl servedInputConnection;
if (ic != null) {
Handler handler = null;
try {
handler = ic.getHandler();
} catch (AbstractMethodError ignored) {
}
icHandler = handler;
mServedInputConnectionHandler = icHandler;
//这是一个RemoteInputConnectionImpl,是负责IPC通信,里面有一个绑定好View的ic--EditableInputConnection
servedInputConnection = new RemoteInputConnectionImpl(
icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view);
} else {
servedInputConnection = null;
icHandler = null;
mServedInputConnectionHandler = null;
}
mServedInputConnection = servedInputConnection;
...
res = android.view.inputmethod.IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
startInputReason, mClient, windowGainingFocus, startInputFlags,
softInputMode, windowFlags, editorInfo, servedInputConnection,
servedInputConnection == null ? null
: servedInputConnection.asIRemoteAccessibilityInputConnection(),
view.getContext().getApplicationInfo().targetSdkVersion, targetUserId,
mImeDispatcher);
这里的流程就不一步步追了,可以看安卓输入法的弹出与隐藏 1.4 ~ 1.7,注意InputConnection的传递
1.7 IMMS.startInputUncheckedLocked
private InputBindResult startInputUncheckedLocked(@NonNull ClientState cs,
IRemoteInputConnection inputConnection,
@Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
@NonNull EditorInfo editorInfo, @StartInputFlags int startInputFlags,
@StartInputReason int startInputReason,
int unverifiedTargetSdkVersion,
@NonNull ImeOnBackInvokedDispatcher imeDispatcher,
@NonNull com.android.server.inputmethod.InputMethodBindingController bindingController) {
...
//这些都是IMMS 的私有变量,以后就不会出现cs、inputConnection,而是直接使用私有变量
mCurClient = cs;
mCurInputConnection = inputConnection;
mCurRemoteAccessibilityInputConnection = remoteAccessibilityInputConnection;
mCurImeDispatcher = imeDispatcher;
...
final String curId = bindingController.getCurId();
final int displayIdToShowIme = bindingController.getDisplayIdToShowIme();
if (curId != null && curId.equals(bindingController.getSelectedMethodId())
&& displayIdToShowIme == getCurTokenDisplayIdLocked()) {
if (cs.mCurSession != null) {
cs.mSessionRequestedForAccessibility = false;
requestClientSessionForAccessibilityLocked(cs);
attachNewAccessibilityLocked(startInputReason,
(startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0);
//下一步就会到这里,没有传递InputConnection参数
return attachNewInputLocked(startInputReason,
(startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0);
}
1.8 IMMS.attachNewInputLocked
InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) {
...
final SessionState session = mCurClient.mCurSession;
setEnabledSessionLocked(session);
//1.7 中mCurInputConnection被赋值了。就是从imm传递来的 RemoteInputConnection
session.mMethod.startInput(startInputToken, mCurInputConnection, mCurEditorInfo, restarting,
navButtonFlags, mCurImeDispatcher);
这mMethod 是一个Invoker,不知道流程的,可以看安卓输入法的弹出与隐藏 1.8 ~ 1.9,注意mCurInputConnection的传递
1.12 IMS.doStartInput
void doStartInput(InputConnection ic, EditorInfo editorInfo, boolean restarting) {
if (!restarting && mInputStarted) {
doFinishInput();
}
ImeTracing.getInstance().triggerServiceDump("InputMethodService#doStartInput", mDumper,
null /* icProto */);
mInputStarted = true;
// 至此,将应用的 RemoteInputConnection 绑定到了IMS 的私有变量上
mStartedInputConnection = ic;
mInputEditorInfo = editorInfo;
initialize();
}
四、使用
经过一番分析,我们终于理清了应用中的 InputConnection(IC)是如何绑定到 IIMS 的。
总结如下:
应用的 IC 本质上是一个 RemoteInputConnectionImpl 对象,它实现了 IRemoteInputConnection.aidl,从而支持跨进程通信(IPC)。在 RemoteInputConnectionImpl 内部,封装了一个 EditableInputConnection,而 EditableInputConnection 直接绑定到应用侧的 EditText 或其他可编辑 View。
因此,当 IMS 调用 IC 相关接口时,实际会通过 RemoteInputConnectionImpl 代理,将操作转发到 EditableInputConnection,最终作用于 View,使输入内容能够正确显示在编辑框中。
1、ic.commitText
IMS端
IMS点击文字的时候,会调用ic.commitText函数,将候选词传递给输入框,我们直接来看看吧。
讯飞输入法 继承 IMS,有很多函数都进行了重载,我只能找到父类--IMS代码,但对于理解是没有问题的。
public void sendKeyChar(char charCode) {
switch (charCode) {
//点击回车的时候会进入这个函数,但重点是default的函数
case '\n':
if (!sendDefaultEditorAction(true)) {
sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER);
}
break;
default:
if (charCode >= '0' && charCode <= '9') {367307
sendDownUpKeyEvents(charCode - '0' + KeyEvent.KEYCODE_0);
} else {
//我们先看一下getCurrentInputConnection做了什么吧
InputConnection ic = getCurrentInputConnection();
if (ic != null) {
//这里就直接跨进程带应用去了
ic.commitText(String.valueOf(charCode), 1);
}
}
break;
}
}
public InputConnection getCurrentInputConnection() {
//这个ic熟悉不,就是1.12的时候进行绑定的ic
InputConnection ic = mStartedInputConnection;
if (ic != null) {
return ic;
}
return mInputConnection;
}
IMM端
现在我们调试 应用进程 ,看看RemoteInputConnectionImpl.commitText 有没有被调用
调用成功!RemoteInputConnectionImpl中的ic是绑定了View的EditableInputConnection,继续追踪
EditableInputConnection的父类是BaseInputConnection
这个content就是View中已经有的字,ab是新字插入的位置,至此“我是程序猿”就显示在View中了。
2、ic.sendKeyEvent
在 前文,我们是使用commitText来改变输入框的内容的,当然,大部分的输入法也是这么做的。 这里说一个例外,小米安全键盘 输入数字的时候,使用的是ic.sendKeyEvent。我们下面学习一下,IMS通过sendKeyEvent 如何将内容传递到 输入框 的。
4.1 RemoteInputConnectionImpl.sendKeyEvent
//frameworks/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
public void sendKeyEvent(InputConnectionCommandHeader header, KeyEvent event) {
dispatchWithTracing("sendKeyEvent", () -> {
if (header.mSessionId != mCurrentSessionId.get()) {
return; // cancelled
}
InputConnection ic = getInputConnection();
if (ic == null || mDeactivateRequested.get()) {
Log.w(TAG, "sendKeyEvent on inactive InputConnection");
return;
}
//这个是Impl中的EditableInputConnection,我们直接看Edit父类BaseInputConnection
ic.sendKeyEvent(event);
});
}
4.2 BaseInputConnection.
//frameworks/core/java/android/view/inputmethod/BaseInputConnection.java
public boolean sendKeyEvent(KeyEvent event) {
//mTargetView -- 就是输入框组件
//接着我们去IMM看源码
mIMM.dispatchKeyEventFromInputMethod(mTargetView, event);
return false;
}
4.3 IMM.dispatchKeyEventFromInputMethod
//frameworks/core/java/android/view/inputmethod/InputMethodManager.java
public void dispatchKeyEventFromInputMethod(@Nullable View targetView,
...
synchronized (mH) {
ViewRootImpl viewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
if (viewRootImpl == null) {
final View servedView = getServedViewLocked();
if (servedView != null) {
viewRootImpl = servedView.getViewRootImpl();
}
}
if (viewRootImpl != null) {
//这个Event需要进行事件分发
viewRootImpl.dispatchKeyFromIme(event);
}
}
4.4 ViewRootImpl.dispatchKeyFromIme
//frameworks/core/java/android/view/ViewRootImpl.java
public void dispatchKeyFromIme(KeyEvent event) {
Message msg = mHandler.obtainMessage(MSG_DISPATCH_KEY_FROM_IME, event);
msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
之后就是一系列的事件分发,我们看堆栈就可以看到调用顺序了,下面我按顺序挑几个重点讲解一下。
4.5 View.dispatchKeyEvent
//frameworks/core/java/android/view/View.java
public boolean dispatchKeyEvent(KeyEvent event) {
...
//用keyEvent继续去传递事件
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
if (ViewDebugManager.DEBUG_KEY) {
ViewDebugManager.getInstance().debugEventHandled(this, event, "onKeyXXX");
}
return true;
}
4.6 KeyEvent.dispatch
可以看到receiver,即哪个组件会处理这个keyEvent
//frameworks/core/java/android/view/KeyEvent.java
public final boolean dispatch(Callback receiver, DispatcherState state,
Object target) {
switch (mAction) {
case ACTION_DOWN: {
mFlags &= ~FLAG_START_TRACKING;
if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
+ ": " + this);
//res : 返回 True 代表该receiver可以处理这个事件
//reveiver 很重要! 一般都是xxxEditText,如果是Activity的话,可能你的输入框没有焦点,此时你可能会输入不了
boolean res = receiver.onKeyDown(mKeyCode, this);
if (state != null) {
4.7 TextView.onKeyDown
为什么选取TextView? 因为大部分的输入框都是继承自 TextView
//frameworks/core/java/android/widget/TextView.java
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;
}
4.8 TextView.doKeyDown
//frameworks/core/java/android/widget/TextView.java
private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
...
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;
}
}
五、总结
自此,相信你已经了解了 IC 是如何从 IMM 传递到 IMS。 之后,你也知道了 IMS 是如何通过 IC 将候选词传递给 IMM。建议你结合源码调试,理解会更深刻。
如果你想了解 输入法的初始化可以参考我的博客 :安卓输入法源码1--IMM、IMMS、IMS启动流程
如果你想了解 输入法的弹出/隐藏/绑定全流程可以看 : 安卓输入法源码2 -- 键盘弹出与隐藏