Android 11 InputMethod 工作原理解析(一)

当用户在Android系统的输入框轻点,就会弹出预设的输入法软件,点击软件上的字符,能够拼出中文字词,并填入到输入框中。在这个简单的场景中,Android系统究竟又做了哪些复杂的工作,才能让整套流程运转起来呢?这一篇文章将从用户点击输入框的这一个动作开始,一步步逐渐深入揭示InputMethod背后的工作原理。

Question:当按下输入框的那一刻,究竟发生了什么?

现在从事件的源头开始,来看看TextViewperformAccessibilityActionClick方法:

java 复制代码
//TextView.java
private boolean performAccessibilityActionClick(Bundle arguments) {
      ...
		if (isClickable() || isLongClickable()) {
            // Simulate View.onTouchEvent for an ACTION_UP event
            if (isFocusable() && !isFocused()) {
                requestFocus();
            }

            performClick();
            handled = true;
        }

        // Show the IME, except when selecting in read-only text.
        if ((mMovement != null || onCheckIsTextEditor()) && hasSpannableText() && mLayout != null
                && (isTextEditable() || isTextSelectable()) && isFocused()) {
            final InputMethodManager imm = getInputMethodManager();
            viewClicked(imm);
            if (!isTextSelectable() && mEditor.mShowSoftInputOnFocus && imm != null) {
                handled |= imm.showSoftInput(this, 0);
            }
        }

        return handled;
    }

请注意上面这段代码的关键在于requestFocus()这个方法,EditText获取焦点后弹出IME只和这句话有关,不要被代码中的imm.showSoftInput迷惑了。

requestFocus方法获取焦点变换处理逻辑的代码链十分漫长,这里给一个方法调用链,各位自己去梳理一下:

View#requestFocus()->View#requestFocusNoSearch()->View#handleFocusGainInternal()->View#onFocusChanged()->View#notifyFocusChangeToImeFocusController() ->ImeFocusController#onViewFocusChanged()->ViewRootImpl#dispatchCheckFocus()->ImeFocusController#checkFocus()->InputMethodManagerDelegate#startInput();

通过InputMethodManagerDelegate,最终调用到InputMethodManager#DelegateImplstartInput方法中,代码如下:

java 复制代码
//InputMethodManager.java
 public boolean startInput(@StartInputReason int startInputReason, View focusedView,
                @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
                int windowFlags) {
            ...
            return startInputInner(startInputReason,
                    focusedView != null ? focusedView.getWindowToken() : null, startInputFlags,
                    softInputMode, windowFlags);
        }

继续往下看:

java 复制代码
//InputMethodManager.java
boolean startInputInner(@StartInputReason int startInputReason,
            @Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
            @SoftInputModeFlags int softInputMode, int windowFlags) {
       	...

        if (windowGainingFocus == null) {
            windowGainingFocus = view.getWindowToken();
            if (windowGainingFocus == null) {
                Log.e(TAG, "ABORT input: ServedView must be attached to a Window");
                return false;
            }
			//Attention 1 !!!
            startInputFlags = getStartInputFlags(view, startInputFlags);
            softInputMode = view.getViewRootImpl().mWindowAttributes.softInputMode;
            windowFlags = view.getViewRootImpl().mWindowAttributes.flags;
        }

      	...

        // Okay we are now ready to call into the served view and have it
        // do its stuff.
        // Life is good: let's hook everything up!
        EditorInfo tba = new EditorInfo();
        // Note: Use Context#getOpPackageName() rather than Context#getPackageName() so that the
        // system can verify the consistency between the uid of this process and package name passed
        // from here. See comment of Context#getOpPackageName() for details.
        tba.packageName = view.getContext().getOpPackageName();
        tba.autofillId = view.getAutofillId();
        tba.fieldId = view.getId();
        InputConnection ic = view.onCreateInputConnection(tba);
        if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);

        synchronized (mH) {
            // Now that we are locked again, validate that our state hasn't
            // changed.
            final View servedView = getServedViewLocked();
            if (servedView != view || !mServedConnecting) {
                // Something else happened, so abort.
                if (DEBUG) Log.v(TAG,
                        "Starting input: finished by someone else. view=" + dumpViewInfo(view)
                        + " servedView=" + dumpViewInfo(servedView)
                        + " mServedConnecting=" + mServedConnecting);
                return false;
            }

            // If we already have a text box, then this view is already
            // connected so we want to restart it.
            if (mCurrentTextBoxAttribute == null) {
                startInputFlags |= StartInputFlags.INITIAL_CONNECTION;
            }

            // Hook 'em up and let 'er rip.
            mCurrentTextBoxAttribute = tba;
            maybeCallServedViewChangedLocked(tba);
            mServedConnecting = false;
            if (mServedInputConnectionWrapper != null) {
                mServedInputConnectionWrapper.deactivate();
                mServedInputConnectionWrapper = null;
            }
            ControlledInputConnectionWrapper servedContext;
            final int missingMethodFlags;
            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();
                }
                servedContext = new ControlledInputConnectionWrapper(
                        icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view);
            } else {
                servedContext = null;
                missingMethodFlags = 0;
            }
            mServedInputConnectionWrapper = servedContext;

            try {
                if (DEBUG) Log.v(TAG, "START INPUT: view=" + dumpViewInfo(view) + " ic="
                        + ic + " tba=" + tba + " startInputFlags="
                        + InputMethodDebug.startInputFlagsToString(startInputFlags));
				//Attention 2!!!
                final InputBindResult res = mService.startInputOrWindowGainedFocus(
                        startInputReason, mClient, windowGainingFocus, startInputFlags,
                        softInputMode, windowFlags, tba, servedContext, missingMethodFlags,
                        view.getContext().getApplicationInfo().targetSdkVersion);
             	 ...
            } catch (RemoteException e) {
                Log.w(TAG, "IME died: " + mCurId, e);
            }
        }

        return true;
    }

上面这段代码的内容比较多,但目前重点需要关注两个点,笔者用Attetion 式样的注释予以了标注:

1、 startInputFlags = getStartInputFlags(view, startInputFlags); ,这句话对获取焦点的View的类型做了判断,如果View是一个text editor,则startInputFlags会增加一个StartInputFlags.IS_TEXT_EDITOR的标志,这个标志十分重要,它决定了IME的显示与隐藏的行为。

2、在创建完InputConnection后,调用了IMMS的startInputOrWindowGainedFocus方法,控制IME App的显示或隐藏。

继续追踪下去,看一下IMMS的startInputOrWindowGainedFocusInternalLocked方法:

java 复制代码
private InputBindResult startInputOrWindowGainedFocusInternalLocked(
            @StartInputReason int startInputReason, IInputMethodClient client,
            @NonNull IBinder windowToken, @StartInputFlags int startInputFlags,
            @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo attribute,
            IInputContext inputContext, @MissingMethodFlags int missingMethods,
            int unverifiedTargetSdkVersion, @UserIdInt int userId) {
   
		...

       if (!didStart) {
            if (attribute != null) {
                if (sameWindowFocused) {
                    // On previous platforms, when Dialogs re-gained focus, the Activity behind
                    // would briefly gain focus first, and dismiss the IME.
                    // On R that behavior has been fixed, but unfortunately apps have come
                    // to rely on this behavior to hide the IME when the editor no longer has focus
                    // To maintain compatibility, we are now hiding the IME when we don't have
                    // an editor upon refocusing a window.
                    if (startInputByWinGainedFocus) {
                        hideCurrentInputLocked(mCurFocusedWindow, 0, null,
                                SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
                    }
                    res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
                            startInputFlags, startInputReason);
                } else if (!DebugFlags.FLAG_OPTIMIZE_START_INPUT.value()
                        || (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0) {
					//Attention 1!!!
                    res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
                            startInputFlags, startInputReason);
                } else {
                    res = InputBindResult.NO_EDITOR;
                }
            } else {
                res = InputBindResult.NULL_EDITOR_INFO;
            }
        }
        return res;
    }

上述这个方法,包含了 IME 的显示与隐藏的逻辑判断,根据获取焦点的View的类型,决定是否需要隐藏IME。此外,还包含了softInputMode逻辑的处理。在未指定softInputMode且由EditText类型的控件获取到焦点后,最终会走到Attention 1注释处的代码。 请注意这段代码的判断条件,(startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0) 这段代码是非EditText获取到焦点无法弹出IME的关键逻辑。startInputFlags是怎么指定这个特殊的flag的?不记得的读者可以往回稍微翻一下,前文有所说明。

java 复制代码
//InputMethodManagerService.java
 InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,
            @MissingMethodFlags int missingMethods, @NonNull EditorInfo attribute,
            @StartInputFlags int startInputFlags, @StartInputReason int startInputReason) {
       	...

        mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE);
        mCurIntent.setComponent(info.getComponent());
        mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
                com.android.internal.R.string.input_method_binding_label);
        mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
                mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
                PendingIntent.FLAG_IMMUTABLE));

        if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
            mLastBindTime = SystemClock.uptimeMillis();
            mHaveConnection = true;
            mCurId = info.getId();
            mCurToken = new Binder();
            mCurTokenDisplayId = displayIdToShowIme;
            try {
                if (DEBUG) {
                    Slog.v(TAG, "Adding window token: " + mCurToken + " for display: "
                            + mCurTokenDisplayId);
                }
                mIWindowManager.addWindowToken(mCurToken, LayoutParams.TYPE_INPUT_METHOD,
                        mCurTokenDisplayId);
            } catch (RemoteException e) {
            }
            return new InputBindResult(
                    InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
                    null, null, mCurId, mCurSeq, null);
        }
        mCurIntent = null;
        Slog.w(TAG, "Failure connecting to input method service: " + mCurIntent);
        return InputBindResult.IME_NOT_CONNECTED;
    }

上述代码将IMMS 与另一个远程服务建立了绑定关系,Intent的具体参数如下:

  • Action: android.view.InputMethod
  • Component:默认输入法App的Component信息,它是动态查询的;当系统存在多个输入法App时,由Settings.Secure.DEFAULT_INPUT_METHOD属性决定默认的输入法。
  • Extra: EXTRA_CLIENT_LABEL,一个label,携带的信息是不同语言下的"输入法"这个词语,不太重要。
  • Extra: EXTRA_CLIENT_INTENT,输入法设置页面Activity路径,一般配置在系统的Settings App中。

通过这一部分代码,IMMS与系统默认的IME App最终建立了绑定关系,实现了远端相互通讯。

bindService成功后,再来看看onServiceConnected的回调:

java 复制代码
//InputMethodManagerService.java
public void onServiceConnected(ComponentName name, IBinder service) {
        synchronized (mMethodMap) {
            if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
              ...
                if (mCurClient != null) {
                    clearClientSessionLocked(mCurClient);
					//Attention Here!!!
                    requestClientSessionLocked(mCurClient);
                }
            }
        }
    }

关键语句在于requestClientSessionLocked(mCurClient);,它的作用是创建一个IM客户端Session------IInputMethodSession,用于IMMS与IME的具体事件通讯,如软键盘字符按下的事件、隐藏软键盘等。

java 复制代码
//InputMethodManagerService.java
 void requestClientSessionLocked(ClientState cs) {
        if (!cs.sessionRequested) {
            if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs);
            InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString());
            cs.sessionRequested = true;
            executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO(
                    MSG_CREATE_SESSION, mCurMethod, channels[1],
                    new MethodCallback(this, mCurMethod, channels[0])));
        }
    }

请注意上述代码的MethodCallback这个类,从命名上可以看出,它是一个回调接口。事实上,当IMMS成功创建IInputMethodSession并将它注册到服务端后,MethodCallback就会收到Session创建成功的回调,回调代码如下:

java 复制代码
//InputMethodManagerService.java
  public void sessionCreated(IInputMethodSession session) {
            long ident = Binder.clearCallingIdentity();
            try {
                mParentIMMS.onSessionCreated(mMethod, session, mChannel);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

它调用了IMMS类中的onSessionCreated方法:

java 复制代码
//InputMethodManagerService.java
void onSessionCreated(IInputMethod method, IInputMethodSession session,
            InputChannel channel) {
        synchronized (mMethodMap) {
            if (mUserSwitchHandlerTask != null) {
                // We have a pending user-switching task so it's better to just ignore this session.
                channel.dispose();
                return;
            }
            if (mCurMethod != null && method != null
                    && mCurMethod.asBinder() == method.asBinder()) {
                if (mCurClient != null) {
                    clearClientSessionLocked(mCurClient);
                    mCurClient.curSession = new SessionState(mCurClient,
                            method, session, channel);
					//Attention Here!!
                    InputBindResult res = attachNewInputLocked(
                            StartInputReason.SESSION_CREATED_BY_IME, true);
                    if (res.method != null) {
                        executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO(
                                MSG_BIND_CLIENT, mCurClient.client, res));
                    }
                    return;
                }
            }
        }

        // Session abandoned.  Close its associated input channel.
        channel.dispose();
    }

核心代码在attachNewInputLocked这个调用中:

java 复制代码
//InputMethodManagerService.java
InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) {
        if (!mBoundToMethod) {
        ...
		//Attention Here!!!
        executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(
                MSG_START_INPUT, mCurInputContextMissingMethods, initial ? 0 : 1 /* restarting */,
                startInputToken, session, mCurInputContext, mCurAttribute));
        if (mShowRequested) {
            if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
            showCurrentInputLocked(mCurFocusedWindow, getAppShowFlags(), null,
                    SoftInputShowHideReason.ATTACH_NEW_INPUT);
        }
        return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
                session.session, (session.channel != null ? session.channel.dup() : null),
                mCurId, mCurSeq, mCurActivityViewToScreenMatrix);
    }

它向Handler发送了一条MSG_START_INPUT的消息:

java 复制代码
//InputMethodManagerService.java
   case MSG_START_INPUT: {
                final int missingMethods = msg.arg1;
                final boolean restarting = msg.arg2 != 0;
                args = (SomeArgs) msg.obj;
                final IBinder startInputToken = (IBinder) args.arg1;
                final SessionState session = (SessionState) args.arg2;
                final IInputContext inputContext = (IInputContext) args.arg3;
                final EditorInfo editorInfo = (EditorInfo) args.arg4;
                try {
                    setEnabledSessionInMainThread(session);
					//Attention Here!!!
                    session.method.startInput(startInputToken, inputContext, missingMethods,
                            editorInfo, restarting, session.client.shouldPreRenderIme);
                } catch (RemoteException e) {
                }
                args.recycle();
                return true;
            }

session.method.startInput这句话开始,中间会经历IInputMethodWrapper#startInput->InputMethod#dispatchStartInputWithToken->InputManagerService#startInput的调用链,最终调用到IMMS bind服务端------InputMethodService的doStartInput方法中:

java 复制代码
//InputMethodService.java
 void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) {
     	...
        if (mDecorViewVisible) {
           ...
        } else if (mCanPreRender && mInputEditorInfo != null && mStartedInputConnection != null) {
            // Pre-render IME views and window when real EditorInfo is available.
            // pre-render IME window and keep it invisible.
            if (DEBUG) Log.v(TAG, "Pre-Render IME for " + mInputEditorInfo.fieldName);
            if (mInShowWindow) {
                Log.w(TAG, "Re-entrance in to showWindow");
                return;
            }

            mDecorViewWasVisible = mDecorViewVisible;
            mInShowWindow = true;
            startViews(prepareWindow(true /* showInput */));

            // compute visibility
            mIsPreRendered = true;
            onPreRenderedWindowVisibilityChanged(false /* setVisible */);

            // request draw for the IME surface.
            // When IME is not pre-rendered, this will actually show the IME.
            if (DEBUG) Log.v(TAG, "showWindow: draw decorView!");
			//Attention Here!! 
            mWindow.show();
            maybeNotifyPreRendered();
            mDecorViewWasVisible = true;
            mInShowWindow = false;
        } else {
            mIsPreRendered = false;
        }
    }

看到代码中的mWindow.show()这句话了吗,这句话的调用标识着IME软键盘的UI界面已经成功展示在屏幕上,至此,已完成了 点击输入框到调出软键盘的整个流程。

下一篇文章,我们将从IME的视角入手,探究一下IME将中文显示在EditText中的工作原理。

相关推荐
踏雪羽翼6 小时前
android TextView实现文字字符不同方向显示
android·自定义view·textview方向·文字方向·textview文字显示方向·文字旋转·textview文字旋转
lxysbly6 小时前
安卓玩MRP冒泡游戏:模拟器下载与使用方法
android·游戏
夏沫琅琊9 小时前
Android 各类日志全面解析(含特点、分析方法、实战案例)
android
程序员JerrySUN9 小时前
OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录
android·java·开发语言·redis·yolo·架构
TeleostNaCl10 小时前
Android | 启用 TextView 跑马灯效果的方法
android·经验分享·android runtime
TheNextByte111 小时前
Android USB文件传输无法使用?5种解决方法
android
quanyechacsdn12 小时前
Android Studio创建库文件用jitpack构建后使用implementation方式引用
android·ide·kotlin·android studio·implementation·android 库文件·使用jitpack
程序员陆业聪13 小时前
聊聊2026年Android开发会是什么样
android
编程大师哥13 小时前
Android分层
android
极客小云15 小时前
【深入理解 Android 中的 build.gradle 文件】
android·安卓·安全架构·安全性测试