当用户在Android系统的输入框轻点,就会弹出预设的输入法软件,点击软件上的字符,能够拼出中文字词,并填入到输入框中。在这个简单的场景中,Android系统究竟又做了哪些复杂的工作,才能让整套流程运转起来呢?这一篇文章将从用户点击输入框的这一个动作开始,一步步逐渐深入揭示InputMethod背后的工作原理。
Question:当按下输入框的那一刻,究竟发生了什么?
现在从事件的源头开始,来看看TextView
的performAccessibilityActionClick
方法:
//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#DelegateImpl
的startInput
方法中,代码如下:
//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开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)