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
  • 9
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android 输入法开发是指为Android系统开发一款输入法应用程序的过程。输入法是一种用于输入文字的工具,通过输入法,用户可以在Android设备上输入各种语言的文字。 Android 输入法开发需要掌握一定的编程知识和技巧。首先,开发者需要了解Android系统的架构和输入法的工作原理。其次,需要使用Java等编程语言来编写输入法的代码。开发者还需要使用Android Studio等开发工具来调试和测试输入法的功能。 在Android 输入法开发过程中,需要考虑以下几个关键点。首先,输入法需要正确地处理用户输入的字符,并将其显示在屏幕上。其次,输入法需要提供联想和自动完成的功能,以帮助用户更快地输入文字。此外,输入法还需要支持多种输入方式,例如手写输入、语音输入等。 同时,开发者还应该关注输入法的用户体验。输入法应该具有良好的界面设计和交互方式,方便用户使用。此外,输入法还应该具备一定的智能化功能,例如根据用户的输入习惯进行个性化设置,提供更准确的输入建议等。 最后,输入法开发完成后,开发者还需要将其发布到应用商店供用户下载和使用。在发布前,需要对输入法进行充分的测试和迭代,确保其稳定性和功能完善性。 总的来说,Android 输入法开发是一个复杂而有挑战的过程,但随着技术的不断发展和改进,越来越多的开发者参与到输入法开发中,推动了输入法的创新和提升。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值