android輸入法機制的學習總結

android輸入法機制包含三部分:java

  1. 輸入法服務(InputMethodService),簡稱IMS;
  2. 輸入法系統服務(InputMethodManagerService),簡稱IMMS;
  3. 客戶端app(即當前要輸入內容的app);

android中的四大組件,其中常常用的包含Activity和Service。它們就像是系統和app通訊的接口同樣。經過Activity中能夠展現UI,業務處理等。Service也一樣能夠作到。輸入法就是靠Service來展現UI,業務處理的。android

拋出幾個問題

  1. 輸入法Service是如何啓動的呢?
  2. 輸入法Service是如何展現UI(鍵盤)的呢?
  3. 第三方app如何向輸入法service發信息的呢?好比發起彈鍵盤的請求。
  4. 該Service如何向第三方app發信息的呢?好比把按鍵信息傳給第三方app的EditText。

總體歸納

輸入法服務的啓動以及和第三方app的關係的搭建,離不開IMMS(InputMethodManagerService)和其它系統服務。而啓動、關係搭建、通訊過程,離不開binder。bash

先把圖奉上session

圖裏邊用紅色數字標註的地方是用binder進行通訊的,而藍色標註的A,B處也是用binder通訊,藍色部分標註的是和普通app的按鍵觸屏通訊機制同樣,在這裏先不分析了。app

首先簡潔的一筆帶過IMMS的啓動,它的啓動是在SystemServer運行起來時,會調用代碼startOtherServices,代碼以下。ide

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

在startOtherServices中,有這段代碼oop

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

這裏就是啓動了InputMethodManagerService。具體代碼在SystemServer.java中。而SystemServer屬於什麼進程?被誰啓動?在這裏不分析。ui

輸入法Service的啓動

那麼輸入法服務是被誰啓動的呢?沒錯,是它是它就是它,咱們的朋友IMMS。摘一段IMMS的代碼(來自於InputMethodManagerService.java),刪去了一些代碼,簡化以下。this

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,先不考慮。只須要簡要知道,這個方法啓動了輸入法服務。spa

這個方法是被誰調用的呢?它是被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
相關文章
相關標籤/搜索