android输入法机制的学习总结

回主页

android输入法机制包含三部分:
1. 输入法服务(InputMethodService),简称IMS;
2. 输入法系统服务(InputMethodManagerService),简称IMMS;
3. 客户端app(即当前要输入内容的app);

android中的四大组件,其中经常用的包含Activity和Service。它们就像是系统和app通信的接口一样。通过Activity中可以展示UI,业务处理等。Service也同样可以做到。输入法就是靠Service来展示UI,业务处理的。

抛出几个问题

  1. 输入法Service是如何启动的呢?
  2. 输入法Service是如何展示UI(键盘)的呢?
  3. 第三方app如何向输入法service发信息的呢?比如发起弹键盘的请求。
  4. 该Service如何向第三方app发信息的呢?比如把按键信息传给第三方app的EditText。

整体概括

输入法服务的启动以及和第三方app的关系的搭建,离不开IMMS(InputMethodManagerService)和其它系统服务。而启动、关系搭建、通信过程,离不开binder。

先把图奉上

图里边用红色数字标注的地方是用binder进行通信的,而蓝色标注的A,B处也是用binder通信,蓝色部分标注的是和普通app的按键触屏通信机制一样,在这里先不分析了。

首先简洁的一笔带过IMMS的启动,它的启动是在SystemServer运行起来时,会调用代码startOtherServices,代码如下。

        // Start services.
        try {
            traceBeginAndSlog("StartServices");
            startBootstrapServices();
            startCoreServices();
            startOtherServices();
            SystemServerInitThreadPool.shutdown();
        } catch (Throwable ex) {
            throw ex;
        } finally {
            traceEnd();
        }

在startOtherServices中,有这段代码

        // Bring up services needed for UI.
        if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
            mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);
            // ... ...
        }

这里就是启动了InputMethodManagerService。具体代码在SystemServer.java中。而SystemServer属于什么进程?被谁启动?在这里不分析。

输入法Service的启动

那么输入法服务是被谁启动的呢?没错,是它是它就是它,我们的朋友IMMS。摘一段IMMS的代码(来自于InputMethodManagerService.java),删去了一些代码,简化如下。

    InputBindResult startInputInnerLocked() {
        InputMethodInfo info = mMethodMap.get(mCurMethodId);
        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), 0));
        if (bindCurrentInputMethodService(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
            mCurToken = new Binder();
            mIWindowManager.addWindowToken(mCurToken, TYPE_INPUT_METHOD, DEFAULT_DISPLAY);
            return new InputBindResult(null, null, mCurId, mCurSeq,
                    mCurUserActionNotificationSequenceNumber);
        } else {
            mCurIntent = null;
        }
        return null;
    }

通过代码我们看到了这个方法里,绑定了当前输入法服务(bindCurrentInputMethodService)。为什么当前输入法呢?看代码这里InputMethodInfo info = mMethodMap.get(mCurMethodId)。map这里存储了百度、搜狗、讯飞、KK等一系列输入法的信息。至于mIWindowManager.addWindowToken和InputBindResult,先不考虑。只需要简要知道,这个方法启动了输入法服务。

这个方法是被谁调用的呢?它是被IMMS中的startInputOrWindowGainedFocus方法调用,而startInputOrWindowGainedFocus是被第三方app请求弹起输入法时通过binder机制调用。(startInputOrWindowGainedFocus是在IInputMethodManager.aidl声明的)。

第三方app请求弹起输入法时是如何通过binder机制调用到这里startInputOrWindowGainedFocus的?这里要看第三方app进程中的InputMethodManager。

InputMethodManager

InputMethodManager是第三方app所在进程的一个对象,它有startInputInner这个方法,方法内部有一段这样的代码

try {
 		// ...
		final InputBindResult res = mService.startInputOrWindowGainedFocus(
                        startInputReason, mClient, windowGainingFocus, 							 controlFlags, softInputMode,
                        windowFlags, tba, servedContext,missingMethodFlags);
} catch (RemoteException e) {
        // ...
}

注意,注意!第三方app这里也调用了startInputOrWindowGainedFocus方法,它和IMMS中的startInputOrWindowGainedFocus是通过binder机制通信的。

InputMethodManager的startInputInner方法会在编辑框获取焦点时被调用。

输入法服务如何展现UI键盘

先上个图,展示输入法服务的类继承关系。

在去了解AbstractInputMethodService、InputMethodService前,先抛出一个认知。

通常我们要展示一个界面时,除了用activity之外,我们也可以获取WindowManager,然后调用它的addView。也可以通过popupwindow、dialog展示界面。

输入法的界面就是靠最后一种方式(dialog)展示出来的,而调用dialog的地方,必然是在AbstractInputMethodService、InputMethodService这2个类中某一个地方。

然后,我们去分析一下InputMethodService。发现该类中有个私有字段

SoftInputWindow mWindow;

而这个SoftInputWindow正是继承了Dialog。那么说,输入法的UI所需要的view,必然是添加到mWindow中,然后靠mWindow的show方法来显示界面。照这个思路去分析相应的代码。

在InputMethodService重写的onCreate方法中,创建了SoftInputWindow实例,该实例赋值给mWindow,然后调用方法initViews()。在initViews方法中,创建了一个mRootView,然后把该mRootView作为参数传入到mWindow的setContentView方法中。

InputMethodService提供了一个方法public View onCreateInputView(),该方法返回的view是挂接到mRootView的树结构的某一个节点上的,然后我们就可以继承InputMethodService来实现这个onCreateInputView(),这样就可以自定义键盘的外观了。

回头总结下,原来输入法service中有个mWindow,类型是继承了Dialog的SoftInputWindow,它的contentView是mRootView,然后输入法的view是通过onCreateInputView()方法创建出来后,挂接到mRootView中的。

而什么时候调用mWindow的show方法呢?我们看到InputMethodService有个方法showWindowInner,在这个方法尾部调用了mWindow.show()。那么showWindowInner是干什么的?谁调用了它?看名字就知道它是要弹起输入法,一定是弹起输入法时,某个系统回调中调用了它。

我把showWindowInner方法的代码展示出来入下。

    void showWindowInner(boolean showInput) {
        boolean doShowInput = false;
        final int previousImeWindowStatus =
                (mWindowVisible ? IME_ACTIVE : 0) | (isInputViewShown() ? IME_VISIBLE : 0);
        mWindowVisible = true;
        if (!mShowInputRequested && mInputStarted && showInput) {
            doShowInput = true;
            mShowInputRequested = true;
        }

        if (DEBUG) Log.v(TAG, "showWindow: updating UI");
        initialize();
        updateFullscreenMode();
        updateInputViewShown();
        
        if (!mWindowAdded || !mWindowCreated) {
            mWindowAdded = true;
            mWindowCreated = true;
            initialize();
            if (DEBUG) Log.v(TAG, "CALL: onCreateCandidatesView");
            View v = onCreateCandidatesView();
            if (DEBUG) Log.v(TAG, "showWindow: candidates=" + v);
            if (v != null) {
                setCandidatesView(v);
            }
        }
        if (mShowInputRequested) {
            if (!mInputViewStarted) {
                if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
                mInputViewStarted = true;
                onStartInputView(mInputEditorInfo, false);
            }
        } else if (!mCandidatesViewStarted) {
            if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");
            mCandidatesViewStarted = true;
            onStartCandidatesView(mInputEditorInfo, false);
        }
        
        if (doShowInput) {
            startExtractingText(false);
        }

        final int nextImeWindowStatus = IME_ACTIVE | (isInputViewShown() ? IME_VISIBLE : 0);
        if (previousImeWindowStatus != nextImeWindowStatus) {
            mImm.setImeWindowStatus(mToken, mStartInputToken, nextImeWindowStatus,
                    mBackDisposition);
        }
        if ((previousImeWindowStatus & IME_ACTIVE) == 0) {
            if (DEBUG) Log.v(TAG, "showWindow: showing!");
            onWindowShown();
            mWindow.show();
            // Put here rather than in onWindowShown() in case people forget to call
            // super.onWindowShown().
            mShouldClearInsetOfPreviousIme = false;
        }
    

这里说的净是InputMethodService内部的东西,AbstractInputMethodService到底干了什么?以后会分析。

第三方app和输入法service如何通信

第三方app可以控制输入法弹出和收起等。我们都知道进程间通信靠binder。这里也不例外。but!!!还记得这个输入法Service是在IMMS中bind的吧(不记得了就查IMMS中的这个方法bindCurrentInputMethodService),而不是在第三方app中bind的。所以这个输入法service所对应的binder对象应该是存在于IMMS,而现在是要第三方app通过持有的binder向输入法service通信,该怎么办?

最初始的binder关联是这样的。

紧接着第三方app通过IMMS和IMS进行通信,于是就变成了这样。

这样就可以实现,客户端app去通知输入法Service弹出键盘,在通知时,把自己的一个binder作为参数最终传给了IMS,于是就变成了这样。IMS可以通过这个binder向客户端app通信。

此时大家会发出质疑,“是这样吗?没代码你说个河蟹啊。上代码!”

代码分析

第三方app持有IMMS的binder

第三方app是如何持有IMMS的binder的呢?第三方app运行起来后,会持有一个InputMethodManager对象(简称IMM),平时调用context.getSystemService(Context.INPUT_METHOD_SERVICE)就是获取的这个IMM,IMM的构造方式是

    InputMethodManager(Looper looper) throws ServiceNotFoundException {
        this(IInputMethodManager.Stub.asInterface(
                ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)), looper);
    }

这个IInputMethodManager对应的就是InputMethodManagerService,我们看IMMS的类的定义

public class InputMethodManagerService extends IInputMethodManager.Stub
        implements ServiceConnection, Handler.Callback {
        ...

IInputMethodManager对应的aidl文件是IInputMethodManager.aidl:

interface IInputMethodManager {
    void addClient(in IInputMethodClient client,
            in IInputContext inputContext, int uid, int pid);
    boolean showSoftInput(in IInputMethodClient client, int flags,
            in ResultReceiver resultReceiver);
    boolean hideSoftInput(in IInputMethodClient client, int flags,
            in ResultReceiver resultReceiver);
    InputBindResult startInputOrWindowGainedFocus(int startInputReason,
            in IInputMethodClient client, in IBinder windowToken, int controlFlags,int softInputMode,int windowFlags, in EditorInfo attribute, IInputContext inputContext,int missingMethodFlags,int unverifiedTargetSdkVersion);
    // ...
}

IMMS持有输入法Service的binder

IMMS在调用bindCurrentInputMethodService时,传入了一个ServiceConnection(其实就是IMMS自己实现了这个接口)。

在onServiceConnected方法中获取到类型是IInputMethod的binder,并赋值给mCurMethod。

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        synchronized (mMethodMap) {
            if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
                mCurMethod = IInputMethod.Stub.asInterface(service);
                if (mCurToken == null) {
                    Slog.w(TAG, "Service connected without a token!");
                    unbindCurrentMethodLocked(false);
                    return;
                }
                executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
                        MSG_ATTACH_TOKEN, mCurMethod, mCurToken));
                if (mCurClient != null) {
                    clearClientSessionLocked(mCurClient);
                    requestClientSessionLocked(mCurClient);
                }
            }
        }
    }

在输入法Service这一端,上边提到到AbstractInputMethodService开始发挥自己的功能了,它实现了onBind方法,返回IInputMethodWrapper。IInputMethodWrapper继承了一个Stub,实现了IInputMethod接口。

    @Override
    final public IBinder onBind(Intent intent) {
        if (mInputMethod == null) {
            mInputMethod = onCreateInputMethodInterface();
        }
        return new IInputMethodWrapper(this, mInputMethod);
    }

IInputMethod.aidl如下所示。

oneway interface IInputMethod {
    void attachToken(IBinder token);
    void bindInput(in InputBinding binding);
    void unbindInput();
    void startInput(in IBinder startInputToken, in IInputContext inputContext, int missingMethods,
            in EditorInfo attribute, boolean restarting);
    void createSession(in InputChannel channel, IInputSessionCallback callback);
    void setSessionEnabled(IInputMethodSession session, boolean enabled);
    void revokeSession(IInputMethodSession session);
    void showSoftInput(int flags, in ResultReceiver resultReceiver);
    void hideSoftInput(int flags, in ResultReceiver resultReceiver);
    void changeInputMethodSubtype(in InputMethodSubtype subtype);
}

现在知道了: 1. 第三方app持有IMMS的binder IInputMethodManager,这个IInputMethodManager实现类就是IMMS(InputMethodManagerService); 2. IMMS持有输入法Service的binder IInputMethod,这个IInputMethod的实现类是IInputMethodWrapper;

app和输入法关联

app向IMMS发起可输入请求,是靠调用IInputMethodManager的startInputOrWindowGainedFocus,IMMS收到消息后,向输入法service发起请求,是靠调用IInputMethod的startInput。

注意!注意!这两个aidl的startInput都有个参数IInputContext,它也是个binder。这个参数最终传给了输入法Service。具体代码如下。

第三方app端,InputMethodManager的方法startInputInner中有一段代码是

这个ControlledInputConnectionWrapper既是IInputContext的binder。

IMMS收到消息后如何发送消息startInput给IMS的,IMMS这里边逻辑太多,这里不再细说。

IMS端IInputMethodWrapper.java中,类型是IInputContext的inputContext作为参数传给了InputConnectionWrapper,InputConnectionWrapper对象赋值给了InputConnection。

case DO_START_INPUT: {
    final SomeArgs args = (SomeArgs) msg.obj;
    final int missingMethods = msg.arg1;
    final boolean restarting = msg.arg2 != 0;
    final IBinder startInputToken = (IBinder) args.arg1;
    final IInputContext inputContext = (IInputContext) args.arg2;
    final EditorInfo info = (EditorInfo) args.arg3;
    final InputConnection ic = inputContext != null
            ? new InputConnectionWrapper(mTarget, inputContext, missingMethods) : null;
    info.makeCompatible(mTargetSdkVersion);
    inputMethod.dispatchStartInputWithToken(ic, info, restarting /* restarting */,
            startInputToken);
    args.recycle();
    return;
            }

总结一下。 1. 客户端app通过IMMS,把IInputContext这个binder传递给了IMS,这个过程都使用了binder机制。 2. IMS端靠这个IInputContext向客户端app发送指令(文字、符号等)。

IInputContext.aidl是:

oneway interface IInputContext {
    void deleteSurroundingText(int leftLength, int rightLength);
    void commitText(CharSequence text, int newCursorPosition);
    void commitCompletion(in CompletionInfo completion);
    // ...
}




草稿

IMS一端

类的关系

我先把类列出来 1. InputMethodService; 2. IInputMethodWrapper; 3. InputMethod <|-- AbstractInputMethodImpl <|-- InputMethodImpl; 4. InputConnectionWrapper(它实现了InputConnection接口); 5. InputContextCallback;

这些类的关系是什么呢,于是根据代码画了一张UML图

概括说,service通过onBind()返回一个继承了Stub的IInputMethodWrapper对象,IInputMethodWrapper内部弱引用了一个InputMethodImpl对象。那么InputConnectionWrapper对象是如何被最终传递给service的呢。于是我画了一个时序图,如下。

远程IPC调用IInputMethodWrapper的startInput方法,把IInputContext引用的对象传递过来,通过时序图可以看到InputMethodService是如何得到InputConnectionWrapper对象的,从而间接地可以得到IInputContext引用的对象(InputConnectionWrapper内聚了IInputContext)。这就赋予了servie远程和第三方app客户端通信的能力。这个IInputContext提供了什么接口,service就可以和客户端做什么通信。比如它有个void commitText(CharSequence text, int newCursorPosition);接口,可以使得service提交文字到客户端相应的EditText中。

注意,这里的IInputMethodWrapper是继承了IInputMethod.Stub,所以它 is-a Binder,实现了aidl定义的接口IInputMethod,而不是InputMethod。 这个容易迷惑人,InputMethod不是aidl定义的。

客户端

  1. IInputConnectionWrapper;
  2. ControlledInputConnectionWrapper;

IInputConnectionWrapper相关的类的关系如下

在客户端的InputMethodManager的startInputInner方法中,创建了ControlledInputConnectionWrapper对象,并把它作为参数调用IInputMethodManager的startInputOrWindowGainedFocus。IInputMethodManager正是IMMS对应的binder,这样就通过IMMS把ControlledInputConnectionWrapper对应的binder传递给了IMS端。

不难想象,这里的ControlledInputConnectionWrapper所对应的service端的正是InputConnectionWrapper里边的IInputContext。

系统服务端

InputMethodManagerService

IMS端和客户端共用:InputConnection

需要用到的aidl

  1. IInputContextCallback.aidl
  2. IInputContext.aidl
  3. InputBinding.aidl
  4. IInputMethod.aidl
  5. IInputMethodManager.aidl
  6. IInputMethodClient.aidl
回主页