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

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

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

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

//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方法中,代码如下:

//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);
        }

继续往下看:

//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方法:

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的?不记得的读者可以往回稍微翻一下,前文有所说明。

//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的回调:

//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的具体事件通讯,如软键盘字符按下的事件、隐藏软键盘等。

//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创建成功的回调,回调代码如下:

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

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

//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这个调用中:

//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的消息:

//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方法中:

//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界面已经成功展示在屏幕上,至此,已完成了 点击输入框到调出软键盘的整个流程。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

猜你喜欢

转载自blog.csdn.net/weixin_43440181/article/details/134700603
今日推荐