android輸入法機制包含三部分:java
android中的四大組件,其中常常用的包含Activity和Service。它們就像是系統和app通訊的接口同樣。經過Activity中能夠展現UI,業務處理等。Service也一樣能夠作到。輸入法就是靠Service來展現UI,業務處理的。android
輸入法服務的啓動以及和第三方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
那麼輸入法服務是被誰啓動的呢?沒錯,是它是它就是它,咱們的朋友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是第三方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方法會在編輯框獲取焦點時被調用。
先上個圖,展現輸入法服務的類繼承關係。
在去了解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能夠控制輸入法彈出和收起等。咱們都知道進程間通訊靠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運行起來後,會持有一個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在調用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);
}
複製代碼
如今知道了:
app向IMMS發起可輸入請求,是靠調用IInputMethodManager的startInputOrWindowGainedFocus,IMMS收到消息後,向輸入法service發起請求,是靠調用IInputMethod的startInput。
注意!注意!這兩個aidl的startInput都有個參數IInputContext,它也是個binder。這個參數最終傳給了輸入法Service。具體代碼以下。
第三方app端,InputMethodManager的方法startInputInner中有一段代碼是
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;
}
複製代碼
總結一下。
IInputContext.aidl是:
oneway interface IInputContext {
void deleteSurroundingText(int leftLength, int rightLength);
void commitText(CharSequence text, int newCursorPosition);
void commitCompletion(in CompletionInfo completion);
// ...
}
複製代碼
我先把類列出來
這些類的關係是什麼呢,因而根據代碼畫了一張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定義的。
IInputConnectionWrapper相關的類的關係以下
在客戶端的InputMethodManager的startInputInner方法中,建立了ControlledInputConnectionWrapper對象,並把它做爲參數調用IInputMethodManager的startInputOrWindowGainedFocus。IInputMethodManager正是IMMS對應的binder,這樣就經過IMMS把ControlledInputConnectionWrapper對應的binder傳遞給了IMS端。
不難想象,這裏的ControlledInputConnectionWrapper所對應的service端的正是InputConnectionWrapper裏邊的IInputContext。
InputMethodManagerService
IMS端和客戶端共用:InputConnection