输入法浅谈
  R3jekIqJDwNw 2023年11月02日 24 0

一、输入法介绍

输入法是指将各种文字/符号等信息输入电子信息设备而采用的编码方式,例如我们常用的拼音输入法、手写输入法、语音输入法等都是在完成这一过程。通俗的来讲输入法是一种已经融入到我们生活方方面面,各行各业的基本人机交互方式:

输入法

场景

特点

拼音输入法/五笔输入法/仓颉输入法

电脑手机等设备打字、发邮件

简单、高效、手打

手写输入法

老人机

笔迹识别、直打

语音输入法

车载智能语音交互

简单、直说、高效

...



从上面可以看出输入法根据运用场景的不同,其功能特点也有所不同,输入法可以进行以下分类:


  • 根据输入方式和特点分类:
  • 输入法浅谈_android


  • 根据语系/语种分类:
  • 输入法浅谈_输入法_02


  • 根据键盘布局特点
  • 输入法浅谈_输入法_03

而我们最为熟悉的中文输入法根据编码原理的特点可进行以下分类

输入法浅谈_android_04

可以说虽然输入使用方便,操作简单,但其背后的实现技术复杂且巧妙。 (想进一步了解输入法如何对应各种字符可查看《字符编码介绍》, 进一步了解几种常见中文输入法实现可查看《中文输入法浅谈》)以百度输入法app为例

输入法浅谈_android_05

输入法可分为以下几个组件:

输入法浅谈_android_06

输入法软件除了实现输入字符的基本功能外,往往还集成了其他功能,以提升其效率和体验,主流输入法软件功能汇总如下:

输入法浅谈_输入法_07

二、Android 10(Q) 输入法框架(IMF)介绍

简单介绍输入法基本概念功能,接下来看看Android平台输入法功能的相关实现技术。 所涉及源码及路径如下:

frameworks\base\core\java\android\inputmethodservice\InputMethodService.java
frameworks\base\core\java\android\view\ViewRootImpl.java
frameworks\base\core\java\android\view\WindowManagerGlobal.java
frameworks\base\core\java\android\view\inputmethod\InputMethodManager.java
frameworks\base\core\java\android\widget\TextView.java
frameworks\base\core\java\android\view\inputmethod\InputMethod.java
frameworks\base\core\java\android\inputmethodservice\SoftInputWindow.java
frameworks\base\core\java\com\android\internal\view\IInputSessionCallback.aidl
frameworks\base\services\core\java\com\android\server\inputmethod\InputMethodManagerService.java
frameworks\base\services\core\java\com\android\server\wm\WindowState.java
packages\inputmethods\PinyinIME\AndroidManifest.xml
packages\inputmethods\PinyinIME\src\com\android\inputmethod\pinyin\PinyinIME.java

2.1 IMF架构

IMF(Input Method Framework)即输入法框架,是Android 1.5添加的重要功能,用来支持软键盘和各种输入法。 IMF有三个部分组成:IMMS、IMS、IMM

IMF整体框架如下图:

IMF交互

2.1.1 InputMethodManagerService(IMMS)输入法管理服务

IMMS负责管理系统的所有输入法,包括输入法的加载及切换。

2.1.2 InputMethodService(IMS)输入法服务

IMS即输入法应用,例如LatinIME、PinyinIME应用(本质是IMS的一个实现类),实现输入法界面,控制字符输入等。 以PinyinIME为例: packages\inputmethods\PinyinIME\AndroidManifest.xml

<service android:name=".PinyinIME"
                android:label="@string/ime_name"
                    android:permission="android.permission.BIND_INPUT_METHOD">
                <intent-filter>
                    <action android:name="android.view.InputMethod" />
                </intent-filter>
                <meta-data android:name="android.view.im" android:resource="@xml/method" />
            </service>

packages\inputmethods\PinyinIME\src\com\android\inputmethod\pinyin\PinyinIME.java

/**
 * Main class of the Pinyin input method.
 */
public class PinyinIME extends InputMethodService {//PinyinIME继承IMS
	...
}

IMS类图结构如下:

输入法浅谈_android_08

由此可以看出PinyinIME输入法应用是一个带Dialog 的service。

2.1.3 InputMethodManager(IMM)输入法客户端

IMM即通常带有EditView的app应用,使用IMM来发起显示/隐藏输入法的请求,也可以配置输入法的一些属性,是app与IMMS通信的接口。每个程序有一个IMM的实例,在ViewRootImlp初始化时创建:frameworks\base\core\java\android\view\ViewRootImpl.java

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        ...
    }

frameworks\base\core\java\android\view\WindowManagerGlobal.java

@UnsupportedAppUsage
    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    // Emulate the legacy behavior.  The global instance of InputMethodManager
                    // was instantiated here.
                    InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();//准备创建IMM实例
                    ...
    }

frameworks\base\core\java\android\view\inputmethod\InputMethodManager.java

public static void ensureDefaultInstanceForDefaultDisplayIfNecessary() {
        forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper());
    }

    @NonNull
    private static InputMethodManager forContextInternal(int displayId, Looper looper) {
        final boolean isDefaultDisplay = displayId == Display.DEFAULT_DISPLAY;
        synchronized (sLock) {
            InputMethodManager instance = sInstanceMap.get(displayId);//从缓存Map中查找默认IMM
            if (instance != null) {
                return instance;
            }
            instance = createInstance(displayId, looper);//没有默认IMM则创建新的IMM实例
            // For backward compatibility, store the instance also to sInstance for default display.
            if (sInstance == null && isDefaultDisplay) {
                sInstance = instance;//如果当前创建的IMM是用于默认的显示,则用作全局单例实例
            }
            sInstanceMap.put(displayId, instance);//缓存到Map中
            return instance;
        }
    }

    @NonNull
    private static InputMethodManager createInstance(int displayId, Looper looper) {
        return isInEditMode() ? createStubInstance(displayId, looper)
                : createRealInstance(displayId, looper);
    }
    private static boolean isInEditMode() {	//对于layoutlib,要覆盖此方法以返回{@code true}
        return false;
    }

    @NonNull
    private static InputMethodManager createRealInstance(int displayId, Looper looper) {
        final IInputMethodManager service;
        try {
            service = IInputMethodManager.Stub.asInterface(	//获取输入法服务
                    ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
        } catch (ServiceNotFoundException e) {
            throw new IllegalStateException(e);
        }
        //创建IMM 实例
        final InputMethodManager imm = new InputMethodManager(service, displayId, looper);
        final long identity = Binder.clearCallingIdentity();
        try {
        	//将IMM实例添加到输入法服务
        	//imm.mClient是一个IInputMethodClient.aidl的实例,IMMS通过它告知IMM IInputMethodSession
        	//imm.mIInputContext是一个IInputContext.aidl的实例
            service.addClient(imm.mClient, imm.mIInputContext, displayId);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
        return imm;
    }

2.2 输入法启动

这里以常见的EditText调出输入法界面为例,输入法的启动分为以下几个过程:

  • 控件获取焦点,通过IMM向IMMS请求绑定输入法
  • IMMS处理IMM请求,view绑定输入法整体启动时序图如下:

2.2.1 控件获取焦点

一个app或控件要想启动IME,首先需要获取焦点。当打开需要调出IME的某个App时,焦点开始变更,我们从WindowState分发焦点开始往下梳理:frameworks\base\services\core\java\com\android\server\wm\WindowState.java

/**
     * Report a focus change.  Must be called with no locks held, and consistently
     * from the same serialized thread (such as dispatched from a handler).
     */
    void reportFocusChangedSerialized(boolean focused, boolean inTouchMode) {
        try {
            mClient.windowFocusChanged(focused, inTouchMode);//通知app的IWindow焦点改变
        } catch (RemoteException e) {
        }
        ...
    }

frameworks\base\core\java\android\view\ViewRootImpl.java

public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
        synchronized (this) {
            mWindowFocusChanged = true;	//标记焦点改变
            mUpcomingWindowFocus = hasFocus;	//记录传下来的焦点
            mUpcomingInTouchMode = inTouchMode;	//记录传下来的触摸模式标记
        }
        Message msg = Message.obtain();
        msg.what = MSG_WINDOW_FOCUS_CHANGED;
        mHandler.sendMessage(msg);	//转移到主线程进行处理
    }


    final class ViewRootHandler extends Handler {
    	...
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            	...
                case MSG_WINDOW_FOCUS_CHANGED: {
                    handleWindowFocusChanged();
                } break;
                ...
            }
    }

    private void handleWindowFocusChanged() {
        final boolean hasWindowFocus;
        final boolean inTouchMode;
        synchronized (this) {
            if (!mWindowFocusChanged) { //焦点没有改变这不往下走,直接return
                return;
            }
            mWindowFocusChanged = false;
            hasWindowFocus = mUpcomingWindowFocus;
            inTouchMode = mUpcomingInTouchMode;
        }
        ...
        if (mAdded) {
            ...

            mLastWasImTarget = WindowManager.LayoutParams
                    .mayUseInputMethod(mWindowAttributes.flags);//通过window的flag判断是否需要与输入法进行交互

            InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
            ...
            // Note: must be done after the focus change callbacks,
            // so all of the view state is set up correctly.
            if (hasWindowFocus) {//只有获取到了焦点才会与输入法进行交互
                if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) {
                    imm.onPostWindowFocus(mView, mView.findFocus(),
                            mWindowAttributes.softInputMode,
                            !mHasHadWindowFocus, mWindowAttributes.flags);
                }
               ...
        }
    }

frameworks\base\core\java\android\view\inputmethod\InputMethodManager.java

public void onPostWindowFocus(View rootView, View focusedView,
            @SoftInputModeFlags int softInputMode, boolean first, int windowFlags) {
        ...
        //设置输入法启动标志位
        int startInputFlags = 0;
        if (focusedView != null) {
            startInputFlags |= StartInputFlags.VIEW_HAS_FOCUS;
            if (focusedView.onCheckIsTextEditor()) {
                startInputFlags |= StartInputFlags.IS_TEXT_EDITOR;
            }
        }
        if (first) {	//第一次启动输入法
            startInputFlags |= StartInputFlags.FIRST_WINDOW_FOCUS_GAIN;
        }

        if (checkFocusNoStartInput(forceNewFocus)) {
        	//准备启动输入法
            if (startInputInner(StartInputReason.WINDOW_FOCUS_GAIN, rootView.getWindowToken(),
                    startInputFlags, softInputMode, windowFlags)) {
                return;
            }
        }
        ...
        }
    }

    boolean startInputInner(@StartInputReason int startInputReason,
            @Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
            @SoftInputModeFlags int softInputMode, int windowFlags) {
        ...
        EditorInfo tba = new EditorInfo();	//创建EditorInfo实例,输入法会根据它显示不同面板
        tba.packageName = view.getContext().getOpPackageName();
        tba.fieldId = view.getId();
        InputConnection ic = view.onCreateInputConnection(tba);	//创建InputConnection,接收IMS传递的文本信息。
        ...
            if (ic != null) {
                ...
                //创建一个真正与IMS交互的Binder对象,后续IMS会通过ControlledInputConnectionWrapper间接的对InputConnection调用,从而操作目标控件
                servedContext = new ControlledInputConnectionWrapper(
                        icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this);
            } else {
                servedContext = null;
                missingMethodFlags = 0;
            }
            mServedInputConnectionWrapper = servedContext;

            try {
				//通过IMMS开启输入法
                final InputBindResult res = mService.startInputOrWindowGainedFocus(
                        startInputReason, mClient, windowGainingFocus, startInputFlags,
                        softInputMode, windowFlags, tba, servedContext, missingMethodFlags,
                        view.getContext().getApplicationInfo().targetSdkVersion);
               ...
        }
        return true;
    }

2.2.2 IMMS处理IMM请求

frameworks\base\services\core\java\com\android\server\inputmethod\InputMethodManagerService.java

@NonNull
    @Override
    public InputBindResult startInputOrWindowGainedFocus(...) {
        ...
        synchronized (mMethodMap) {
            final long ident = Binder.clearCallingIdentity();
            try {
                result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client,
                        windowToken, startInputFlags, softInputMode, windowFlags, attribute,
                        inputContext, missingMethods, unverifiedTargetSdkVersion, userId);
        ...
        return result;
    }

    @NonNull
    private InputBindResult startInputOrWindowGainedFocusInternalLocked(...) {
        ...
        if (!didStart) {	//检查是否启动过,未启动过则往下继续
            if (attribute != null) {	//attribute为EditorInfo对象,检查是否为空
                if (!DebugFlags.FLAG_OPTIMIZE_START_INPUT.value()
                        || (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0) { 		//检查是否为文本编辑器
                    res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
                            startInputFlags, startInputReason);
		...
    }

    @GuardedBy("mMethodMap")
    @NonNull
    InputBindResult startInputUncheckedLocked(...) {
        ...
        InputMethodInfo info = mMethodMap.get(mCurMethodId);//从输入法列表中获取当前输入法服务信息
		//清理环境,解绑当前输入法
        unbindCurrentMethodLocked();
		//开始绑定IMS
        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));

		//绑定当前IMS
        if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
            mLastBindTime = SystemClock.uptimeMillis();
            mHaveConnection = true;
            ...
            //返回正在等待绑定的状态结果
            return new InputBindResult(
                    InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
                    null, null, mCurId, mCurSeq, null);
        }
        ...
    }


    @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();
                    return;
                }
                //IME绑定成功,并得到IInputMethod.aidl实例mCurMethod;发送msg准备初始化输入法
                executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(
                        MSG_INITIALIZE_IME, mCurTokenDisplayId, mCurMethod, mCurToken));
                if (mCurClient != null) {	//如果此时有app等待输入法则创建Session,准备启动输入法
                    clearClientSessionLocked(mCurClient);
                    requestClientSessionLocked(mCurClient);
                }
            }
        }
    }

    void requestClientSessionLocked(ClientState cs) {
        if (!cs.sessionRequested) {	//client未绑定过IME则往下执行
            InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString());	//创建inputChannel数组,长度为2,一个用于发布输入事件,一个用于消费输入事件
            cs.sessionRequested = true;
            executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO(	//发送msg创建Session
                    MSG_CREATE_SESSION, mCurMethod, channels[1],
                    new MethodCallback(this, mCurMethod, channels[0])));
        }
    }
    
    void executeOrSendMessage(IInterface target, Message msg) {
         ...
             handleMessage(msg);
             msg.recycle();
         }
    }
    
    @MainThread
    @Override
    public boolean handleMessage(Message msg) {
        SomeArgs args;
        switch (msg.what) {
        	case MSG_CREATE_SESSION: {
                args = (SomeArgs)msg.obj;
                IInputMethod method = (IInputMethod)args.arg1;
                InputChannel channel = (InputChannel)args.arg2;
                try {
                    method.createSession(channel, (IInputSessionCallback)args.arg3);	//告知输入法,需要创建输入法对话
                ...
        }
    }

frameworks\base\core\java\com\android\internal\view\IInputSessionCallback.aidl

oneway interface IInputSessionCallback {
    void sessionCreated(IInputMethodSession session);
}

frameworks\base\services\core\java\com\android\server\inputmethod\InputMethodManagerService.java

void onSessionCreated(IInputMethod method, IInputMethodSession session,
            InputChannel channel) {
        synchronized (mMethodMap) {
            if (mCurMethod != null && method != null
                    && mCurMethod.asBinder() == method.asBinder()) {
                if (mCurClient != null) {
                    clearClientSessionLocked(mCurClient);	//清除client旧的Session
                    ////IMMS 使用IInputMethod为客户端创建一个IInputMethodSession
                    mCurClient.curSession = new SessionState(mCurClient,
                            method, session, channel);
                    InputBindResult res = attachNewInputLocked(	//准备启动输入法
                            StartInputReason.SESSION_CREATED_BY_IME, true);
                    if (res.method != null) {
                        executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO(	//通知client进行绑定
                                MSG_BIND_CLIENT, mCurClient.client, res));
                    }
                    return;
	...
    }


    @GuardedBy("mMethodMap")
    @NonNull
    InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) {
        if (!mBoundToMethod) {	//没有将client绑定到输入法则需先进行绑定操作
            executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
                    MSG_BIND_INPUT, mCurMethod, mCurClient.binding));
            mBoundToMethod = true;
        }
		...
        final SessionState session = mCurClient.curSession;
        executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(	//通知输入法要准备启动了
                MSG_START_INPUT, mCurInputContextMissingMethods, initial ? 0 : 1 /* restarting */,
                startInputToken, session, mCurInputContext, mCurAttribute));
        if (mShowRequested) {
            showCurrentInputLocked(getAppShowFlags(), null);	//准备启动输入法
        }
        //将结果SUCCESS_WITH_IME_SESSION返回给client
        return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
                session.session, (session.channel != null ? session.channel.dup() : null),
                mCurId, mCurSeq, mCurActivityViewToScreenMatrix);
    }

    @MainThread
    @Override
    public boolean handleMessage(Message msg) {
        SomeArgs args;
        switch (msg.what) {
            case MSG_START_INPUT: {
               	...
                    setEnabledSessionInMainThread(session);	//IMMS最终使用IInputMethod激活IInputMethodSession
                    session.method.startInput(startInputToken, inputContext, missingMethods,	//通过IInputMethod调用startInput()准备启动输入法,并传递IInputContext
                            editorInfo, restarting, session.client.shouldPreRenderIme);
                ...
            }
    }

    @GuardedBy("mMethodMap")
    boolean showCurrentInputLocked(int flags, ResultReceiver resultReceiver) {
        ...
        if (mCurMethod != null) {
            if (DEBUG) Slog.d(TAG, "showCurrentInputLocked: mCurToken=" + mCurToken);
            executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(	//发送MSG_SHOW_SOFT_INPUT消息,显示输入法
                    MSG_SHOW_SOFT_INPUT, getImeShowFlags(), mCurMethod,
                    resultReceiver));
            mInputShown = true;
            ...
        return res;
    }

    @MainThread
    @Override
    public boolean handleMessage(Message msg) {
        SomeArgs args;
        switch (msg.what) {
        	...
	        case MSG_SHOW_SOFT_INPUT:
                args = (SomeArgs)msg.obj;
                try {
                   	//调用InputMethodService.InputMethodImpl.showSoftInput()
                    ((IInputMethod)args.arg1).showSoftInput(msg.arg1, (ResultReceiver)args.arg2);
                } catch (RemoteException e) {
                }
                args.recycle();
                return true;
             ...
        }
   	}

frameworks\base\core\java\android\inputmethodservice\InputMethodService.java

...
    SoftInputWindow mWindow;
	...
    @Override public void onCreate() {
        ...
        //创建一个输入法窗口,并设置窗口属性,这个窗口本质是一个Dialog
        mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState,
                WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false);
        // For ColorView in DecorView to work, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS needs to be set
        // by default (but IME developers can opt this out later if they want a new behavior).
        mWindow.getWindow().setFlags(
                FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);

        initViews();//初始化有关View
        mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);//设置输入法窗口宽高
    }
	...
	//由上述IInputMethod调用
    public void startInput(InputConnection ic, EditorInfo attribute) {
        if (DEBUG) Log.v(TAG, "startInput(): editor=" + attribute);
        doStartInput(ic, attribute, false);
    }
    void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) {
        if (!restarting) {
            doFinishInput();
        }
        mInputStarted = true;
        mStartedInputConnection = ic;//设置文本信息通信接口InputConnection,是IInputContext的封装
    	...    
    }
	public InputConnection getCurrentInputConnection() {
        InputConnection ic = mStartedInputConnection;
        if (ic != null) {
            return ic;
        }
        return mInputConnection;
    }
	...
    public class InputMethodImpl extends AbstractInputMethodImpl {
    	...
        @MainThread
        @Override
        public void showSoftInput(int flags, ResultReceiver resultReceiver) {
            ...
                    showWindow(true);
                }
            }
            // If user uses hard keyboard, IME button should always be shown.
            setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
            final boolean isVisible = mIsPreRendered
                    ? mDecorViewVisible && mWindowVisible : isInputViewShown();
            final boolean visibilityChanged = isVisible != wasVisible;
            if (resultReceiver != null) {
                resultReceiver.send(visibilityChanged
                        ? InputMethodManager.RESULT_SHOWN
                        : (wasVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN
                                : InputMethodManager.RESULT_UNCHANGED_HIDDEN), null);
            }
        }
		...
    }

    public void showWindow(boolean showInput) {
		...
        // request draw for the IME surface.
        // When IME is not pre-rendered, this will actually show the IME.
        if ((previousImeWindowStatus & IME_ACTIVE) == 0) {
            mWindow.show();
        }
        maybeNotifyPreRendered();
        mDecorViewWasVisible = true;
        mInShowWindow = false;
    }

frameworks\base\core\java\android\inputmethodservice\SoftInputWindow.java

@Override
    public final void show() {
        switch (mWindowState) {
            case SoftInputWindowState.TOKEN_PENDING:
                throw new IllegalStateException("Window token is not set yet.");
            case SoftInputWindowState.TOKEN_SET:
            case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
                // Normal scenario.  Nothing to worry about.
                try {
                    super.show();
                    updateWindowState(SoftInputWindowState.SHOWN_AT_LEAST_ONCE);
                } catch (WindowManager.BadTokenException e) {
                    // Just ignore this exception.  Since show() can be requested from other
                    // components such as the system and there could be multiple event queues before
                    // the request finally arrives here, the system may have already invalidated the
                    // window token attached to our window.  In such a scenario, receiving
                    // BadTokenException here is an expected behavior.  We just ignore it and update
                    // the state so that we do not touch this window later.
                    Log.i(TAG, "Probably the IME window token is already invalidated."
                            + " show() does nothing.");
                    updateWindowState(SoftInputWindowState.REJECTED_AT_LEAST_ONCE);
                }
                return;
            case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
                // Just ignore.  In general we cannot completely avoid this kind of race condition.
                Log.i(TAG, "Not trying to call show() because it was already rejected once.");
                return;
            case SoftInputWindowState.DESTROYED:
                // Just ignore.  In general we cannot completely avoid this kind of race condition.
                Log.i(TAG, "Ignoring show() because the window is already destroyed.");
                return;
            default:
                throw new IllegalStateException("Unexpected state=" + mWindowState);
        }
    }

2.3 输入法传递输入信息给view

上述过程IMM通过请求IMMS启动了输入法,输入法再将输入文本信息通过IInputContext传递给客户端的view。这里以PinyinIME为例,从打开PinyinIME -->输入"w" --> 选择第一个字"我"--> 将该字传递给view,整体流程如下:

输入法浅谈_android_09

整体过程为:

  • 打开输入法:初始化输入模式、设置键盘布局、加载UI、设置监听等。
  • 按下键盘字母"w",显示候选词:
  • SoftKeyboardView键盘响应按键事件,弹出所按w键的冒泡提示。
  • PinyinIME的onKeyUp()响应,识别为字母按键,添加至拼音字符串,updateDecInfoForSearch()传给输入引擎解码,更新创作栏view和侯选栏view。
  • 选择侯选栏第一个字"我":
  • CandidateView onTouchEventReal()响应触摸事件,showBalloon()显示所选字的冒泡提示。
  • PinyinIME监听到点击事件,一方面将所选词通过commitResultText()传递给view,一方面根据所选词继续更新侯选词,进行联想。
  • 通过InputConnection通讯接口(IInputConnext的封装)将text信息最终传给BaseInputConnection,由replaceText()更新view的内容。
  • 如果text为单字符,在sendCurrentText()判断是否为需要处理的按键事件,例如send,search键等,传递给ViewRootImp放入input事件队列进行处理。

2.4 总结IMF交互过程

回顾开篇IMF整体框架图,总结IMF交互过程如下:

输入法浅谈_android_10

  • IMM 利用IInputMethodManager请求IMMS
  • IMMS 绑定IMS,得到IInputMethod
  • IMMS 请求IInputMethod创建IInputMethodSession
  • IMMS 通过IInputMethodClient 告知IMM IInputMethodSession
  • IMM和IMS通过IInputMethodSession和IInputContext交互

三、 输入法debug常用命令

3.1 使用ime命令查看详情

$ ime
ime <command>:
  list [-a] [-s]
    prints all enabled input methods.
      -a: see all input methods
      -s: only a single summary line of each
  enable [--user <USER_ID>] <ID>
    allows the given input method ID to be used.
      --user <USER_ID>: Specify which user to enable. Assumes the current user if not specified.
  disable [--user <USER_ID>] <ID>
    disallows the given input method ID to be used.
      --user <USER_ID>: Specify which user to disable. Assumes the current user if not specified.
  set [--user <USER_ID>] <ID>
    switches to the given input method ID.
      --user <USER_ID>: Specify which user to enable. Assumes the current user if not specified.
  reset [--user <USER_ID>]
    reset currently selected/enabled IMEs to the default ones as if the device is initially booted w
    ith the current locale.
      --user <USER_ID>: Specify which user to reset. Assumes the current user if not specified.

ime命令参考

作用

ime list

查看已启用输入法详细信息

ime list -a

查看所有输入法详细信息

ime list -s

单行打印所有已启用输入法

ime enable com.android.inputmethod.latin/.LatinIME

启用LatinIME

ime diable com.android.inputmethod.latin/.LatinIME

禁用LatinIME

ime set com.android.inputmethod.latin/.LatinIME

切换至LatinIME输入法

ime reset com.android.inputmethod.latin/.LatinIME

重置LatinIME输入法配置

3.2 dumpsys input_method

130|zc16a:/ # dumpsys input_method
Current Input Method Manager state:
  Input Methods: mMethodMapUpdateCount=2
  InputMethod #0:
    mId=com.android.inputmethod.pinyin/.PinyinIME mSettingsActivityName=com.android.inputmethod.pinyin.SettingsActivity mIsVrOnly=false mSupportsSwitchingToNextInputMethod=false
    mIsDefaultResId=0x7f010000
    Service:
      priority=0 preferredOrder=0 match=0x108000 specificIndex=-1 isDefault=false
      ServiceInfo:
        name=com.android.inputmethod.pinyin.PinyinIME
        packageName=com.android.inputmethod.pinyin
        labelRes=0x7f080000 nonLocalizedLabel=null icon=0x0 banner=0x0
        enabled=true exported=true directBootAware=false
        permission=android.permission.BIND_INPUT_METHOD
        flags=0x0
        ApplicationInfo:
          packageName=com.android.inputmethod.pinyin
          labelRes=0x7f080000 nonLocalizedLabel=null icon=0x7f040000 banner=0x0
          processName=com.android.inputmethod.pinyin
          taskAffinity=com.android.inputmethod.pinyin
          uid=10069 flags=0x2088be45 privateFlags=0xc401000 theme=0x0
          requiresSmallestWidthDp=0 compatibleWidthLimitDp=0 largestWidthLimitDp=0
          sourceDir=/system/app/PinyinIME/PinyinIME.apk
          seinfo=default:targetSdkVersion=29
          seinfoUser=:complete
          dataDir=/data/user/0/com.android.inputmethod.pinyin
          deviceProtectedDataDir=/data/user_de/0/com.android.inputmethod.pinyin
          credentialProtectedDataDir=/data/user/0/com.android.inputmethod.pinyin
          enabled=true minSdkVersion=29 targetSdkVersion=29 versionCode=29 targetSandboxVersion=1
          supportsRtl=false
          fullBackupContent=true
          HiddenApiEnforcementPolicy=0
          usesNonSdkApi=true
          allowsPlaybackCapture=true
  InputMethod #1:
    mId=com.he.ardc/.ARDCIME mSettingsActivityName=null mIsVrOnly=false mSupportsSwitchingToNextInputMethod=false
    mIsDefaultResId=0x0
    Service:
      priority=0 preferredOrder=0 match=0x108000 specificIndex=-1 isDefault=false
      ServiceInfo:
        name=com.he.ardc.ARDCIME
        packageName=com.he.ardc
        labelRes=0x7f030001 nonLocalizedLabel=null icon=0x0 banner=0x0
        enabled=true exported=true directBootAware=false
        permission=android.permission.BIND_INPUT_METHOD
        flags=0x0
        ApplicationInfo:
          packageName=com.he.ardc
          processName=com.he.ardc
          taskAffinity=com.he.ardc
          uid=10060 flags=0x38e8be44 privateFlags=0x24001000 theme=0x0
          requiresSmallestWidthDp=0 compatibleWidthLimitDp=0 largestWidthLimitDp=0
          sourceDir=/data/app/com.he.ardc-2eyG7rZV7X8Cv-mHxfxR5Q==/base.apk
          seinfo=default:targetSdkVersion=26
          seinfoUser=:complete
          dataDir=/data/user/0/com.he.ardc
          deviceProtectedDataDir=/data/user_de/0/com.he.ardc
          credentialProtectedDataDir=/data/user/0/com.he.ardc
          enabled=true minSdkVersion=19 targetSdkVersion=26 versionCode=1379 targetSandboxVersion=1
          supportsRtl=true
          fullBackupContent=true
          HiddenApiEnforcementPolicy=2
          usesNonSdkApi=false
          allowsPlaybackCapture=false
  InputMethod #2:
    mId=com.koushikdutta.vysor/.VysorIME mSettingsActivityName=com.koushikdutta.vysor.DummyIMEActivity mIsVrOnly=false mSupportsSwitchingToNextInputMethod=true
    mIsDefaultResId=0x0
    Service:
      priority=0 preferredOrder=0 match=0x108000 specificIndex=-1 isDefault=false
      ServiceInfo:
        name=com.koushikdutta.vysor.VysorIME
        packageName=com.koushikdutta.vysor
        labelRes=0x7f0d001d nonLocalizedLabel=null icon=0x0 banner=0x0
        enabled=true exported=true directBootAware=false
        permission=android.permission.BIND_INPUT_METHOD
        flags=0x0
        ApplicationInfo:
          name=com.koushikdutta.vysor.VysorApplication
          packageName=com.koushikdutta.vysor
          labelRes=0x7f0d001d nonLocalizedLabel=null icon=0x7f0c0000 banner=0x0
          className=com.koushikdutta.vysor.VysorApplication
          processName=com.koushikdutta.vysor
          taskAffinity=com.koushikdutta.vysor
          uid=10125 flags=0x38a8be44 privateFlags=0xc001000 theme=0x7f0e0116
          requiresSmallestWidthDp=0 compatibleWidthLimitDp=0 largestWidthLimitDp=0
          sourceDir=/data/app/com.koushikdutta.vysor-th96FyIzBNMu_OEC5tVbbw==/base.apk
          seinfo=default:targetSdkVersion=30
          seinfoUser=:complete
          dataDir=/data/user/0/com.koushikdutta.vysor
          deviceProtectedDataDir=/data/user_de/0/com.koushikdutta.vysor
          credentialProtectedDataDir=/data/user/0/com.koushikdutta.vysor
          enabled=true minSdkVersion=19 targetSdkVersion=30 versionCode=1656572400 targetSandboxVersion=1
          supportsRtl=false
          fullBackupContent=true
          HiddenApiEnforcementPolicy=2
          usesNonSdkApi=false
          allowsPlaybackCapture=true
		  ...

四、展望/交流/讨论

综上简单介绍了输入法的基本概念,分类,特点;android 系统中PinyinIME的启动流程,文本的交互流程。当然输入法最核心的是在输入引擎上,好的输入引擎能够既快又准的显示输入内容,能够根据用户使用习惯来动态联想和纠错。这部分待研究熟悉后再分享交流。在享受输入法带给我们便利的同时也需要警惕其背后的风险和隐患,主流的输入法基本是闭源,联网的,而我们的各类隐私信息经常需要通过输入法与各式各样的应用软件交互,这无疑存在很大的安全隐患,同时也由于主流输入法功能过于固定,并不适用各类行业和人群,因此开源输入法也应运而生,较为著名的是开源的,基于rime输入引擎的多平台支持的输入法(trime、Wease、Squirrel、ibus-rime),支持丰富的定制化功能,效果图如下:

输入法浅谈_android_11

除了文章开头提到的诸如键打、手写、语音方式的输入法,也有或将有更多有用和有趣的输入法,如手势识别,脑电波识别等等~

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  R3jekIqJDwNw   2023年11月12日   21   0   0 android关机
R3jekIqJDwNw