安卓输入法源码3 -- InputConnection

一、前言

安卓输入法之间的通信,最重要的类就是这个 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 -- 键盘弹出与隐藏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值