Android 输入法框架流程整理

Android 输入法框架流程整理

个人简单整理, 如有错误请指正

1. 基本简介

1.1 输入法框架构成

输入法框架整体分为三个部分:

  1. 输入法管理端
  2. 输入法服务端
  3. 输入法客户端

输入法管理端(IMMS)

主要指输入法框架的InputMethodManagerService, 如切换输入法, 输入法显示/隐藏, 输入法启用/关闭, 输入法服务端的绑定, 输入法服务端与输入法客户端的绑定等. 运行在system_server进程

输入法服务端(IMS)

主要指输入法框架的InputMethodService, 这是一个输入法服务, 真正实现输入法界面, 控制字符输入的地方. 运行在输入法进程, 例如搜狗输入法进程

输入法客户端(IMM)

主要指输入法框架的InputMethodManager, 每个app都一个实例, 用来和输入法控制端交互. 运行在需要使用输入法的进程

1.2 输入法交互框架

输入法的整体交互过程如下:

  1. IMM 利用 IInputMethodManager 请求 IMMS
  2. IMMS 绑定输入法服务 InputMethodService, 得到 IInputMethod
  3. IMMS 请求 IInputMethod 创建交互 IInputMethodSession
  4. IMMS 通过 IInputMethodClient 告知 IMM IInputMethodSession
  5. IMM 和 IMS 通过 IInputMethodSession 和 IInputContext 交互

输入法的整体交互框架如下:

输入法三端交互框架

2. 输入法初始化过程

2.1 输入法客户端( IMM )的初始化过程

IMM
InputMethodManager
frameworks/base/core/java/android/view/inputmethod/InputMethodManager.java

每个app都有一个IMM实例, 并且在应用内是单例存在. 当app需要输入法时操作IMM来请求IMMS进行输入法操作; 当IMMS需要通知app时, 则通过IMM告知app下一步操作.

输入法的初始话从添加window开始的, 每次添加window时会实例化 ViewRootImpl, ViewRootImpl 在获取 IWindowSession时会检查输入法是否已经初始化.

输入法客户端(IMM)的初始化过程

以下源码从 android 10.0 摘抄出来, 无用代码已删除.

// ViewRootImpl.java
public ViewRootImpl(Context context, Display display){
    // 此前添加window的过程省略了, 直接从实例化 ViewRootImpl 开始
	mWindowSession = WindowManagerglobal.getWindowSession();
}

// ----------------------------------------------------------------------

// WindowManagerglobal.java
public static IWindowSession getWindowSession() {
  synchronized (WindowManagerGlobal.class) {
    if (sWindowSession == null) {
      // 这里就进行IMM的初始化
      // 和之前的版本有不同, 似乎为多显示器使用不同的输入法做适配
      InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
    }
    return sWindowSession;
  }
}

// ----------------------------------------------------------------------

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

// InputMethodManager.java
private static InputMethodManager forContextInternal(int displayId, Looper looper) {
  final boolean isDefaultDisplay = displayId == Display.DEFAULT_DISPLAY;
  synchronized (sLock) {
    // 先从缓存 Map 里找找有没有默认的 IMM
    InputMethodManager instance = sInstanceMap.get(displayId);
    if (instance != null) {
      return instance;
    }
    // 没有缓存过默认的 IMM, 那就创建一个新的
    instance = createInstance(displayId, looper);
    // 如果当前创建的 IMM 是用于默认的显示, 就把它作为全局单例实例
    if (sInstance == null && isDefaultDisplay) {
      sInstance = instance;
    }
    // 缓存到 Map 里面
    sInstanceMap.put(displayId, instance);
    return instance;
  }
}

// InputMethodManager.java
private static InputMethodManager createInstance(int displayId, Looper looper) {
  return isInEditMode() ? createStubInstance(displayId, looper)
    : createRealInstance(displayId, looper);
}

// InputMethodManager.java
private static boolean isInEditMode() {
  return false;
}

// InputMethodManager.java
private static InputMethodManager createRealInstance(int displayId, Looper looper) {
  final IInputMethodManager service;
  // 获取输入法服务
  service = IInputMethodManager.Stub.asInterface(
    ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
  // 创建 IMM 客户端实例
  final InputMethodManager imm = new InputMethodManager(service, displayId, looper);
  // 添加 IMM 实例到输入法服务
  // imm.mClient 是一个aidl对象, IInputMethodClient
  // imm.mIInputContext 是一个aidl对象, IInputContext
  service.addClient(imm.mClient, imm.mIInputContext, displayId);
  return imm;
}

// InputMethodManager.java
private InputMethodManager(IInputMethodManager service, int displayId, Looper looper) {
  mService = service;
  mMainLooper = looper;
  mH = new H(looper);
  mDisplayId = displayId;
  // 创建一个虚拟的输入法上下文, 一本正经的接收输入事件, 然而大多数事情并不处理
  // 用于监听输入法服务的激活状态
  mIInputContext = new ControlledInputConnectionWrapper(looper, mDummyInputConnection, this);
}

// InputMethodManager.java
private static class ControlledInputConnectionWrapper extends IInputConnectionWrapper {

  @Override
  public boolean isActive() {
    // 处理事件之一, 判断输入法是否激活状态
    return mParentInputMethodManager.mActive && !isFinished();
  }

  void deactivate() {
    // 处理事件之二, 关闭 InputConnection
    closeConnection();
  }
}

// ----------------------------------------------------------------------

// InputMethodManagerService.java
public void addClient(IInputMethodClient client,
                      IInputContext inputContext,
                      int selfReportedDisplayId) {
  synchronized (mMethodMap) {
    // 利用IBinder.DeathRecipient监听Client的存活状态
    // 如果Client的Binder死亡, 就将Client从缓存Map里移除掉
    final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
    try {
      client.asBinder().linkToDeath(deathRecipient, 0);
    } catch (RemoteException e) {
      throw new IllegalStateException(e);
    }
    // 创建一个ClientState对象, 并将client和inputContext缓存到里面
    // 然后将ClientState对象缓存在mClients缓存Map里
    mClients.put(client.asBinder(), new ClientState(client,
                                                    inputContext,
                                                    callerUid,
                                                    callerPid,
                                                    selfReportedDisplayId,
                                                    deathRecipient));
  }
}

// InputMethodManager.java
// 保存Client的相关属性
static final class ClientState {
  final IInputMethodClient client;
  final IInputContext inputContext;
  final int uid;
  final int pid;
  final int selfReportedDisplayId;
  final InputBinding binding;
  final ClientDeathRecipient clientDeathRecipient;

  boolean sessionRequested;
  boolean shouldPreRenderIme;
  SessionState curSession;
}

// ----------------------------------------------------------------------

// IInputMethodClient.aidl
// 输入法客户端, 主要用于报告输入法当前的状态, 让app端的 IMM 做出相应的处理
oneway interface IInputMethodClient {
  void onBindMethod(in InputBindResult res);
  void onUnbindMethod(int sequence, int unbindReason);
  void setActive(boolean active, boolean fullscreen);
  void reportFullscreenMode(boolean fullscreen);
  void reportPreRendered(in EditorInfo info);
  void applyImeVisibility(boolean setVisible);
  void updateActivityViewToScreenMatrix(int bindSequence, in float[] matrixValues);
}

// ----------------------------------------------------------------------

// IInputContext.aidl
// 输入法上下文, 主要用于操作字符输入操作, 让当前接收字符的view进行处理
 oneway interface IInputContext {
   void getTextBeforeCursor(int length, int flags, int seq, IInputContextCallback ...);
   void getTextAfterCursor(int length, int flags, int seq, IInputContextCallback ...);
   void getCursorCapsMode(int reqModes, int seq, IInputContextCallback callback);
   void getExtractedText(in ExtractedTextRequest request, int flags, int seq, ...);
   void deleteSurroundingText(int beforeLength, int afterLength);
   void deleteSurroundingTextInCodePoints(int beforeLength, int afterLength);
   void setComposingText(CharSequence text, int newCursorPosition);
   void finishComposingText();
   void commitText(CharSequence text, int newCursorPosition);
   void commitCompletion(in CompletionInfo completion);
   void commitCorrection(in CorrectionInfo correction);
   void setSelection(int start, int end);
   void performEditorAction(int actionCode);
   void performContextMenuAction(int id);
   void beginBatchEdit();
   void endBatchEdit();
   void sendKeyEvent(in KeyEvent event);
   void clearMetaKeyStates(int states);
   void performPrivateCommand(String action, in Bundle data);
   void setComposingRegion(int start, int end);
   void getSelectedText(int flags, int seq, IInputContextCallback callback);
   void requestUpdateCursorAnchorInfo(int cursorUpdateMode, int seq, ...);
   void commitContent(in InputContentInfo inputContentInfo, int flags, in Bundle ...);
}

从上述代码可以看到:

  1. 在app端, IMM 有且只有一个实例, 并且每次创建 ViewRootImpl 时都会检查 IMM 是否已经实例化完成.
  2. 在实例化最后, 会通过 IMMS 的 addClient 将 IMM 添加到 IMMS 中, 这样app端输入法客户端就算实例化完成.
  3. 在实例 IMM 对象时, 会创建一个虚拟的 IInputContext 代表输入控件, 但这里用于监听输入法的激活状态,
  4. 在实例化过程中可以看到有个 displayId 参数, 这个参数是为了多屏幕显示做准备的.

2.2 输入法管理端( IMMS )的初始化过程

IMMS
InputMethodManagerService
frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java

IMMS 运行在system_server进程, 属于系统服务的一部分, 用于控制输入法的切换, 显示/隐藏状态, 输入法服务的绑定等操作.

IMMS 既然为系统服务, 那么会随着系统的启动而启动.

输入法管理端( IMMS )的初始化过程

以下源码从 android 10.0 摘抄出来, 无用代码已删除.

// SystemServer.java
public static void main(String[] args) {
	new SystemServer().run();
}

// SystemServer.java
private void run() {
  ...
  startOtherServices();
}

// SystemServer.java
private void startOtherServices() {
  if (InputMethodSystemProperty.MULTI_CLIENT_IME_ENABLED) {
    // 启动多客户端输入法管理服务, 这个还没有研究, 估计是同时支持多个输入法客户端
    mSystemServiceManager.startService(
      MultiClientInputMethodManagerService.Lifecycle.class);
  } else {
    // 启动输入法管理服务
    mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);
  }
}

// ----------------------------------------------------------------------

// InputMethodManagerService.java
public static final class Lifecycle extends SystemService {

  public Lifecycle(Context context) {
    super(context);
    // 实例化输入法管理服务
    mService = new InputMethodManagerService(context);
  }

  public void onStart() {
    // 将输入法管理服务发布到系统服务中, 以便其他应用可以通过 Binder 获取到
    publishBinderService(Context.INPUT_METHOD_SERVICE, mService);
  }
}

// InputMethodManagerService.java
// IMMS 的构造函数, 主要用于注册一些监听事件, 获取必须的系统服务, UI相关的组件等
public InputMethodManagerService(Context context) {

  // 用于监听来自设置的输入法配置, 比如默认输入法, 启用的输入法, 选择的输入法等
  mSettingsObserver = new SettingsObserver(mHandler);

  // 也是SystemServer.startOtherServices()中初始化的, 在输入法管理服务前发布到系统服务中
  mIWindowManager = IWindowManager.Stub.asInterface(
                ServiceManager.getService(Context.WINDOW_SERVICE));

  // 状态栏输入法图标名称, 会根据这个名称设置输入法的图标显示
  mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);

  // 切换输入法时的通知
  Bundle extras = new Bundle();
  extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);
  @ColorInt final int accentColor = mContext.getColor(
    com.android.internal.R.color.system_notification_accent_color);
  mImeSwitcherNotification = new Notification.Builder(mContext,
				SystemNotificationChannels.VIRTUAL_KEYBOARD)
		.setSmallIcon(com.android.internal.R.drawable.ic_notification_ime_default)
    .setWhen(0)
    .setOngoing(true)
    .addExtras(extras)
    .setCategory(Notification.CATEGORY_SYSTEM)
    .setColor(accentColor);

  // 点击切换输入法通知时要发出对的广播
  Intent intent = new Intent(ACTION_SHOW_INPUT_METHOD_PICKER)
    .setPackage(mContext.getPackageName());
  mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);

  // 获取uid
  userId = ActivityManager.getService().getCurrentUser().id;
  mLastSwitchUserId = userId;

  // 输入法设置对象
  mSettings = new InputMethodSettings(
                mRes, context.getContentResolver(), mMethodMap, userId, !mSystemReady);
}

IMMS 里面有几个比较重要的变量:

String mCurMethodId

系统当前默认的输入法id, 可能为空, 与 Settings.Secure#DEFAULT_INPUT_METHOD 值保持一致

String mCurId

当前已经绑定的输入法id, 如果没有输入法绑定上的话, 值为null

ClientState mCurClient

当前绑定到输入法的客户端, 保存客户端 IMM 的 IInputMethodClient 和 IInputContext

IBinder mCurToken

用于当前激活的 IME, 只有持有这个令牌的 IME 才被系统认可

IInputMethod mCurMethod

当前已经绑定的输入法接口, 如果为null, 说明没有任何输入法连接上

2.3 输入法服务端( IMS )的初始化过程

IMS
InputMethodService
frameworks/base/core/java/android/inputmethodservice/InputMethodService.java

IMS 运行在输入法进程, 是一种特殊的输入法后台服务. 继承结构为:

InputMethodService extends AbstractInputMethodServiceService

所以输入法服务本质上是一个服务, 使用时需要 IMMS 通过 bindService 的方式绑定.

初始化过程在 Service 的 onCreate 方法中, 绑定方法在 onBind 方法中

输入法服务端( IMS )的初始化过程

以下源码从 android 10.0 摘抄出来, 无用代码已删除.

// InputMethodService.java
@Override
public void onCreate() {
  // 主题设置
  mTheme = Resources.selectSystemTheme(
    mTheme,
    getApplicationInfo().targetSdkVersion,
    android.R.style.Theme_InputMethod,
    android.R.style.Theme_Holo_InputMethod,
    android.R.style.Theme_DeviceDefault_InputMethod,
    android.R.style.Theme_DeviceDefault_InputMethod);
  super.setTheme(mTheme);
  super.onCreate();
  // 获取 IMMS 服务
  mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
  // 主要用于监听外接键盘接入事件
  mSettingsObserver = SettingsObserver.createAndRegister(this);
  // 创建一个输入法窗口并设置窗口属性, 这个窗口本质上是一个 Dialog
  mWindow = new SoftInputWindow(this,
                                "InputMethod",
                                mTheme,
                                null,
                                null,
                                mDispatcherState,
                                WindowManager.LayoutParams.TYPE_INPUT_METHOD,
                                Gravity.BOTTOM,
                                false);
  mWindow.getWindow().setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
                               FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
	// 初始化相关View
  initViews();
  // 设置输入法窗口的宽高
  mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);
}

// InputMethodService.java
public static SettingsObserver createAndRegister(InputMethodService service) {
  final SettingsObserver observer = new SettingsObserver(service);
  // 向设置注册外接键盘接入事件
  service.getContentResolver().registerContentObserver(
    Settings.Secure.getUriFor(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD),
    false,
    observer);
  return observer;
}

// InputMethodService.java
void initViews() {
  // 将输入法的View设置给Window
  // 输入法界面布局可以查看 input_method.xml 文件
  mRootView = mInflater.inflate(
    com.android.internal.R.layout.input_method, null);
  mWindow.setContentView(mRootView);

  // 获取输入法Window动画
  if (Settings.Global.getInt(getContentResolver(),
                             Settings.Global.FANCY_IME_ANIMATIONS, 0) != 0) {
    mWindow.getWindow().setWindowAnimations(
      com.android.internal.R.style.Animation_InputMethodFancy);
  }
  // 其他View操作
  ...
}

// SoftInputWindow.java
public class SoftInputWindow extends Dialog {
  public SoftInputWindow(Context context, ...) {
    super(context, theme);
    initDockWindow();
  }
}

// SoftInputWindow.java
private void initDockWindow() {
  // 因为输入法窗口比较特殊, 需要进行单独设置
  WindowManager.LayoutParams lp = getWindow().getAttributes();

  lp.type = mWindowType;
  lp.setTitle(mName);

  // 默认的gravity是 Gravity.BOTTOM
  lp.gravity = mGravity;
  // 根据gravity更新宽高
  updateWidthHeight(lp);

  getWindow().setAttributes(lp);

  int windowSetFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
  int windowModFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
    WindowManager.LayoutParams.FLAG_DIM_BEHIND;

  // 默认值是不获取焦点
  if (!mTakesFocus) {
    windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
  } else {
    windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
    windowModFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
  }

  getWindow().setFlags(windowSetFlags, windowModFlags);
}

// SoftInputWindow.java
private void updateWidthHeight(WindowManager.LayoutParams lp) {
  if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) {
    // 如果window的gravity是top或者bottom, 则横向占满
    // 默认值是bottom
    lp.width = WindowManager.LayoutParams.MATCH_PARENT;
    lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
  } else {
    // 其他情况, 则纵向占满
    lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
    lp.height = WindowManager.LayoutParams.MATCH_PARENT;
  }
}

// AbstractInputMethodService.java
final public IBinder onBind(Intent intent) {
  if (mInputMethod == null) {
    // 创建控制输入法的接口, 不是binder对象, 只存在输入法进程
    // 后续控制输入法的相关操作均是通过这个对象操作的
    mInputMethod = onCreateInputMethodInterface();
  }
  return new IInputMethodWrapper(this, mInputMethod);
}

// InputMethodService.java
public AbstractInputMethodImpl onCreateInputMethodInterface() {
  return new InputMethodImpl();
}

// IInputMethodWrapper.java
// binder 对象, IMMS 就是靠这个来控制输入法的
class IInputMethodWrapper extends IInputMethod.Stub implements HandlerCaller.Callback {
  public IInputMethodWrapper(
    	AbstractInputMethodService context,
    	InputMethod inputMethod) {
    // 一个Handler工具, 用于将消息转到主线程, 实现方式比较奇特, 并且是异步Handler
    mCaller = new HandlerCaller(mContext, null, this, true /*asyncHandler*/);
    // 保存 AbstractInputMethodImpl 对象, 用于操作输入法
    mInputMethod = new WeakReference<>(inputMethod);
  }
}

// IInputMethod.aidl
oneway interface IInputMethod {
  void initializeInternal(IBinder token,
                          int displayId,
                          IInputMethodPrivilegedOperations privOps);
  void bindInput(in InputBinding binding);
  void unbindInput();
  void startInput(in IBinder startInputToken,
                  in IInputContext inputContext,
                  int missingMethods,
                  in EditorInfo attribute,
                  boolean restarting,
                  boolean preRenderImeViews);
  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);
}

// InputMethodService.java
// IMMS 绑定输入法后, 会通知输入法进行初始化
public final void initializeInternal(
  @NonNull IBinder token,
  int displayId,
  IInputMethodPrivilegedOperations privilegedOperations) {
  // 用于设置window的dissplayId
  updateInputMethodDisplay(displayId);
  // 给window设置token, 因为没有token令牌的话, window是不会显示出来的
  attachToken(token);
}

// InputMethodService.java
public void attachToken(IBinder token) {
  if (mToken != null) {
    throw new IllegalStateException(
      "attachToken() must be called at most once. token=" + token);
  }
  mToken = token;
  mWindow.setToken(token);
}

// SoftInputWindow.java
public void setToken(IBinder token) {
  switch (mWindowState) {
    case SoftInputWindowState.TOKEN_PENDING:
      WindowManager.LayoutParams lp = getWindow().getAttributes();
      lp.token = token;
      // 给window设置token令牌
      getWindow().setAttributes(lp);
      // 更新window的token令牌已经被设置好了
      updateWindowState(SoftInputWindowState.TOKEN_SET);
      return;
    case SoftInputWindowState.TOKEN_SET:
    case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
    case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
      throw new IllegalStateException("setToken can be called only once");
    case SoftInputWindowState.DESTROYED:
      Log.i(TAG, "Ignoring setToken() because window is already destroyed.");
      return;
    default:
      throw new IllegalStateException("Unexpected state=" + mWindowState);
  }
}

从上述代码中可以了解到, 输入法的初始化过程主要如下:

  1. 开启Service, 会调用输入法服务的 onCreate, 这里面主要进行softInputwindow和view的初始化过程
  2. 绑定Service, 这里主要是将控制输入法的 InputMethod 用 aidl 接口 IInputMethod 包装后传递给 IMMS
  3. 输入法初始化, 调用 IInputMethod 的 initializeInternal 方法, 将 token 令牌传递给输入法的 window

3. 输入法的启用过程

3.1 客户端从控件获取焦点开始

一个控件如果没有焦点, 并且不是输入目标的话, 输入法是不会弹出来的. 其中, 焦点是输入法弹出的必要不充分条件.

焦点的变更从 WMS 发出, 焦点的变更不是输入法的内容, 暂时略过. 这里从 WindowState 分发焦点开始解析

输入法启动之获取焦点

以下源码从 android 10.0 摘抄出来, 无用代码已删除.

// WindowState.java
void reportFocusChangedSerialized(boolean focused, boolean inTouchMode) {
  // 通知app的IWindow焦点有变化
  mClient.windowFocusChanged(focused, inTouchMode);
}

// ViewRootImpl.java
// ViewRootImpl.W
public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
  synchronized (this) {
    // 标记焦点有改变
    mWindowFocusChanged = true;
    // 暂时记录上游传来的焦点, 这里的值为 true
    mUpcomingWindowFocus = hasFocus;
  }
  // 转移到主线程进行处理
  Message msg = Message.obtain();
  msg.what = MSG_WINDOW_FOCUS_CHANGED;
  mHandler.sendMessage(msg);
}

// ViewRootImpl.java
// ViewRootImpl.ViewRootHandler
case MSG_WINDOW_FOCUS_CHANGED: {
  handleWindowFocusChanged();
} break;

// ViewRootImpl.java
private void handleWindowFocusChanged() {
  synchronized (this) {
    // 焦点没有改变的话, 后续流程就没有必要再走了
    if (!mWindowFocusChanged) {
      return;
    }
    mWindowFocusChanged = false;
    hasWindowFocus = mUpcomingWindowFocus;
  }
  // 调用 ViewRootImpl.setView 时设置为true
  // 调用 ViewRootImpl.setView 是添加View到WindowManager时调用的
	if (mAdded) {
    // 判断是否需要与输入法进行交互, 通过window的标志位来判断的
    mLastWasImTarget = WindowManager.LayoutParams
      	.mayUseInputMethod(mWindowAttributes.flags);

    InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
    // isInLocalFocusMode() 用于判断是否为本地焦点模式
    // 如果为本地焦点模式, 那么就不会从 WindowManager 里获取到触摸和按键事件
    // 只能通过 Window.injectInputEvent 获取相应的事件
    if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) {
      imm.onPreWindowFocus(mView, hasWindowFocus);
    }
    if (mView != null) {
      mView.dispatchWindowFocusChanged(hasWindowFocus);
    }
    // 只有获取到了焦点才会与输入法交互
    if (hasWindowFocus) {
      if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) {
        imm.onPostWindowFocus(mView, mView.findFocus(),
                              mWindowAttributes.softInputMode,
                              !mHasHadWindowFocus, mWindowAttributes.flags);
      }
    }
  }
}
// WindowManager.java
// WindowManager.LayoutParams
public static boolean mayUseInputMethod(int flags) {
  // FLAG_ALT_FOCUSABLE_IM 辅助 FLAG_NOT_FOCUSABLE 使用
  // 正常情况下:
  // 1. 设置了 FLAG_NOT_FOCUSABLE: 目标 window 不能有焦点, 不能弹出输入法
  // 2. 没设置 FLAG_NOT_FOCUSABLE: 目标 window 可以有焦点, 可以弹出输入法
  // FLAG_ALT_FOCUSABLE_IM 的作用就是为了翻转上面两种效果
  switch (flags&(FLAG_NOT_FOCUSABLE|FLAG_ALT_FOCUSABLE_IM)) {
    // 如果目标是可获取焦点的, 并不翻转与输入法的交互行为
    case 0:
  	// 如果目标是不可获取焦点的, 但翻转与输入法的交互行为
    case FLAG_NOT_FOCUSABLE|FLAG_ALT_FOCUSABLE_IM:
      return true;
  }
  // 其他不与输入法交互的情况:
  // 1. 目标可获取焦点, 但翻转与输入法的交互行为
  // 2. 目标不获取焦点, 并不翻转与输入法的交互行为
  return false;
}

// InputMethodManager.java
// 作用是设置或者清理当前获取焦点的控件
public void onPreWindowFocus(View rootView, boolean hasWindowFocus) {
  synchronized (mH) {
    if (rootView == null) {
      // 目标控件为空时, 将当前目标View对象置空
      mCurRootView = null;
    } if (hasWindowFocus) {
      // 目标控件不为空, 并且获取到了焦点, 保存下目标View对象
      mCurRootView = rootView;
    } else if (rootView == mCurRootView) {
      // 目标控件不为空, 并且与当前的目标View对象相同, 但是失去了焦点, 将当前目标View对象置空
      mCurRootView = null;
    } else {
      // 目标控件不为空, 并与当前的目标View对象不同, 同时还失去了焦点, 不进行任何操作
      // 因为当前的目标View对象可能指向另外一个有焦点的View, 并且 IMM 是应用内单例的
      // 贸然操作可能会造成其他获取到焦点的对象不能正常弹出输入法
      if (DEBUG) {
        Log.v(TAG, "Ignoring onPreWindowFocus()."
              + " mCurRootView=" + mCurRootView + " rootView=" + rootView);
      }
    }
  }
}

// View.java
public void dispatchWindowFocusChanged(boolean hasFocus) {
  onWindowFocusChanged(hasFocus);
}

// View.java
public void onWindowFocusChanged(boolean hasWindowFocus) {
  if (!hasWindowFocus) {
    if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
      // 当前控件失去焦点, 通知输入法对该控件的操作
      notifyFocusChangeToInputMethodManager(false /* hasFocus */);
    }
  } else if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
      // 当前控件获得焦点, 通知输入法对该控件的操作
    notifyFocusChangeToInputMethodManager(true /* hasFocus */);
  }
}

// View.java
private void notifyFocusChangeToInputMethodManager(boolean hasFocus) {
  final InputMethodManager imm =
    getContext().getSystemService(InputMethodManager.class);
  if (imm == null) {
    return;
  }
  if (hasFocus) {
    // 通知IMM, 当前View获取到了焦点
    imm.focusIn(this);
  } else {
    // 通知IMM, 当前View失去了焦点, 这里暂不解析
    imm.focusOut(this);
  }
}

// InputMethodManager.java
public void focusIn(View view) {
  synchronized (mH) {
    focusInLocked(view);
  }
}

// InputMethodManager.java
void focusInLocked(View view) {
	// 当发现view的根View与在onPreWindowFocus设置的View不一致时, 说明不是与输入法交互的Window
  // 不进行任何操作
  if (mCurRootView != view.getRootView()) {
    // This is a request from a window that isn't in the window with
    // IME focus, so ignore it.
    if (DEBUG) Log.v(TAG, "Not IME target window, ignoring");
    return;
  }

  // 标记即将拥有输入法焦点的控件
  mNextServedView = view;
  // 检查焦点情况, 确保输入法焦点没有错误
  scheduleCheckFocusLocked(view);
}

// 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;
    // 检查是否为文本编辑器, View默认返回false, TextView一般返回true
    if (focusedView.onCheckIsTextEditor()) {
      // 添加view是文本编辑器的标志位
      startInputFlags |= StartInputFlags.IS_TEXT_EDITOR;
    }
  }
  if (first) {
    // 如果是第一次获取输入法, 添加这个标志位
    startInputFlags |= StartInputFlags.FIRST_WINDOW_FOCUS_GAIN;
  }
	// 检查下是否有需要输入法焦点的控件, IMM 启动输入法前的最后检查操作
  if (checkFocusNoStartInput(forceNewFocus)) {
    // 准备启动输入法
    if (startInputInner(StartInputReason.WINDOW_FOCUS_GAIN, rootView.getWindowToken(),
                        startInputFlags, softInputMode, windowFlags)) {
      return;
    }
  }
  synchronized (mH) {
    try {
	  // 目标控件获取到了焦点, 但是没经过考验, 那么就把这个事件告诉 IMMS
      // 走到这一步的原因如下:
      // 1. 等待输入法过程中, 添加了一些变故, 致使等待拥有输入法焦点的控件变为空
      // 2. 有一些必要条件不满足, 不能够启动输入法
	  // 3. 总的来说, 控件获取了焦点, 但是不满足开启输入法的条件
      if (DEBUG) Log.v(TAG, "Reporting focus gain, without startInput");
      mService.startInputOrWindowGainedFocus(
        StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient,
        rootView.getWindowToken(), startInputFlags, softInputMode, windowFlags,
        null, null, 0 /* missingMethodFlags */,
        rootView.getContext().getApplicationInfo().targetSdkVersion);
    } catch (RemoteException e) {
      throw e.rethrowFromSystemServer();
    }
  }
}

// InputMethodManager.java
private boolean checkFocusNoStartInput(boolean forceNewFocus) {
  synchronized (mH) {
		// 如果即将拥有输入法焦点的控件是空的话, 说明用不到输入法, 关闭当前的输入法
    if (mNextServedView == null) {
      finishInputLocked();
      closeCurrentInput();
      return false;
    }
		// 经过层层考验, 备胎控件转正, 可以拥有输入法
    mServedView = mNextServedView;
    mServedConnecting = true;
  }
  return true;
}

// InputMethodManager.java
boolean startInputInner(@StartInputReason int startInputReason,
                        @Nullable IBinder windowGainingFocus,
                        @StartInputFlags int startInputFlags,
                        @SoftInputModeFlags int softInputMode,
                        int windowFlags) {
  // Parcelable对象, 主要记录编辑器的相关信息, 比如inputType
  // 输入法会根据这个信息显示不同的面板
  EditorInfo tba = new EditorInfo();
  tba.packageName = view.getContext().getOpPackageName();
  tba.fieldId = view.getId();
  // 通知View去创建一个InputConnection, 并将 EditorInfo 初始化
  // 后续通过这个InputConnection对目标控件进行字符操作
  // TextView的具体对象是 EditableInputConnection
  InputConnection ic = view.onCreateInputConnection(tba);

  synchronized (mH) {
    // 确认输入法相关的状态没有改变
    if (mServedView != view || !mServedConnecting) {
      return false;
    }
	// EditorInfo 对象
    if (mCurrentTextBoxAttribute == null) {
      // 如果当前IMM没有连接到输入法服务上, 就会添加这个标志位
      // 如果没有这个标志位, 说明客户端需要与输入法服务建立连接
      startInputFlags |= StartInputFlags.INITIAL_CONNECTION;
    }
    mCurrentTextBoxAttribute = tba;
    mServedConnecting = false;
    ControlledInputConnectionWrapper servedContext;
    if (ic != null) {
      // 创建一个真正与输入法服务交互的Binder对象
      // 后续输入法会通过 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);
      if (res == null) {
        // res不会为null值, 只是以防万一
        return false;
      }
      // Activity 的 Window 需要进行的变形操作
      mActivityViewToScreenMatrix = res.getActivityViewToScreenMatrix();
      // 这里就是启动输入法的结果状态了, 可能等待, 也可能失败, IMMS 并没有做相应的操作处理
      // 仅仅是判断了 InputBindResult.id 的值, 因为如果值不是 null, 那就是输入法请求成功了
      // 在 3.4 小结有对这个的说明
      if (res.id != null) {
        // 设置 InputChannel
        setInputChannelLocked(res.channel);
        mBindSequence = res.sequence;
        // 这个是输入法服务的Binder代理, 用这个和输入法服务交互
        // 在输入法服务的实现是 IInputMethodSessionWrapper
        // IInputMethodSession.aidl
        mCurMethod = res.method;
        mCurId = res.id;
      }
      switch (res.result) {
        case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
          // WMS 检查时发现客户端没有获取到焦点, 就会返回这个值, 则等待下次焦点获取时再启动输入法
          // 造成这个的原因很可能是 WMS 还没来得及将焦点转移到这个 Window 上
          mRestartOnNextWindowFocus = true;
          break;
      }
    } catch (RemoteException e) {
      Log.w(TAG, "IME died: " + mCurId, e);
    }
  }
  return true;
}

// TextView
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
  if (onCheckIsTextEditor() && isEnabled()) {
    outAttrs.inputType = getInputType();
    // 对 EditorInfo 各种标志位的设置
    ...
    outAttrs.hintText = mHint;
    outAttrs.targetInputMethodUser = mTextOperationUser;
    if (mText instanceof Editable) {
      // 这里创建 InputConnection
      InputConnection ic = new EditableInputConnection(this);
      outAttrs.initialSelStart = getSelectionStart();
      outAttrs.initialSelEnd = getSelectionEnd();
      outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
      return ic;
    }
  }
  return null;
}

// IInputMethodSession.aidl
oneway interface IInputMethodSession {
    void updateExtractedText(int token, in ExtractedText text);
    void updateSelection(int oldSelStart,
                         int oldSelEnd,
                         int newSelStart,
                         int newSelEnd,
                         int candidatesStart,
                         int candidatesEnd);
    void viewClicked(boolean focusChanged);
    void updateCursor(in Rect newCursor);
    void displayCompletions(in CompletionInfo[] completions);
    void appPrivateCommand(String action, in Bundle data);
    void toggleSoftInput(int showFlags, int hideFlags);
    void finishSession();
    void updateCursorAnchorInfo(in CursorAnchorInfo cursorAnchorInfo);
    void notifyImeHidden();
}

客户端启动输入法这个过程不算复杂, 主要分为三个部分:

  1. 一部分用于焦点核查
  2. 一部分做连接准备
  3. 一部分与 IMMS 交互

焦点核查主要是因为app 的 Window 窗口和 View 很多, 但只有一个 IMM 实例, 为了保证正确性, 必须走核查逻辑.

连接准备主要准备将输入控件的信息, 接口等对象传递给输入法服务, 以保证正常的和输入法服务交互

IMMS 交互就是请求输入法了, 会根据返回的输入法绑定服务做下一步操作, 可能等待, 也可能准备变换Activity Window, 准备接收输入事件

3.2 控制端开始处理输入法请求

IMMS 得到 IMM 的请求之后就开始准备弹出输入法. 在弹出输入法之前会校验很多的条件, 一旦条件不满足, 尤其是windowToken令牌, 没有这个令牌, 就直接打回了.

整体输入法展示的控制流程比较复杂, 到处是IPC调用, 穿梭于输入法客户端和输入法服务之间.

这里仅分析了第一启动绑定输入法的情况, 对于绑定输入法再启动的情况看源代码即可, 与第一次某些操作类似.

输入法启动之控制端处理请求

以下源码从 android 10.0 摘抄出来, 无用代码已删除.

// InputMethodManagerService.java
public InputBindResult startInputOrWindowGainedFocus(
                        @StartInputReason int startInputReason,
                        IInputMethodClient client,
                        IBinder windowToken,
                        @StartInputFlags int startInputFlags,
                        @SoftInputModeFlags int softInputMode,
                        int windowFlags,
                        @Nullable EditorInfo attribute,
                        IInputContext inputContext,
                        @MissingMethodFlags int missingMethods,
                        int unverifiedTargetSdkVersion) {
  // 首先校验windowToken, 如果为null, 就直接打回
  if (windowToken == null) {
    Slog.e(TAG, "windowToken cannot be null.");
    return InputBindResult.NULL;
  }
  final InputBindResult result;
  synchronized (mMethodMap) {
    final long ident = Binder.clearCallingIdentity();
    try {
      result = startInputOrWindowGainedFocusInternalLocked(
                startInputReason,
                client,
                windowToken,
                startInputFlags,
                softInputMode,
                windowFlags,
                attribute,
                inputContext,
                missingMethods,
                unverifiedTargetSdkVersion,
                userId);
    } finally {
      Binder.restoreCallingIdentity(ident);
    }
  }
}

// InputMethodManagerService.java
private InputBindResult startInputOrWindowGainedFocusInternalLocked(
                          @StartInputReason int startInputReason,
                          IInputMethodClient client,
                          @NonNull IBinder windowToken,
                          @StartInputFlags int startInputFlags,
                          @SoftInputModeFlags int softInputMode,
                          int windowFlags, EditorInfo attribute,
                          IInputContext inputContext,
                          @MissingMethodFlags int missingMethods,
                          int unverifiedTargetSdkVersion,
                          @UserIdInt int userId) {
  // 这个Log很重要, 很多情况下可以根据这条Log信息判断出具体问题出现在了哪里
  if (DEBUG) {
    Slog.v(TAG, "startInputOrWindowGainedFocusInternalLocked: reason="
           + InputMethodDebug.startInputReasonToString(startInputReason)
           + " client=" + client.asBinder()
           + " inputContext=" + inputContext
           + " missingMethods="
           + InputConnectionInspector.getMissingMethodFlagsAsString(missingMethods)
           + " attribute=" + attribute
           + " startInputFlags="
           + InputMethodDebug.startInputFlagsToString(startInputFlags)
           + " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode)
           + " windowFlags=#" + Integer.toHexString(windowFlags)
           + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion);
  }
  // 这里就是从 Client 缓存 Map 中获取缓存的客户端对象
  // 在 IMM 初始化时 addClient 添加的
  final ClientState cs = mClients.get(client.asBinder());
  if (cs == null) {
    throw new IllegalArgumentException("unknown client " + client.asBinder());
  }
  // 让 WMS 去检查下这个客户端是不是真的获取到焦点了
  // 万一唬我呢
  if (!mWindowManagerInternal.isInputMethodClientFocus(cs.uid, cs.pid,
                                                       cs.selfReportedDisplayId)) {
    if (DEBUG) {
      Slog.w(TAG, "Focus gain on non-focused client " + cs.client
             + " (uid=" + cs.uid + " pid=" + cs.pid + ")");
    }
    return InputBindResult.NOT_IME_TARGET_WINDOW;
  }
  // 之前就已经启动了输入法, 获取到了输入法焦点, 那么就直接启动咯
  if (mCurFocusedWindow == windowToken) {
    if (DEBUG) {
      Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client
             + " attribute=" + attribute + ", token = " + windowToken);
    }
    // attribute 是 EditorInfo 对象
    // 启动时如果输入信息存在, 没问题, 可以直接启动输入法
    if (attribute != null) {
      // 走到这一步的其中一个原因如下:
      // 客户端第一次请求输入法后, 控制端发现没有输入法, 或者客户端没有 Session
      // 此时控制端就会打发客户端等待输入法就绪, 等就绪时控制端会通知客户端再次请求输入法
      // 通常等待这种情况向下, 返回给客户端等待的 ResultCode 是:
      // SUCCESS_WAITING_IME_BINDING 和 SUCCESS_WAITING_IME_SESSION
      // 打发客户端等待的同时, 控制端会绑定输入法服务, 获取 IME, 为客户端创建 Session 等操作
      // 等这些操作在控制端完成之后, 控制端就会通知客户端去再次请求输入法
      // 客户端再次请求输入法时, 就会走这种情况, 走到这里
      return startInputUncheckedLocked(cs, inputContext, missingMethods,
                                       attribute, startInputFlags, startInputReason);
    }
    // 如果没有输入信息存在, 那么这个客户端仅仅是获取到了焦点而已, 没有弹出输入法的场景
    return new InputBindResult(
      InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
      null, null, null, -1, null);
  }
  // 设置当前输入法焦点窗口等属性状态
  mCurFocusedWindow = windowToken;
  mCurFocusedWindowSoftInputMode = softInputMode;
  mCurFocusedWindowClient = cs;

  // 如果window允许调整大小或者是超大屏幕, 可以放得下输入法的情况下, 那就允许自动展示输入法
  // 对手机而言就是允许调整window大小
  final boolean doAutoShow =
      	(softInputMode & LayoutParams.SOFT_INPUT_MASK_ADJUST)
      		== LayoutParams.SOFT_INPUT_ADJUST_RESIZE
      || mRes.getConfiguration().isLayoutSizeAtLeast(
      		Configuration.SCREENLAYOUT_SIZE_LARGE);
  // 看看是不是文本编辑器
  final boolean isTextEditor = (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0;
	// 重头戏
  // 下面就是用于判断 softInputMode, 就是在清单文件里设置的 softInputMode
  boolean didStart = false;
  InputBindResult res = null;
  switch (softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE) {
    // SOFT_INPUT_STATE_UNSPECIFIED 输入法根据情况进行弹出或者隐藏
    case LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
      // 没有输入框或者窗口不允许改变大小的情况
      if (!isTextEditor || !doAutoShow) {
        // 在 3.1 节介绍的, 根据widnow焦点是否翻转输入法弹出行为
        // 如果需要翻转的话, 就没有必要操作了, 原本隐藏的, 继续隐藏; 原本显示的, 继续显示
        // 如果不翻转, 那么就是正常操作流程
        if (LayoutParams.mayUseInputMethod(windowFlags)) {
          if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
          // 隐藏输入法
          hideCurrentInputLocked(InputMethodManager.HIDE_NOT_ALWAYS, null);
          // 如果在执行这个方法的过程中, 输入法焦点变化了, 就将输入法解绑, 为后续的输入法绑定腾地方
          // 如果没有改变, 可能只是要求隐藏输入法而已, 后续可能还会要求弹出继续输入, 保留下来复用
          if (cs.selfReportedDisplayId != mCurTokenDisplayId) {
            unbindCurrentMethodLocked();
          }
        }
      // 如果有文本编辑器, 允许窗口改变大小
      // 并且还是从其他window跳转过来的情况
      } else if (isTextEditor
                 && doAutoShow
                 && (softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                 			!= 0) {
        // 这条Log在调试时也挺重要
        if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
        // 如果编辑信息不是空的话, 就启动输入法, 与客户端绑定吧
        if (attribute != null) {
          res = startInputUncheckedLocked(cs,
                                          inputContext,
                                          missingMethods,
                                          attribute,
                                          startInputFlags,
                                          startInputReason);
          didStart = true;
        }
        // 然后尝试展示输入法界面
        showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
      }
      break;
    // SOFT_INPUT_STATE_UNCHANGED 和原生代码注释一样, 啥都不干
    case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
      // Do nothing.
      break;
    // SOFT_INPUT_STATE_HIDDEN 根据情况隐藏输入法
    case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
      // 这里其实有一个Bug, 操作步骤如下:
      // 1. 进入一个 SOFT_INPUT_STATE_HIDDEN 并且有输入框的界面(微信聊天界面)
      // 2. 回到桌面, 打开短信, 新建短信, 弹出输入法
      // 3. 有一个(微信)消息发来, 并在通知栏显示
      // 4. 点击通知进入第一步的界面(微信聊天界面)
      // 5. 输入框没有焦点, 输入法没有隐藏, 输入法打字没有上屏

      // 设置 SOFT_INPUT_IS_FORWARD_NAVIGATION 标志位的情况如下:
      // 1. 新打开一个Activity
      // 2. 新打开一个Task
      // 3. 将Task移到最前面
      // 目前就只有这三种情况会设置 SOFT_INPUT_IS_FORWARD_NAVIGATION 标志位
      // 决定设置这个标志位的地方在 DisplayContent.isNextTransitionForward() 方法

      // 就是说如果是新打开的Activity或者将Task任务栈移到最前面时就隐藏输入法
      if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
        // 这条Log在调试时也挺重要
        if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
        hideCurrentInputLocked(0, null);
      }
      break;
    // SOFT_INPUT_STATE_ALWAYS_HIDDEN 不管什么情况, 始终隐藏
    case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
      // 这条Log在调试时也挺重要
      if (DEBUG) Slog.v(TAG, "Window asks to hide input");
      hideCurrentInputLocked(0, null);
      break;
    // SOFT_INPUT_STATE_VISIBLE 根据情况显示输入法
    case LayoutParams.SOFT_INPUT_STATE_VISIBLE:
      // SOFT_INPUT_IS_FORWARD_NAVIGATION 看 SOFT_INPUT_STATE_HIDDEN 的介绍
      // 如果是新打开的Activity或者将Task任务栈移到最前面时就开启输入法
      if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
        // 这条Log在调试时也挺重要
        if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
        // 这个就是检查目标控件是否有开启输入法的条件
        // 开启的条件是:
        // 1. 兼容性, 如果 targetSdkVersion 小于 P (28)
        // 2. 目标控件有焦点
        // 3. 目标控件是文本编辑器
        if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
          		unverifiedTargetSdkVersion, startInputFlags)) {
          // 有编辑信息的话就启动咯
          if (attribute != null) {
            res = startInputUncheckedLocked(cs,
                                            inputContext,
                                            missingMethods,
                                            attribute,
                                            startInputFlags,
                                            startInputReason);
            didStart = true;
          }
          showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
        } else {
          Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
                 + " there is no focused view that also returns true from"
                 + " View#onCheckIsTextEditor()");
        }
      }
      break;
    // SOFT_INPUT_STATE_ALWAYS_VISIBLE 字面意, 总是显示输入法
    // 和 SOFT_INPUT_STATE_VISIBLE 类似
    // 只是不需要检查 SOFT_INPUT_IS_FORWARD_NAVIGATION 标志位了而已
    case LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
      // 这条Log在调试时也挺重要
      if (DEBUG) Slog.v(TAG, "Window asks to always show input");
      if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
        		unverifiedTargetSdkVersion, startInputFlags)) {
        if (attribute != null) {
          res = startInputUncheckedLocked(cs,
                                          inputContext,
                                          missingMethods,
                                          attribute,
                                          startInputFlags,
                                          startInputReason);
          didStart = true;
        }
        showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
      } else {
        Slog.e(TAG, "SOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored because"
               + " there is no focused view that also returns true from"
               + " View#onCheckIsTextEditor()");
      }
      break;
  }
  // 如果之前步骤没能启动输入法, 这里就再检查下
  // 有可能后续这个界面可能会使用输入法, 只是暂时不用显示出来而已
  // 为了加快显示输入法的效率, 就为客户端启动输入法, 但是不显示
  // 判断是否显示输入法的条件如下:
  // 1. 刚才的步骤没有启动过
  // 2. 编辑信息不是空的
  // 3. 是一个文本编辑器
  if (!didStart) {
    if (attribute != null) {
      if (!DebugFlags.FLAG_OPTIMIZE_START_INPUT.value()
          || (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0) {
        res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
                                        startInputFlags, startInputReason);
      } else {
        res = InputBindResult.NO_EDITOR;
      }
    } else {
      res = InputBindResult.NULL_EDITOR_INFO;
    }
  }
}

// InputMethodUtils.java
static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion,
                                                  @StartInputFlags int startInputFlags) {
  // 兼容P以下的app版本
  if (targetSdkVersion < Build.VERSION_CODES.P) {
    // for compatibility.
    return true;
  }
  // 是否有了焦点
  if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) == 0) {
    return false;
  }
  // 是否为文本编辑器
  if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0) {
    return false;
  }
  return true;
}

// InputMethodManagerService.java
// 这里主要进行输入法服务的绑定, 就是绑定 InputMethodService
InputBindResult startInputUncheckedLocked(@NonNull ClientState cs,
                                          IInputContext inputContext,
                                          @MissingMethodFlags int missingMethods,
                                          @NonNull EditorInfo attribute,
                                          @StartInputFlags int startInputFlags,
                                          @StartInputReason int startInputReason) {
  // 没有输入法
  if (mCurMethodId == null) {
    return InputBindResult.NO_IME;
  }
  // 系统还没准备好
  if (!mSystemReady) {
    return new InputBindResult(
      InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
      null, null, mCurMethodId, mCurSeq, null);
  }
	// 给IMMS赋上当前输入法的状态值
  mCurClient = cs;
  mCurInputContext = inputContext;
  mCurActivityViewToScreenMatrix =
    getActivityViewToScreenMatrixLocked(cs.selfReportedDisplayId, displayIdToShowIme);
  mCurInputContextMissingMethods = missingMethods;
  mCurAttribute = attribute;
	// 这里是判断之前是否已经绑定过输入法了
  if (mCurId != null && mCurId.equals(mCurMethodId)
      && displayIdToShowIme == mCurTokenDisplayId) {
    // 已经有绑定了输入法服务的动作了
    if (mHaveConnection) {
      if (mCurMethod != null) {
        // 此时等待创建一个输入法对话就好了
        requestClientSessionLocked(cs);
        return new InputBindResult(
          InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
          null, null, mCurId, mCurSeq, null);
      } else if (SystemClock.uptimeMillis()
                 < (mLastBindTime+TIME_TO_RECONNECT)) {
        // TIME_TO_RECONNECT = 3000
        // 如果距离上次绑定在超时时间内, 那就继续等待绑定
        return new InputBindResult(
          InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
          null, null, mCurId, mCurSeq, null);
      } else {
        // 绑定输入法超时了, 就需要重新进行绑定
        EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME,
                            mCurMethodId, SystemClock.uptimeMillis()-mLastBindTime, 0);
      }
    }
  }

  // 如果走到这里, 说明输入法需要(重新)绑定
  // 从自己的输入法列表中获取安装的输入法服务信息
  InputMethodInfo info = mMethodMap.get(mCurMethodId);
  if (info == null) {
    throw new IllegalArgumentException("Unknown id: " + mCurMethodId);
  }
	// 可能之前绑定过了, 但是超时了, 需要先解绑, 清理环境
  unbindCurrentMethodLocked();

  // 开始绑定准备
  mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE);
  mCurIntent.setComponent(info.getComponent());
  ...

  // 这里就是绑定输入法服务的地方
  // 这个this是ServiceConnection, 后续会有介绍
  if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
    mHaveConnection = true;
    mCurId = info.getId();
    mCurToken = new Binder();
    try {
      if (DEBUG) {
        // 绑定输入法的时候会弹出这个log出来
        Slog.v(TAG, "Adding window token: " + mCurToken + " for display: "
               + mCurTokenDisplayId);
      }
      // 告知WMS,这个token令牌是用于输入法展示的
      mIWindowManager.addWindowToken(mCurToken, LayoutParams.TYPE_INPUT_METHOD,
                                     mCurTokenDisplayId);
    } catch (RemoteException e) {
    }
    // 返回一个正在等待绑定的状态结果
    // 此时客户端第一次请求输入法的操作就完成了, 客户端将要做的就是等待控制端通知再次绑定
    // 控制端通知客户端再次绑定的时机是:
    // 1. 输入法服务 IMS 绑定完成
    // 2. 为客户端准备的 Session 创建完成并激活
    // 3. 开启输入法完成, 也就是让 IME 准备就绪
    // 只有这三步都准备完成后, 才会通知客户端去再次绑定
    // 再次绑定的时候就会直接走 startInputUncheckedLocked 方法启动了
    // 不会再走绑定输入法, 创建 Session 等操作了
    return new InputBindResult(
      InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
      null, null, mCurId, mCurSeq, null);
  }
  // 如果走到了这里, 说明绑定service失败了
  mCurIntent = null;
  Slog.w(TAG, "Failure connecting to input method service: " + mCurIntent);
  return InputBindResult.IME_NOT_CONNECTED;
}

// InputMethodManagerService.java
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;
      }
      // 绑定输入法服务成功
      if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
      // Dispatch display id for InputMethodService to update context display.
      // 然后初始化输入法服务, 主要是给输入法一个token令牌
      executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(MSG_INITIALIZE_IME,
                                                                mCurTokenDisplayId,
                                                                mCurMethod,
                                                                mCurToken));
      if (mCurClient != null) {
        // 如果此时有客户端等待输入法, 那就创建个Session, 准备展示输入法
        clearClientSessionLocked(mCurClient);
        requestClientSessionLocked(mCurClient);
      }
    }
  }
}

// InputMethodManagerService.java
// 走到这里可能原因有两个:
// 1. 已经有绑定输入法的动作了, 并且 IME 对象也已经有了, 客户端要求显示输入法, 那就给他创建个
// 2. 绑定输入法服务成功之后, 在 onServiceConnected 里面调用, 调用的条件是有客户端等待输入法启动
void requestClientSessionLocked(ClientState cs) {
  // 如果说这个client还没有和输入法进行过绑定, 那么就要进行一次对话绑定操作
  // 如果有 Session 了, 直接复用就好了
  if (!cs.sessionRequested) {
    // 这个Log也可以协助分析
    if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs);
    // 这里是创建一个InputChannel数组, 长度为2
    // 相对于客户端来说, 第一个用于发布输入事件, 第二个用于消费输入事件
    InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString());
    cs.sessionRequested = true;
    executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO(
      MSG_CREATE_SESSION, mCurMethod, channels[1],
      // MethodCallback 是一个Binder对象, aidl接口, IInputSessionCallback
      // 用于接收输入法创建Session后的回调
      new MethodCallback(this, mCurMethod, channels[0])));
  }
}

// InputMethodManagerService.java
case MSG_CREATE_SESSION: {
  args = (SomeArgs)msg.obj;
  IInputMethod method = (IInputMethod)args.arg1; // mCurMethod
  InputChannel channel = (InputChannel)args.arg2; // channels[1] 客户端消费通道
  try {
    // 告知输入法, 需要创建一个输入法对话
    method.createSession(channel, (IInputSessionCallback)args.arg3);
  } catch (RemoteException e) {
  } finally {
    // 关闭客户端发布通道
    if (channel != null && Binder.isProxy(method)) {
      channel.dispose();
    }
  }
  args.recycle();
  return true;
}

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

// InputMethodManagerService.java
private static final class MethodCallback extends IInputSessionCallback.Stub {
  @Override
  public void sessionCreated(IInputMethodSession session) {
      mParentIMMS.onSessionCreated(mMethod, session, mChannel);
  }
}

// InputMethodManagerService.java
void onSessionCreated(IInputMethod method,
                      IInputMethodSession session,
                      InputChannel channel) {
  synchronized (mMethodMap) {
    // 检查下输入法相关的状态是否正确, 如果不正确, 这个session就被抛弃
    // 同时事件通道也被关闭
    if (mCurMethod != null
        && method != null
        && mCurMethod.asBinder() == method.asBinder()) {
      if (mCurClient != null) {
        // 清理客户端旧的session状态
        clearClientSessionLocked(mCurClient);
        // SessionState 保存了输入法相关的Binder对象, 仅此而已
        // ClientState IInputMethod IInputMethodSession InputChannel
        mCurClient.curSession = new SessionState(mCurClient, method, session, channel);
        // 然后准备并展示输入法
        InputBindResult res = attachNewInputLocked(
          StartInputReason.SESSION_CREATED_BY_IME, true);
        // res.method 就是 IInputMethodSession 对象
        if (res.method != null) {
          // 如果session对象存在, 就通知客户端进行绑定
          executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO(
            MSG_BIND_CLIENT, mCurClient.client, res));
        }
        return;
      }
    }
  }
  channel.dispose();
}

// InputMethodManagerService.java
case MSG_BIND_CLIENT: {
  args = (SomeArgs)msg.obj;
  IInputMethodClient client = (IInputMethodClient)args.arg1;
  InputBindResult res = (InputBindResult)args.arg2;
  try {
    // 通知客户端绑定的结果
    client.onBindMethod(res);
  } catch (RemoteException e) {
    // 有可能输入法还没绑定完, 客户端已经退出了
    Slog.w(TAG, "Client died receiving input method " + args.arg2);
  } finally {
    if (res.channel != null && Binder.isProxy(client)) {
      res.channel.dispose();
    }
  }
  args.recycle();
  return true;
}

// InputMethodManagerService.java
InputBindResult attachNewInputLocked(@StartInputReason int startInputReason,
                                     boolean initial) {

  // 如果之前没有将客户端绑定到 IME, 则需要先进行绑定操作
  if (!mBoundToMethod) {
    executeOrSendMessage(mCurMethod,
                         mCaller.obtainMessageOO(
                                                  MSG_BIND_INPUT,
                           												mCurMethod,
                           												mCurClient.binding));
    mBoundToMethod = true;
  }

  final SessionState session = mCurClient.curSession;
  // 这里就是告知输入法服务你要准备展示了, 仅仅是准备
  // IME 那边就是将 Connection 切换为客户端的
  executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(
    MSG_START_INPUT,
    mCurInputContextMissingMethods,
    initial ? 0 : 1 /* restarting */,
    startInputToken,
    session,
    mCurInputContext,
    mCurAttribute));
  if (mShowRequested) {
    // 这条Log也很有用, 往往是输入法已经绑定, 但是在等待Session创建成功时才会打印
    if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
    // 这里就是准备输入法进行展示
    showCurrentInputLocked(getAppShowFlags(), null);
  }
  // 如果能走到这里, 说明 IME, Session 都具备了, 就直接返回成功给客户端使用就可以了
  return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
                             session.session,
                             (session.channel != null ? session.channel.dup() : null),
                             mCurId, mCurSeq, mCurActivityViewToScreenMatrix);
}

// InputMethodManagerService.java
case MSG_BIND_INPUT:
args = (SomeArgs)msg.obj;
try {
  // 这里通过 IInputMethod 告知输入法服务, 需要将客户端绑定到 IME
  ((IInputMethod)args.arg1).bindInput((InputBinding)args.arg2);
} catch (RemoteException e) {
}
args.recycle();

// InputMethodManagerService.java
case MSG_START_INPUT: {
  // 这里通过 IInputMethod 告知输入法服务, 现在使用这个session进行工作
  setEnabledSessionInMainThread(session);
  // 这里通过 IInputMethod 告知输入法准备展示
  session.method.startInput(startInputToken, inputContext, missingMethods,
                            editorInfo, restarting, session.client.shouldPreRenderIme);
  return true;
}

// InputMethodManagerService.java
void setEnabledSessionInMainThread(SessionState session) {
    if (mEnabledSession != session) {
        if (mEnabledSession != null && mEnabledSession.session != null) {
            try {
                // 这条Log挺重要
                if (DEBUG) Slog.v(TAG, "Disabling: " + mEnabledSession);
                // 告知输入法, 之前的session不使用了
                mEnabledSession.method.setSessionEnabled(mEnabledSession.session, false);
            } catch (RemoteException e) {
            }
        }
        mEnabledSession = session;
        if (mEnabledSession != null && mEnabledSession.session != null) {
            try {
                // 这条Log特别重要, 一般调用这个Log之后, 说明 Session 激活
              	// 通常, 这条 Log 之后, 输入法会真正显示出来, 不是绝对的
              	// 因为当前方法调用之后, 会紧跟着 startInput 和 showSoftInput
                if (DEBUG) Slog.v(TAG, "Enabling: " + mEnabledSession);
                mEnabledSession.method.setSessionEnabled(mEnabledSession.session, true);
            } catch (RemoteException e) {
            }
        }
    }
}

// InputMethodManagerService.java
boolean showCurrentInputLocked(int flags, ResultReceiver resultReceiver) {
  mShowRequested = true;
	// SHOW_FORCED 和 SHOW_IMPLICIT 标志位在这里开始起作用
  // 如果强制使用这个标志位会导致输入法不能正常隐藏
  // 会影响到 hideCurrentInputLocked() 方法
  if ((flags&InputMethodManager.SHOW_FORCED) != 0) {
    mShowExplicitlyRequested = true;
    mShowForced = true;
  } else if ((flags&InputMethodManager.SHOW_IMPLICIT) == 0) {
    mShowExplicitlyRequested = true;
  }

  boolean res = false;
  if (mCurMethod != null) {
    if (DEBUG) Slog.d(TAG, "showCurrentInputLocked: mCurToken=" + mCurToken);
    // 这里就是告知输入法进行展示
    executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(
      MSG_SHOW_SOFT_INPUT, getImeShowFlags(), mCurMethod,
      resultReceiver));
    mInputShown = true;
    // 到了这里也只是通知了输入法进行展示, 还未处于可见状态
    // 这里还有一个可见性绑定, 只有这样绑定了之后输入法才算是真正的展示出来
    if (mHaveConnection && !mVisibleBound) {
      // 这里只不过是再次绑定了输入法服务, 但参数不同而已, 用于调整可见性
      // mVisibleConnection 监听binder有没有死亡
      bindCurrentInputMethodServiceLocked(
        mCurIntent, mVisibleConnection, IME_VISIBLE_BIND_FLAGS);
      mVisibleBound = true;
    }
    res = true;
  }
  return res;
}

// InputMethodManagerService.java
case MSG_SHOW_SOFT_INPUT:
  args = (SomeArgs)msg.obj;
	// 这条Log很有用, 可以用来判断输入法有没有到可以展示的地步
  if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".showSoftInput("
                    + msg.arg1 + ", " + args.arg2 + ")");
	// 就是利用 IInputMethod 通知输入法进行展示
  ((IInputMethod)args.arg1).showSoftInput(msg.arg1, (ResultReceiver)args.arg2);

// InputMethodManagerService.java
final ServiceConnection mVisibleConnection = new ServiceConnection() {
  @Override public void onBindingDied(ComponentName name) {
    synchronized (mMethodMap) {
      // 仅仅监听死亡状态, 以更新相关的标志位
      if (mVisibleBound) {
        mContext.unbindService(mVisibleConnection);
        mVisibleBound = false;
      }
    }
  }

  @Override public void onServiceConnected(ComponentName name, IBinder service) {
  }

  @Override public void onServiceDisconnected(ComponentName name) {
  }
};

// InputMethodManagerService.java
private boolean bindCurrentInputMethodServiceLocked(
  Intent service, ServiceConnection conn, int flags) {
  if (service == null || conn == null) {
    Slog.e(TAG, "--- bind failed: service = " + service + ", conn = " + conn);
    return false;
  }
  // 没骗你吧, 就是一个绑定服务的操作
  return mContext.bindServiceAsUser(service, conn, flags,
                                    new UserHandle(mSettings.getCurrentUserId()));
}

// InputMethodManagerService.java
boolean hideCurrentInputLocked(int flags, ResultReceiver resultReceiver) {
  if ((flags&InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
      && (mShowExplicitlyRequested || mShowForced)) {
    // 这条Log在分析输入法不能正常隐藏时很有用
    if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
    return false;
  }
  // 如果是因为焦点变换导致输入法隐藏, 通常会有 HIDE_NOT_ALWAYS 这个标志位
  // 如果展示输入法时设置了 SHOW_FORCE 标志位, 那么输入法就不能正常隐藏了
  // 有些App喜欢设置这个标志位, 导致回到桌面时输入法不能正常隐藏掉
  if (mShowForced && (flags&InputMethodManager.HIDE_NOT_ALWAYS) != 0) {
    // 这条Log在分析输入法不能正常隐藏时特别有用
    if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");
    return false;
  }
  final boolean shouldHideSoftInput = (mCurMethod != null)
    && (mInputShown ||(mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
  boolean res;
  if (shouldHideSoftInput) {
    // 通知输入法服务隐藏输入法
    executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
      MSG_HIDE_SOFT_INPUT, mCurMethod, resultReceiver));
    res = true;
  } else {
    res = false;
  }
  // 还会解绑输入法服务
  if (mHaveConnection && mVisibleBound) {
    mContext.unbindService(mVisibleConnection);
    mVisibleBound = false;
  }
  return res;
}

// InputMethodManager.java
void closeCurrentInput() {
  try {
    // 焦点变换需要隐藏输入法时, 这里设置了 HIDE_NOT_ALWAYS 标志位
    mService.hideSoftInput(mClient, HIDE_NOT_ALWAYS, null);
  } catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
  }
}

整体的流程较长, 有些坑还是存在的, 并且流程中包含了很多IPC调用, 开发时可能一不留神就会造成多线程问题.

最主要的方法是 startInputOrWindowGainedFocus 函数和参数列表

// InputMethodManagerService.java
public InputBindResult startInputOrWindowGainedFocus(
    @StartInputReason int startInputReason, // 启动原因
    IInputMethodClient client, // 客户端, 主要指 IMM
    IBinder windowToken, // token令牌, 说明该view已经添加到WMS中
    @StartInputFlags int startInputFlags, // 启动输入法的标志位
    @SoftInputModeFlags int softInputMode, // Activity中输入法启动模式
    int windowFlags, // 窗口的标志位
    @Nullable EditorInfo attribute, // 编辑器信息, 比如输入法框的 inputType
    IInputContext inputContext, // 输入控件上下文信息, IME 通过它操作目标控件
    @MissingMethodFlags int missingMethods, // 限制输入法功能的标志位
    int unverifiedTargetSdkVersion) { // 目标 app targetVersion, 用于兼容性操作
}

int startInputReason

frameworks/base/core/java/com/android/internal/inputmethod/StartInputReason.java

输入法启动原因

// 这是一个注解
public @interface StartInputReason {
    int UNSPECIFIED = 0; // 未指定原因
    int WINDOW_FOCUS_GAIN = 1; // 窗口获取到焦点
    int WINDOW_FOCUS_GAIN_REPORT_ONLY = 2; // 窗口仅仅获取到焦点, 但不启动输入法
    int APP_CALLED_RESTART_INPUT_API = 3; // app 主动请求重新启动输入法
    int CHECK_FOCUS = 4; // 检查焦点
    int BOUND_TO_IMMS = 5; // 绑定到IMMS
    int UNBOUND_FROM_IMMS = 6; // 解绑IMMS
    int ACTIVATED_BY_IMMS = 7; // 被IMMS 设置为激活状态
    int DEACTIVATED_BY_IMMS = 8; // 被IMMS设置为未激活状态
    int SESSION_CREATED_BY_IME = 9; // 与输入法建立了Session对话
}

int softInputMode

frameworks/base/core/java/android/view/WindowManager.java

Activity中输入法启动模式

public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {

    // 下面这 6 个以 SOFT_INPUT_STATE 开头, 控制 Activity 启动时输入法应该是什么状态
    // 其中 SOFT_INPUT_STATE_HIDDEN 和 SOFT_INPUT_STATE_VISIBLE 配合
    // SOFT_INPUT_IS_FORWARD_NAVIGATION 标志位使用

    public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;
    public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
    public static final int SOFT_INPUT_STATE_HIDDEN = 2;
    public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
    public static final int SOFT_INPUT_STATE_VISIBLE = 4;
    public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;

    // 配合 SOFT_INPUT_STATE_HIDDEN 和 SOFT_INPUT_STATE_VISIBLE 使用
    // 在前面代码中已经有了描述
    public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100;

    // 下面这 4 个以 SOFT_INPUT_ADJUST 开头, 在输入法弹出时, Activity 界面应该怎么变化
    // 这些标志位在 DisplayPolicy 里处理(P 上在 PhoneWindowManager 中处理)
    // 主要工作属于 WMS 框架

    // 效果和 SOFT_INPUT_ADJUST_PAN 差不多, 属于 window 范畴, 具体区别没有细究
    public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
    // 调整Window大小为输入法腾空间, resize
    // 这个标志位还会影响 SOFT_INPUT_STATE_UNSPECIFIED 标志位效果
  	// 影响 startInputOrWindowGainedFocusInternalLocked 方法内的 doAutoShow 变量
    public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
    // 界面上面的控件被顶到屏幕外
    // 输入法会覆盖界面一部分, 调整界面保证输入焦点控件不被输入法覆盖
    public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
    // 什么操作也没有, 输入法直接覆盖到界面之上, 不管焦点控件是否会被输入法覆盖
    public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
}

int missingMethods

frameworks/base/core/java/android/view/inputmethod/InputConnectionInspector.java

禁止 IME 操作某些功能的标志

public @interface MissingMethodFlags {
    // 不允许调用 InputConnection#getSelectedText(int)
    int GET_SELECTED_TEXT = 1 << 0;
    // 不允许调用 InputConnection#setComposingRegion(int, int)
    int SET_COMPOSING_REGION = 1 << 1;
    // 不允许调用 InputConnection#commitCorrection(CorrectionInfo)
    int COMMIT_CORRECTION = 1 << 2;
    // 不允许调用 InputConnection#requestCursorUpdates(int)
    int REQUEST_CURSOR_UPDATES = 1 << 3;
    // 不允许调用 InputConnection#deleteSurroundingTextInCodePoints(int, int)
    int DELETE_SURROUNDING_TEXT_IN_CODE_POINTS = 1 << 4;
    // 不允许调用 InputConnection#deleteSurroundingTextInCodePoints(int, int)
    int GET_HANDLER = 1 << 5;
    // 不允许调用 InputConnection#closeConnection()
    int CLOSE_CONNECTION = 1 << 6;
    // 不允许调用 InputConnection#commitContent(InputContentInfo, int, Bundle)
    int COMMIT_CONTENT = 1 << 7;
}

EditorInfo attribute

frameworks/base/core/java/android/view/inputmethod/EditorInfo.java

输入框信息, 比较常用的就是 inputType, 比如密码, 数字等, 方便 IME 根据需要调整自己的界面

InputBindResult.ResultCode

frameworks/base/core/java/com/android/internal/view/InputBindResult.java

请求输入法时, 控制端返回的结果. 一般没什么用. 但是可以告诉我们具体是出现了什么情况, 对排查问题有帮助.

// InputBindResult.java
public @interface ResultCode {
  // 请求输入法成功
  int SUCCESS_WITH_IME_SESSION = 0;
  // 输入法服务已经启动, 等待 IME 创建输入法 Session
  // 后续会通知客户端再次请求输入法
  int SUCCESS_WAITING_IME_SESSION = 1;
  // 输入法服务还未启动, 等待输入法服务启动
  // 后续会通知客户端再次请求输入法
  int SUCCESS_WAITING_IME_BINDING = 2;
  // 检测到客户端仅仅是获取到了焦点, 并没有输入控件或者叫文本编辑器
  int SUCCESS_REPORT_WINDOW_FOCUS_ONLY = 3;
  // 未知错误, 一般是指客户端没有 windowToken
  int ERROR_NULL = 4;
  // 没有配置任何输入法
  int ERROR_NO_IME = 5;
  // 无效的包, 具体的没有细究
  int ERROR_INVALID_PACKAGE_NAME = 6;
  // 系统还没有准备好, 关键的方法在 systemRunning
  int ERROR_SYSTEM_NOT_READY = 7;
  // 输入法服务绑定失败
  int ERROR_IME_NOT_CONNECTED = 8;
  // 无效用户, 具体的没有细究
  int ERROR_INVALID_USER = 9;
  // 没有编辑器信息, EditorInfo 对象为 null
  int ERROR_NULL_EDITOR_INFO = 10;
  // 当前焦点不对, 当前调用的应用并没有获取到系统焦点
  int ERROR_NOT_IME_TARGET_WINDOW = 11;
  // 不是编辑器, 传入的 startInputFlags 没有 StartInputFlags.IS_TEXT_EDITOR 标志位
  int ERROR_NO_EDITOR = 12;
  // 屏幕 id 不匹配
  int ERROR_DISPLAY_ID_MISMATCH = 13;
  // 无效的屏幕 id
  int ERROR_INVALID_DISPLAY_ID = 14;
  // 多客户端情况下的错误返回值, IMMS 用不到
  // MultiClientInputMethodManagerService 使用
  int ERROR_INVALID_CLIENT = 15;
}

请求输入法的返回值流程也挺多变, 正常情况下返回的值应该为:

SUCCESS_WITH_IME_SESSION, SUCCESS_WAITING_IME_SESSION 或者 SUCCESS_WAITING_IME_BINDING

如图:

输入法启动之返回值流程

3.3 服务端接收绑定输入法请求

IME 接收输入法与目标的绑定操作. 绑定操作主要是创建 IInputMethodSession, 预渲染界面

主要的流程如下:

  1. IMMS 绑定输入法服务, 并得到 IInputMethod
  2. IMMS 使用 IInputMethod 初始化 IME
  3. IMMS 使用 IInputMethod 将客户端与 IME 绑定, 将 IInputContext 放到 InputBinding 内传递给 IME
  4. IMMS 使用 IInputMethod 为客户端创建一个 IInputMethodSession, 并将客户端的 InputChannel 递给 IME
  5. IMMS 使用 IInputMethod 激活 IInputMethodSession
  6. IMMS 使用 IInputMethod 启动 IME, 将 IInputContext 传递给 IME, IME 进行界面预渲染, 并添加到 WMS 中
  7. IMMS 使用 IInputMethod 告知 IME 展示界面, IME 通知 IMMS 展示界面
  8. IMMS 使用 IInputMethodClient 告知客户端输入法已经就绪, 客户端会再次请求绑定输入法服务
  9. 客户端使用 IInputMethodSession 告知 IME 相关操作, IME 利用 IInputContext 传递输入给客户端

步骤1和步骤2已经在前面解析过, 不再赘述. 步骤9会在 3.4 小结讲述, 这里从第三步看 IME 的主要过程.

输入法启动之服务端响应请求

以下源码从 android 10.0 摘抄出来, 无用代码已删除.

// ----------------------------------------------------------------------
// 这一段是绑定 IME 的过程

// IInputMethodWrapper.java
public void bindInput(InputBinding binding) {
  // InputBinding 里面有一个 mConnectionToken 变量, 其实就是 IInputContext 的 Binder
  // InputBinding 里面有一个 mConnection 变量, 此时是 null
  // 为了方便调用, 就将 mConnectionToken 封装化为 InputConnection
  // 为了方便理解, 可以认为 InputConnection 和 IInputContext 是一个东西, 功能一样, 但实际不是
  InputConnection ic = new InputConnectionWrapper(
      mTarget,
      IInputContext.Stub.asInterface(binding.getConnectionToken()),
      missingMethodFlags,
      mIsUnbindIssued);
  InputBinding nu = new InputBinding(ic, binding);
  mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu));
}

// IInputMethodWrapper.java
case DO_SET_INPUT_CONTEXT: {
  inputMethod.bindInput((InputBinding)msg.obj);
  return;
}

// InputMethodService.java
public void bindInput(InputBinding binding) {
  // 其实就是记录下当前的客户端信息而已
  mInputBinding = binding;
  mInputConnection = binding.getConnection();
  // 这条Log挺重要, 可以判断客户端是否有效的绑定到了 IME 上
  if (DEBUG) Log.v(TAG, "bindInput(): binding=" + binding
                   + " ic=" + mInputConnection);
}

// ----------------------------------------------------------------------
// 这一段是创建 Session 的过程

// IInputMethodWrapper.java
// IMMS 对输入法的所有操作都是通过这个类将binder调用转接到 IME 上, 并且使所有调用运行在主线程
public void createSession(InputChannel channel, IInputSessionCallback callback) {
  mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_SESSION,
                                                       channel, callback));
}

// IInputMethodWrapper.java
case DO_CREATE_SESSION: {
  SomeArgs args = (SomeArgs)msg.obj;
  inputMethod.createSession(new InputMethodSessionCallbackWrapper(
    	mContext,
    	(InputChannel)args.arg1,
    	(IInputSessionCallback)args.arg2));
  args.recycle();
  return;
}

// AbstractInputMethodService.java
// AbstractInputMethodService.AbstractInputMethodImpl
public void createSession(SessionCallback callback) {
  callback.sessionCreated(onCreateInputMethodSessionInterface());
}

// InputmethodService.java
public AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface() {
  // InputMethodSessionImpl extends AbstractInputMethodSessionImpl
  // AbstractInputMethodSessionImpl implements InputMethodSession
  // 最后这个对象会被 IInputMethodSessionWrapper 封装
  // IInputMethodSessionWrapper 是一个Binder对象, IInputMethodSession.aidl
  return new InputMethodSessionImpl();
}

// IInputMethodSession.aidl
oneway interface IInputMethodSession {
    void updateExtractedText(int token, in ExtractedText text);
    void viewClicked(boolean focusChanged);
    void updateCursor(in Rect newCursor);
    void displayCompletions(in CompletionInfo[] completions);
    void appPrivateCommand(String action, in Bundle data);
    void toggleSoftInput(int showFlags, int hideFlags);
    void finishSession();
    void updateCursorAnchorInfo(in CursorAnchorInfo cursorAnchorInfo);
    void notifyImeHidden();
    void updateSelection(int oldSelStart,
                         int oldSelEnd,
            						 int newSelStart,
                         int newSelEnd,
            						 int candidatesStart,
                         int candidatesEnd);
}

// ----------------------------------------------------------------------
// 这一段是激活 Session 的过程

// IInputMethodWrapper.java
public void setSessionEnabled(IInputMethodSession session, boolean enabled) {
  try {
    mCaller.executeOrSendMessage(mCaller.obtainMessageIO(
      DO_SET_SESSION_ENABLED, enabled ? 1 : 0, ls));
  } catch (ClassCastException e) {
    Log.w(TAG, "Incoming session not of correct type: " + session, e);
  }
}

// IInputMethodWrapper.java
case DO_SET_SESSION_ENABLED:
		inputMethod.setSessionEnabled((InputMethodSession)msg.obj,
                              msg.arg1 != 0);
return;

// AbstractInputMethodService.java
// AbstractInputMethodService.AbstractInputMethodImpl
public void setSessionEnabled(InputMethodSession session, boolean enabled) {
  ((AbstractInputMethodSessionImpl)session).setEnabled(enabled);
}

// AbstractInputMethodService.java
// AbstractInputMethodService.AbstractInputMethodImpl
public void setEnabled(boolean enabled) {
  if (!mRevoked) {
    // 仅此而已, 就设置了一个 mEnabled 标志位
    mEnabled = enabled;
  }
}

// ----------------------------------------------------------------------
// 这一段是启动 IME 的过程

// IInputMethodWrapper.java
public void startInput(IBinder startInputToken,
                       IInputContext inputContext,
                 @InputConnectionInspector.MissingMethodFlags final int missingMethods,
                       EditorInfo attribute,
                       boolean restarting,
                       boolean shouldPreRenderIme) {
  mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOO(
    DO_START_INPUT, startInputToken, inputContext, attribute, mIsUnbindIssued, args));
}

// IInputMethodWrapper.java
case DO_START_INPUT: {
  // 如果传入的 inputContext 不是空, 会创建一个新的 InputContection
  final InputConnection ic = inputContext != null
    	? new InputConnectionWrapper(mTarget,
                                   inputContext,
                                   moreArgs.argi3,
                                   isUnbindIssued) : null;
  inputMethod.dispatchStartInputWithToken(
      ic,
      info,
      moreArgs.argi1 == 1 /* restarting */,
      startInputToken,
      moreArgs.argi2 == 1 /* shouldPreRenderIme */);
  return;
}

// InputMethodService.java
public final void dispatchStartInputWithToken(
    @Nullable InputConnection inputConnection,
    @NonNull EditorInfo editorInfo, boolean restarting,
    @NonNull IBinder startInputToken, boolean shouldPreRenderIme) {
  // 输入法的特权操作, 告诉 IMMS 开启输入法的 windowToken
  mPrivOps.reportStartInput(startInputToken);
  // 判断下是重启还是第一次启动 IME, 之前可能已经展示过输入法界面
  // restarting 的主要作用是清理之前的残余变量和完成这一次输入过程
  // 对于我们的分析来说, restarting = false
  if (restarting) {
    restartInput(inputConnection, editorInfo);
  } else {
    startInput(inputConnection, editorInfo);
  }
}

// InputMethodService.java
public void startInput(InputConnection ic, EditorInfo attribute) {
  if (DEBUG) Log.v(TAG, "startInput(): editor=" + attribute);
  doStartInput(ic, attribute, false);
}

// InputMethodService.java
void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) {
  if (!restarting) {
    // 清理变量和完成上次输入过程
    doFinishInput();
  }
  // 赋值一些全局变量
  mInputStarted = true;
  mStartedInputConnection = ic;
  mInputEditorInfo = attribute;
  // 第一次启动时不会可见的, 故这里是false
  if (mDecorViewVisible) {
    ...
  } else if (mCanPreRender
             && mInputEditorInfo != null
             && mStartedInputConnection != null) {
    ...
    // 所以自然而然的走到了这里
    // prepareWindow 准备界面并显示出来, 但是 WMS 控制着层级, 所以手机上并看不到它
    // startViews 没什么工作, 通知继承 InputMethodService 的类, IME 开启完成
    startViews(prepareWindow(true /* showInput */));
    // 因为 SoftInputWindow 就是一个 Dialog, 直接调用 Show 展示就好了
    mWindow.show();
  } else {
    mIsPreRendered = false;
  }
  // 至此, 开启 IME 的过程就算完成了, 主要工作是准备界面
}

// InputMethodService.java
void doFinishInput() {
  finishViews(true /* finishingInput */);
  ...
  // 清理下变量
  mInputStarted = false;
  mStartedInputConnection = null;
  mCurCompletions = null;
}

// InputMethodService.java
private void finishViews(boolean finishingInput) {
  ...
  // 继续清理变量
  mInputViewStarted = false;
  mCandidatesViewStarted = false;
}

// InputMethodService.java
private boolean prepareWindow(boolean showInput) {
  ...
  // 这一块就是开始更新 UI 了
  // FullscreenMode 指的横屏情况下
  // 并且没有在 EditorInfo.imeOptions 设置 EditorInfo.IME_FLAG_NO_FULLSCREEN 标志位
  // 全屏模式下, 输入法界面完全覆盖输入框, 为了预览输入的内容, 会添加一个另外的布局
  // 这个布局是 input_method_extract_view.xml, 如果需要美化, 可以从这里入手
  // 这里了解即可, 不再赘述
  // 这个还有一个工作是设置 Window 宽高
  updateFullscreenMode();
  updateInputViewShown();

  // 如果之前没有创建过输入法 UI 的话, 就需要初始化输入法界面了
  if (!mViewsCreated) {
    mViewsCreated = 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);
    }
  }
  return doShowInput;
}

// InputMethodService.java
public void updateFullscreenMode() {
  boolean isFullscreen = mShowInputRequested && onEvaluateFullscreenMode();
  boolean changed = mLastShowInputRequested != mShowInputRequested;
  if (mIsFullscreen != isFullscreen || !mFullscreenApplied) {
    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
      	mFullscreenArea.getLayoutParams();
    if (isFullscreen) {
      // 设置背景
      mFullscreenArea.setBackgroundDrawable(mThemeAttrs.getDrawable(
        com.android.internal.R.styleable.InputMethodService_imeFullscreenBackground));
      // 全屏模式下: 设置高为剩余所有高度
      lp.height = 0;
      lp.weight = 1;
    } else {
      // 非全屏模式: 去掉背景, 节省内存
      mFullscreenArea.setBackgroundDrawable(null);
      // 非全屏模式: 设置高度为包裹内容
      lp.height = LinearLayout.LayoutParams.WRAP_CONTENT;
      lp.weight = 0;
    }
    // 通知 ViewGroup 重新计算布局
    ((ViewGroup)mFullscreenArea.getParent()).updateViewLayout(mFullscreenArea, lp);
    if (isFullscreen) {
      // 这里就是设置全屏模式下预览的地方
      // 这里添加了这个布局: input_method_extract_view.xml
      if (mExtractView == null) {
        View v = onCreateExtractTextView();
        if (v != null) {
          setExtractView(v);
        }
      }
      startExtractingText(false);
    }
  }
  if (changed) {
    // 更新window大小
    onConfigureWindow(mWindow.getWindow(), isFullscreen, !mShowInputRequested);
    mLastShowInputRequested = mShowInputRequested;
  }
}

// InputMethodService.java
// 判断是否为全屏模式
public boolean onEvaluateFullscreenMode() {
  Configuration config = getResources().getConfiguration();
  // 先判断是否为横屏模式
  if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) {
    return false;
  }
  // 再判断是否有设置 EditorInfo.IME_FLAG_NO_FULLSCREEN 标志位
  if (mInputEditorInfo != null
      && (mInputEditorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN) != 0) {
    return false;
  }
  return true;
}

// InputMethodService.java
public void onConfigureWindow(Window win, boolean isFullscreen,
                              boolean isCandidatesOnly) {
  // 会根据是否为全屏, 设置window大小
  final int newHeight = isFullscreen ? MATCH_PARENT : WRAP_CONTENT;
  mWindow.getWindow().setLayout(MATCH_PARENT, newHeight);
}

// InputMethodService.java
public void updateInputViewShown() {
  // 判断是否要显示输入法面板, 有可能有外接键盘存在, 根据配置决定
  boolean isShown = mShowInputRequested && onEvaluateInputViewShown();
  if (mIsInputViewShown != isShown && mDecorViewVisible) {
    mIsInputViewShown = isShown;
    mInputFrame.setVisibility(isShown ? View.VISIBLE : View.GONE);
    if (mInputView == null) {
      // 这里就是创建并设置输入法面板的地方
      View v = onCreateInputView();
      if (v != null) {
        setInputView(v);
      }
    }
  }
}

// InputMethodService.java
public boolean onEvaluateInputViewShown() {
  if (mSettingsObserver == null) {
    Log.w(TAG, "onEvaluateInputViewShown: mSettingsObserver must not be null here.");
    return false;
  }
  // 根据设置的 Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD 值判断是否需要显示输入法面板
  if (mSettingsObserver.shouldShowImeWithHardKeyboard()) {
    return true;
  }
  // 我也不了解...
  Configuration config = getResources().getConfiguration();
  return config.keyboard == Configuration.KEYBOARD_NOKEYS
    || config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES;
}

// InputMethodService.java
// 其实就是通知继承的类 IME 开启完成了
private void startViews(boolean doShowInput) {
  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);
}

这一块内容不是很重要, 如果是开发输入法键盘这一块的话需要深入这一块. 这块的主要内容就是绑定客户端, 为客户端创建 Session, 准备展示等相关工作.

3.4 客户端到获取了输入法结束

输入法启用的最后一个步骤就是客户端记录 IME 的连接, 有了这个连接, 以后就什么都能干了.

结束代码和开始代码在同一个方法里面, 每一次调用 startInputInner 都是一次请求输入法操作, 但有区别的点在于以下两种情况:

  1. 客户端已经和 IME 连接过, 也有相应的 Session 和 IME 对象, 此时调用 startIputInner 就是为了让输入法界面展示出来而已
  2. 客户端从未和 IME 连接过, startInputInner 之后就处于完成状态(或者叫等待状态, 因为正常情况下 IMMS 会有后续操作), 当 IMMS 通知客户端再次绑定输入法, 此时会再走一次 startIputInner, 此时情况就和第1种一样了

以下源码从 android 10.0 摘抄出来, 无用代码已删除.

// ----------------------------------------------------------------------
// 只有客户端第一次请求获取输入法的时候才会走到这里
// 因为客户端上次请求输入法时需要等 IMMS 绑定输入法, 或者等待 IME 给它创建 Session
// IMMS 为了不阻塞客户端就返回了 SUCCESS_WAITING_IME_BINDING 或者 SUCCESS_WAITING_IME_SESSION
// 客户端只需要等待 IMMS 通知
// 如果 IMMS 发现之前有客户端请求过输入法, 当 Session 创建好后就会去通知客户端再去绑定一次
// 这一次就可以直接复用之前创建好的 Session 了, 就会走这么一段代码

// InputMethodManager.java
// IInputMethodClient
public void onBindMethod(InputBindResult res) {
  mH.obtainMessage(MSG_BIND, res).sendToTarget();
}

// InputMethodManager.java
case MSG_BIND: {
  final InputBindResult res = (InputBindResult)msg.obj;
  if (DEBUG) {
    Log.i(TAG, "handleMessage: MSG_BIND " + res.sequence + "," + res.id);
  }
  synchronized (mH) {
    // 这里指明绑定的序列是否正确, 如果不正确就抛弃这次绑定指令
    // 有可能因为多线程或者频繁请求获取输入法原因导致 mBindSequence 不一致
    if (mBindSequence < 0 || mBindSequence != res.sequence) {
      Log.w(TAG, "Ignoring onBind: cur seq=" + mBindSequence
            + ", given seq=" + res.sequence);
      if (res.channel != null && res.channel != mCurChannel) {
        res.channel.dispose();
      }
      return;
    }

    mRequestUpdateCursorAnchorInfoMonitorMode = REQUEST_UPDATE_CURSOR_ANCHOR_INFO_NONE;
		// 设置一些关于输入法的基础变量
    setInputChannelLocked(res.channel);
    mCurMethod = res.method;
    mCurId = res.id;
    mBindSequence = res.sequence;
    mActivityViewToScreenMatrix = res.getActivityViewToScreenMatrix();
  }
  // 再次请求启动输入法, 但是启动的原因是 BOUND_TO_IMMS
  startInputInner(StartInputReason.BOUND_TO_IMMS, null, 0, 0, 0);
  return;
}

// ----------------------------------------------------------------------
// InputMethodManager.java
// 如果之前客户端绑定成功过, 再次请求显示输入法, 就会走这块代码
boolean startInputInner(@StartInputReason int startInputReason,
                        @Nullable IBinder windowGainingFocus,
                        @StartInputFlags int startInputFlags,
                        @SoftInputModeFlags int softInputMode,
                        int windowFlags) {
  ...
  synchronized (mH) {
    // 如果之前客户端绑定成功过, 再次请求显示输入法
    // 就会返回 InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION
    // 并且返回 InputBindResult 对象的 id, mCurMethod 都是有值的
    final InputBindResult res = mService.startInputOrWindowGainedFocus(
      startInputReason, mClient, windowGainingFocus, startInputFlags,
      softInputMode, windowFlags, tba, servedContext, missingMethodFlags,
      view.getContext().getApplicationInfo().targetSdkVersion);
    // 这条Log可以用于分析绑定输入法时处于什么状态
    if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
    mActivityViewToScreenMatrix = res.getActivityViewToScreenMatrix();
    if (res.id != null) {
      // 请求输入法直接返回成功, 就走这块
      // 如果没有成功, 或者需要等待时, 啥都不做
      setInputChannelLocked(res.channel);
      mBindSequence = res.sequence;
      mCurMethod = res.method;
      mCurId = res.id;
    }
    if (mCurMethod != null && mCompletions != null) {
      try {
        // 请求输入法成功时, mCurMethod 一定有值
        // 这时候就可以和输入法交互了
        // 不然, 就什么都不做
        mCurMethod.displayCompletions(mCompletions);
      } catch (RemoteException e) {
      }
    }
  } catch (RemoteException e) {
    Log.w(TAG, "IME died: " + mCurId, e);
  }
}

return true;
}

4. 输入法常用的操作

4.1 ime

adb shell ime

# 列出所有输入法服务
adb shell ime list -a -s
# 设置输入法
adb shell ime set com.sohu.inputmethod.sogouoem/.SogouIME
# 启用输入法
adb shell ime enable com.sohu.inputmethod.sogouoem/.SogouIME
# 不启用输入法
adb shell ime disable com.sohu.inputmethod.sogouoem/.SogouIME
# 重置为默认输入法
adb shell ime reset
# 从设置获取默认输入法
adb shell settings get secure default_input_method

4.2 dumpsys input_method

adb shell dumpsys input_method

可以获取到输入法的各种信息, 特别丰富

adb shell “dumpsys input_method | grep -i window”

可以加过滤条件, 过滤出自己需要的信息

input_method_dumpsys_input_method

4.3 dumpsys window | grep -i input

adb shell “dumpsys window | grep -i input”

获取输入法的窗口状态信息

input_method_dumpsys_window

4.4 dumpsys SurfaceFlinger

adb shell dumpsys SurfaceFlinger

获取输入法的窗口层级信息

input_method_dumpsys_SurfaceFlinger

  • 8
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值