Android 输入法框架流程整理
个人简单整理, 如有错误请指正
1. 基本简介
1.1 输入法框架构成
输入法框架整体分为三个部分:
- 输入法管理端
- 输入法服务端
- 输入法客户端
输入法管理端(IMMS)
主要指输入法框架的InputMethodManagerService, 如切换输入法, 输入法显示/隐藏, 输入法启用/关闭, 输入法服务端的绑定, 输入法服务端与输入法客户端的绑定等. 运行在system_server进程
输入法服务端(IMS)
主要指输入法框架的InputMethodService, 这是一个输入法服务, 真正实现输入法界面, 控制字符输入的地方. 运行在输入法进程, 例如搜狗输入法进程
输入法客户端(IMM)
主要指输入法框架的InputMethodManager, 每个app都一个实例, 用来和输入法控制端交互. 运行在需要使用输入法的进程
1.2 输入法交互框架
输入法的整体交互过程如下:
- IMM 利用 IInputMethodManager 请求 IMMS
- IMMS 绑定输入法服务 InputMethodService, 得到 IInputMethod
- IMMS 请求 IInputMethod 创建交互 IInputMethodSession
- IMMS 通过 IInputMethodClient 告知 IMM IInputMethodSession
- 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时会检查输入法是否已经初始化.
以下源码从 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 ...);
}
从上述代码可以看到:
- 在app端, IMM 有且只有一个实例, 并且每次创建 ViewRootImpl 时都会检查 IMM 是否已经实例化完成.
- 在实例化最后, 会通过 IMMS 的 addClient 将 IMM 添加到 IMMS 中, 这样app端输入法客户端就算实例化完成.
- 在实例 IMM 对象时, 会创建一个虚拟的 IInputContext 代表输入控件, 但这里用于监听输入法的激活状态,
- 在实例化过程中可以看到有个 displayId 参数, 这个参数是为了多屏幕显示做准备的.
2.2 输入法管理端( IMMS )的初始化过程
IMMS
InputMethodManagerService
frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
IMMS 运行在system_server进程, 属于系统服务的一部分, 用于控制输入法的切换, 显示/隐藏状态, 输入法服务的绑定等操作.
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 方法中
以下源码从 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