android输入法架构解析
android輸入法架構(gòu)解析
簡介:
前陣子接手維護(hù)了一個密碼鍵盤的項(xiàng)目,之前還沒有接觸過android輸入法這塊的知識點(diǎn),所以在熟悉項(xiàng)目的同時將android系統(tǒng)輸入法實(shí)現(xiàn)框架整理了一遍,記錄在此.整個輸入法架構(gòu)可以簡單劃分為主要三塊:
1.android輸入法管理服務(wù)InputMethodManagerService(IMMS)
2.android輸入法管理InputMethodManager(IMM)與當(dāng)前輸入控件(EditText)
3.輸入法IME
簡要示意圖如下:
幾個主要類關(guān)系圖如下:
以下將按照如下流程分別進(jìn)行介紹:
1.輸入法管理服務(wù)InputMethodManagerService(IMMS)簡介
2.輸入法管理InputMethodManager(IMM)簡介
3.輸入法服務(wù)InputMethodService簡介
4.從edittext點(diǎn)擊到輸入法界面顯示過程
5.輸入法字符傳遞到edittext過程
1.輸入法管理服務(wù)InputMethodManagerService簡介
IMMS主要管理輸入法,通過接收IMM的請求拉起或者隱藏輸入法,保持輸入法與IMM的連接,系統(tǒng)服務(wù)的啟動在systemserver中,輸入法管理是在startOtherServices中:
if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);......}進(jìn)入到InputMethodManagerService中主要進(jìn)行初始化操作,比如當(dāng)前安裝的輸入法列表,設(shè)置默認(rèn)輸入法,運(yùn)行時序圖如下:
這里獲取默認(rèn)輸入法列表和設(shè)置默認(rèn)輸入法的操作都在初始化方法中完成:
public InputMethodManagerService(Context context) {mIPackageManager = AppGlobals.getPackageManager();mContext = context;mRes = context.getResources();mHandler = new Handler(this);// 這里后續(xù)會用來注冊幾個數(shù)據(jù)字段的監(jiān)聽mSettingsObserver = new SettingsObserver(mHandler);mIWindowManager = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE));mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);mCaller = new HandlerCaller(context, null, new HandlerCaller.Callback() {@Overridepublic void executeMessage(Message msg) {handleMessage(msg);}}, true /*asyncHandler*/);mAppOpsManager = mContext.getSystemService(AppOpsManager.class);mUserManager = mContext.getSystemService(UserManager.class);mHardKeyboardListener = new HardKeyboardListener();mHasFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_INPUT_METHODS);//獲取默認(rèn)的系統(tǒng)設(shè)置mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);mHardKeyboardBehavior = mContext.getResources().getInteger(com.android.internal.R.integer.config_externalHardKeyboardBehavior);//初始化通知Bundle extras = new Bundle();extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);mImeSwitcherNotification = new Notification.Builder(mContext).setSmallIcon(com.android.internal.R.drawable.ic_notification_ime_default).setWhen(0).setOngoing(true).addExtras(extras).setCategory(Notification.CATEGORY_SYSTEM).setColor(com.android.internal.R.color.system_notification_accent_color);Intent intent = new Intent(Settings.ACTION_SHOW_INPUT_METHOD_PICKER);mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);mShowOngoingImeSwitcherForPhones = false;//注冊用戶添加移除,從備份中恢復(fù)系統(tǒng)設(shè)置廣播final IntentFilter broadcastFilter = new IntentFilter();broadcastFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);broadcastFilter.addAction(Intent.ACTION_USER_ADDED);broadcastFilter.addAction(Intent.ACTION_USER_REMOVED);broadcastFilter.addAction(Intent.ACTION_SETTING_RESTORED);mContext.registerReceiver(new ImmsBroadcastReceiver(), broadcastFilter);mNotificationShown = false;int userId = 0;try {userId = ActivityManagerNative.getDefault().getCurrentUser().id;} catch (RemoteException e) {Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e);}//注冊app相關(guān)操作廣播,包括安裝卸載清除數(shù)據(jù)改變停止等mMyPackageMonitor.register(mContext, null, UserHandle.ALL, true);//初始化mSettings,后續(xù)用來存放和獲取一些InputMethod設(shè)置相關(guān)內(nèi)容mSettings = new InputMethodSettings(mRes, context.getContentResolver(), mMethodMap, mMethodList, userId, !mSystemReady);//將從usermanager獲取的ProfileIds設(shè)置到mSettings中updateCurrentProfileIds();//InputMethodFileManager用來緩存用戶相關(guān)狀態(tài)mFileManager = new InputMethodFileManager(mMethodMap, userId);synchronized (mMethodMap) {mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(mSettings, context);}//獲取系統(tǒng)默認(rèn)設(shè)置輸入法名稱final String defaultImiId = mSettings.getSelectedInputMethod();if (DEBUG) {Slog.d(TAG, "Initial default ime = " + defaultImiId);}mImeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId);synchronized (mMethodMap) {//解析所有安裝的輸入法,將其轉(zhuǎn)換為InputMethodInfo添加到列表mMethodList和mMethodMap中,并且處理默認(rèn)輸入法相關(guān),比如設(shè)置和重設(shè)新的默認(rèn)輸入法buildInputMethodListLocked(!mImeSelectedOnBoot /* resetDefaultEnabledIme */);}//首次啟動會將所有的輸入法id以":"隔離開組成字符串寫入到系統(tǒng)數(shù)據(jù)庫ENABLED_INPUT_METHODS字段中mSettings.enableAllIMEsIfThereIsNoEnabledIME();if (!mImeSelectedOnBoot) {Slog.w(TAG, "No IME selected. Choose the most applicable IME.");synchronized (mMethodMap) {//如果還沒有設(shè)置默認(rèn)輸入法就去設(shè)置,將獲取到的可用輸入法列表的第一個設(shè)置為默認(rèn)輸入法resetDefaultImeLocked(context);}}synchronized (mMethodMap) {//注冊系統(tǒng)數(shù)據(jù)庫中部分跟輸入法相關(guān)字段改動的監(jiān)聽mSettingsObserver.registerContentObserverLocked(userId);//使能當(dāng)前安裝的所有輸入法,設(shè)置默認(rèn)輸入法,發(fā)送輸入法變化通知,設(shè)置虛擬鍵盤是否要與實(shí)體鍵盤共存updateFromSettingsLocked(true);}//監(jiān)聽ACTION_LOCALE_CHANGED廣播以便能夠在語言區(qū)域發(fā)生改變時候能夠及時對默認(rèn)輸入法做出調(diào)整final IntentFilter filter = new IntentFilter();filter.addAction(Intent.ACTION_LOCALE_CHANGED);mContext.registerReceiver(new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {synchronized(mMethodMap) {resetStateIfCurrentLocaleChangedLocked();}}}, filter);}這里可以看到,在初始化方法中進(jìn)行的操作,可以概括為如下幾點(diǎn):
初始化完成后,會調(diào)用onBootPhase方法:
@Overridepublic void onBootPhase(int phase) {// Called on ActivityManager thread.// TODO: Dispatch this to a worker thread as needed.if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {StatusBarManagerService statusBarService = (StatusBarManagerService) ServiceManager.getService(Context.STATUS_BAR_SERVICE);mService.systemRunning(statusBarService);}}這里詳細(xì)講下systemRunning:
public void systemRunning(StatusBarManagerService statusBar) {synchronized (mMethodMap) {if (DEBUG) {Slog.d(TAG, "--- systemReady");}if (!mSystemReady) {mSystemReady = true;final int currentUserId = mSettings.getCurrentUserId();mSettings.switchCurrentUser(currentUserId,!mUserManager.isUserUnlockingOrUnlocked(currentUserId));mKeyguardManager = mContext.getSystemService(KeyguardManager.class);mNotificationManager = mContext.getSystemService(NotificationManager.class);mStatusBar = statusBar;if (mStatusBar != null) {mStatusBar.setIconVisibility(mSlotIme, false);}updateSystemUiLocked(mCurToken, mImeWindowVis, mBackDisposition);mShowOngoingImeSwitcherForPhones = mRes.getBoolean(com.android.internal.R.bool.show_ongoing_ime_switcher);if (mShowOngoingImeSwitcherForPhones) {mWindowManagerInternal.setOnHardKeyboardStatusChangeListener(mHardKeyboardListener);}buildInputMethodListLocked(!mImeSelectedOnBoot /* resetDefaultEnabledIme */);if (!mImeSelectedOnBoot) {Slog.w(TAG, "Reset the default IME as \"Resource\" is ready here.");resetStateIfCurrentLocaleChangedLocked();InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(mIPackageManager,mSettings.getEnabledInputMethodListLocked(),mSettings.getCurrentUserId(), mContext.getBasePackageName());}mLastSystemLocales = mRes.getConfiguration().getLocales();try {//連接默認(rèn)輸入法的服務(wù)startInputInnerLocked();} catch (RuntimeException e) {Slog.w(TAG, "Unexpected exception", e);}}}}systemRunning中還會繼續(xù)進(jìn)行一遍初始化方法中進(jìn)行過的,獲取當(dāng)前所有安裝的輸入法然會設(shè)置默認(rèn)輸入法的操作,更重要的是,在這里會與默認(rèn)輸入法的服務(wù)進(jìn)行連接
InputBindResult startInputInnerLocked() {if (mCurMethodId == null) {return mNoBinding;}if (!mSystemReady) {// If the system is not yet ready, we shouldn't be running third// party code.return new InputBindResult(null, null, mCurMethodId, mCurSeq,mCurUserActionNotificationSequenceNumber);}InputMethodInfo info = mMethodMap.get(mCurMethodId);if (info == null) {throw new IllegalArgumentException("Unknown id: " + mCurMethodId);}unbindCurrentMethodLocked(true);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, Context.BIND_AUTO_CREATE| Context.BIND_NOT_VISIBLE | Context.BIND_NOT_FOREGROUND| Context.BIND_SHOWING_UI)) {mLastBindTime = SystemClock.uptimeMillis();mHaveConnection = true;mCurId = info.getId();mCurToken = new Binder();try {if (true || DEBUG) Slog.v(TAG, "Adding window token: " + mCurToken);//服務(wù)連接成功就將該服務(wù)添加到窗口管理token列表中mIWindowManager.addWindowToken(mCurToken,WindowManager.LayoutParams.TYPE_INPUT_METHOD);} catch (RemoteException e) {}return new InputBindResult(null, null, mCurId, mCurSeq,mCurUserActionNotificationSequenceNumber);} else {mCurIntent = null;Slog.w(TAG, "Failure connecting to input method service: "+ mCurIntent);}return null;}輸入法管理服務(wù)暫時分析到這里,這里還有幾個類說明下其承擔(dān)的功能:
InputMethodInfo/InputMethodSubtype用來描述輸入法相關(guān)信息
InputMethodUtils$InputMethodSettings包含了所有與輸入法相關(guān)數(shù)據(jù)信息的讀取和寫入
InputMethodFileManager用于從subtypes.xml文件中讀取和寫入subtype信息
InputBindResult 用于記錄和描述一次成功的輸入法管理服務(wù)到輸入法的綁定
ClientState 用于記錄一次遠(yuǎn)程連接到當(dāng)前輸入法的客戶端描述
SessionState 用于記錄一次連接的會話狀態(tài)
總結(jié):輸入法管理服務(wù)主要用于管理輸入法,默認(rèn)輸入法的設(shè)置,輸入法應(yīng)用的安裝卸載監(jiān)聽等,輸入法界面的彈出,退出,持有當(dāng)前輸入法和當(dāng)前輸入法服務(wù)對象的連接。
2. 輸入法管理InputMethodManager(IMM)簡介
InputMethodManager存在應(yīng)用進(jìn)程中,在activity展示階段在就初始化了,其主要功能是管理當(dāng)前焦點(diǎn)view,與IMMS通信完成輸入法的展示與隱藏,讓EditText與輸入法產(chǎn)生交互,之后輸入法發(fā)送的內(nèi)容就可以直接通過從IMM傳入到輸入法的連接通道發(fā)送到edittext中。
/*** This is the root view of the overall window that currently has input* method focus.*/View mCurRootView;/*** This is the view that should currently be served by an input method,* regardless of the state of setting that up.*/View mServedView;/*** This is then next view that will be served by the input method, when* we get around to updating things.*/View mNextServedView;IMM中的三個view,mCurRootView是當(dāng)前activity的decorview,mServedView是當(dāng)前正在被服務(wù)的輸入框edittext,mNextServedView是當(dāng)前焦點(diǎn)處在的view,也就是很有可能成為下一個被服務(wù)的view。當(dāng)view焦點(diǎn)發(fā)生改變時候就會通過focusIn與focusOut方法改變mNextServedView的值:
void focusInLocked(View view) {......對view合法性的檢測mNextServedView = view;scheduleCheckFocusLocked(view);}如果對當(dāng)前view有進(jìn)一步的操作,就會將mNextServedView賦值給mServedView
private boolean checkFocusNoStartInput(boolean forceNewFocus) {......synchronized (mH) {......ic = mServedInputConnectionWrapper;mServedView = mNextServedView;mCurrentTextBoxAttribute = null;mCompletions = null;mServedConnecting = true;}......return true;}checkFocusNoStartInput方法被調(diào)用的頻次非常高
變量mClien表示當(dāng)前輸入法服務(wù)對象,由IMMS持有
final IInputMethodClient.Stub mClient = new IInputMethodClient.Stub() {}其內(nèi)部類ControlledInputConnectionWrapper,持有EditableInputConnection通過IMMS最終傳遞到了IputMethodService中,建立了edittext與輸入法之間的連接,輸入法中提交的字符通過這個通道的commitText方法展示到了edittext中。
3.輸入法服務(wù)InputMethodService簡介
InputMethodService是每個輸入法服務(wù)都必須要繼承的基類,結(jié)構(gòu)如下:
通過IInputMethodWrapper類包裝后,在父類的onbind方法中返回給IMMS,
final public IBinder onBind(Intent intent) {if (mInputMethod == null) {mInputMethod = onCreateInputMethodInterface();}return new IInputMethodWrapper(this, mInputMethod);}IMMS中綁定服務(wù)成功后獲得IInputMethodWrapper,
@Overridepublic 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;}if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(MSG_ATTACH_TOKEN, mCurMethod, mCurToken));if (mCurClient != null) {clearClientSessionLocked(mCurClient);requestClientSessionLocked(mCurClient);}}}}方法中mCurToken非空,在mCurMethod被賦值后,會繼續(xù)通過requestClientSessionLocked建立會話,會話建立完成后會將InputMethodService的子類InputMethodSessionImpl傳遞過來保存到ClientState中
void onSessionCreated(IInputMethod method, IInputMethodSession session,InputChannel channel) {synchronized (mMethodMap) {if (mCurMethod != null && method != null&& mCurMethod.asBinder() == method.asBinder()) {if (mCurClient != null) {clearClientSessionLocked(mCurClient);mCurClient.curSession = new SessionState(mCurClient,method, session, channel);InputBindResult res = attachNewInputLocked(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();}4.從edittext點(diǎn)擊到輸入法界面顯示過程
該過程分為兩步,點(diǎn)擊后與輸入法的bind與start過程,輸入法界面的展示過程。
點(diǎn)擊后的bind與start過程如下:
這里重點(diǎn)看下startInputInner與attachNewInputLocked,在這里會生成view的EditableInputConnection,作為參數(shù)傳入到ControlledInputConnectionWrapper中,ControlledInputConnectionWrapper作為參數(shù)傳入到IMMS中,最后通過startInput在將IInputMethodWrapper再次包裝成一個ControlledInputConnectionWrapper傳入到InputMethodService中賦值給mStartedInputConnection。這樣輸入法與EditText之間的就關(guān)聯(lián)上了。
boolean startInputInner(@InputMethodClient.StartInputReason final int startInputReason,IBinder windowGainingFocus, int controlFlags, int softInputMode,int windowFlags) {.....//得到view的連接EditableInputConnectionInputConnection ic = view.onCreateInputConnection(tba);......servedContext = new ControlledInputConnectionWrapper(icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this);......final InputBindResult res = mService.startInputOrWindowGainedFocus(startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode,windowFlags, tba, servedContext, missingMethodFlags);......} InputBindResult attachNewInputLocked(boolean initial) {......final SessionState session = mCurClient.curSession;if (initial) {executeOrSendMessage(session.method, mCaller.obtainMessageIOOO(MSG_START_INPUT, mCurInputContextMissingMethods, session, mCurInputContext,mCurAttribute));} else {executeOrSendMessage(session.method, mCaller.obtainMessageIOOO(MSG_RESTART_INPUT, mCurInputContextMissingMethods, session, mCurInputContext,mCurAttribute));}if (mShowRequested) {if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");showCurrentInputLocked(getAppShowFlags(), null);}return new InputBindResult(session.session,(session.channel != null ? session.channel.dup() : null),mCurId, mCurSeq, mCurUserActionNotificationSequenceNumber);}進(jìn)入到InputMethodWrapper中,參數(shù)中的inputContext就是IMM中生成的ControlledInputConnectionWrapper
public void startInput(IInputContext inputContext,@InputConnectionInspector.MissingMethodFlags final int missingMethods,EditorInfo attribute) {mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_START_INPUT,missingMethods, inputContext, attribute));} case DO_START_INPUT: {SomeArgs args = (SomeArgs)msg.obj;int missingMethods = msg.arg1;IInputContext inputContext = (IInputContext)args.arg1;//再次封裝一層,這里不明白為何還要再封裝一層InputConnection ic = inputContext != null? new InputConnectionWrapper(mTarget, inputContext, missingMethods) : null;EditorInfo info = (EditorInfo)args.arg2;info.makeCompatible(mTargetSdkVersion);//ic賦值給了InputMethodService中的mStartedInputConnectioninputMethod.startInput(ic, info);args.recycle();return;} void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) {if (!restarting) {doFinishInput();}mInputStarted = true;//連接建立mStartedInputConnection = ic;mInputEditorInfo = attribute;initialize();if (DEBUG) Log.v(TAG, "CALL: onStartInput");onStartInput(attribute, restarting);if (mWindowVisible) {if (mShowInputRequested) {if (DEBUG) Log.v(TAG, "CALL: onStartInputView");mInputViewStarted = true;onStartInputView(mInputEditorInfo, restarting);startExtractingText(true);} else if (mCandidatesVisibility == View.VISIBLE) {if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");mCandidatesViewStarted = true;onStartCandidatesView(mInputEditorInfo, restarting);}}}以上流程完成之后輸入法與edittext之間的連接建立,輸入法也已經(jīng)綁定,可以展示界面進(jìn)行輸入操作了。輸入法界面展示流程如下:
5.輸入法字符傳遞到edittext過程
輸入法字符發(fā)送到edittext的流程如下,這里以sendKeyChar為開頭,看過源碼中的拉丁輸入法是直接getCurrentInputConnection.commitText發(fā)送字符的
在BaseInputConnection中的replaceText方法中
private void replaceText(CharSequence text, int newCursorPosition,boolean composing) {final Editable content = getEditable();......content.replace(a, b, text);.....}子類EditableInputConnection中的重載方法如下:
public Editable getEditable() {TextView tv = mTextView;if (tv != null) {return tv.getEditableText();}return null;}所以最終文字的字符從輸入法發(fā)送到edittext是通過按索引替換來實(shí)現(xiàn)的。
總結(jié)
以上是生活随笔為你收集整理的android输入法架构解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++版本OpenCv教程(四十三)直线
- 下一篇: MCCMNC列表