Android Framework系列---输入法服务

Android Framework系列之输入法服务

  • 本文基于Android R(11),从Framework角度介绍Android输入法框架流程及常用调试方法。
    Android Framework系列---输入法服务_第1张图片

写在前面

车载项目需要定制输入法,也有一些POC演示的项目使用原生比如LatinIME(源码路径为/packages/inputmethods/LatinIME),关于输入法可能会遇到以下一些问题

  • 输入法进程启动崩溃
  • 输入法画面被其他应用遮挡
  • 输入法输入内容显示到错误的编辑框内
  • 多屏情况下输入法显示异常
  • 输入法未弹出或输入法未隐藏
  • 定制多屏多客户端输入法

上面举了一些常见例子,实际开发过程中也会有定制输入法服务这类需求。所以对于Android输入法,作为Android Framework工程师对其要有一个整体框架性的了解。

专用术语

  • IMMS: InputMethodManagerService
  • IMS: InputMethodService
  • IMM: InputMethodManager
  • IME: InputMethodEditor
  • MCIMMS:MultiClientInputMethodManagerService

输入法知识点

输入法框架

Android输入法框架包括:IMMS输入法管理服务、IMS输入法服务、IMM输入法管理(客户端)。

  1. IMMS:顾名思义,用于管理输入法的Service,包括打开、关闭、显示、隐藏、切换、绑定输入法等等。这个Service运行在SystemServer中。另外,Android中引入了MCIMMS用于支持多个输入法Client,MCIMMS目前仅作为一个Test功能,感兴趣的可自行研究。
  2. IMS: 输入法服务,比如Android原生自带的LatinIME通过继承InputMethodService的方式实现了一个IMS。IMS以 Application Service的形式运行在应用进程中,通过IMMS管理其状态(比如打开输入法)。
    Android Framework系列---输入法服务_第2张图片
  3. IMM: 输入法管理(客户端),Android中经常将Client端被命名为 XXManager,比如AudioManager,WindowManager,输入法的客户端也是这样。IMM主要指InputMethodManager这个单例类,应用进程通过这个单例对象与IMMS/IMS进行交互。

Android Framework系列---输入法服务_第3张图片

输入法的启动
IMMS初始化

Android Framework系列---输入法服务_第4张图片

  • Kernel拉起Init进程,Init启动Zygote,Zyogte启动SystemServer。SystemServer在startOtherServices阶段启动 IMMS,代码如下(本文下述代码中省略了部分源码
// SystemServer.java
public static void main(String[] args) {
      new SystemServer().run();
}

// SystemServer.java
private void run() {
        // Start services.
        try {
            t.traceBegin("StartServices");
            startBootstrapServices(t);
            startCoreServices(t);
            startOtherServices(t);
        } catch (Throwable ex) {
            Slog.e("System", "******************************************");
            Slog.e("System", "************ Failure starting system services", ex);
            throw ex;
        } finally {
            t.traceEnd(); // StartServices
        }
}

// SystemServer.java
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
	// Bring up services needed for UI.
   	if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
		t.traceBegin("StartInputMethodManagerLifecycle");
		if (InputMethodSystemProperty.MULTI_CLIENT_IME_ENABLED) {
			// 多客户端(针对多屏情况下的一个Sample,默认不启用)
			mSystemServiceManager.startService(
				MultiClientInputMethodManagerService.Lifecycle.class);
         } else {
			mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);
        }
       t.traceEnd();
     }
}
  • 执行InputMethodManagerServiceLifecycle的构造函数,初始化IMMS。
// InputMethodManagerService.java
public static final class Lifecycle extends SystemService {
	private InputMethodManagerService mService;

	public Lifecycle(Context context) {
		super(context);
		mService = new InputMethodManagerService(context);
	}

	@Override
	public void onStart() {
		// 填加到本地服务
		LocalServices.addService(InputMethodManagerInternal.class,
				new LocalServiceImpl(mService));
		// push到binder service中,之后可以通过bind服务找到IMMS。
		publishBinderService(Context.INPUT_METHOD_SERVICE, mService);
	}
}

//InputMethodManagerService.java
public InputMethodManagerService(Context context) {
	mIPackageManager = AppGlobals.getPackageManager();
	mContext = context;
	mRes = context.getResources();
	mHandler = new Handler(this);
	// Note: SettingsObserver doesn't register observers in its constructor.
	// 监听输入法的设置,比如默认输入法
	mSettingsObserver = new SettingsObserver(mHandler);
	// 下面几行获取了相关服务的LocalService对象,IMMS与window、package、input进行交互。比如显示输入法时,需要利用WMS服务判定IME显示层级。
	mIWindowManager = IWindowManager.Stub.asInterface(
			ServiceManager.getService(Context.WINDOW_SERVICE));
	mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
	mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
	mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
	mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
	// 这个写法比较特殊,是一个lambda表达式
	mImeDisplayValidator = displayId -> mWindowManagerInternal.shouldShowIme(displayId);
	mCaller = new HandlerCaller(context, null, new HandlerCaller.Callback() {
		@Override
		public void executeMessage(Message msg) {
			handleMessage(msg);
		}
	}, true /*asyncHandler*/);
	mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
	mUserManager = mContext.getSystemService(UserManager.class);
	mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
	mHardKeyboardListener = new HardKeyboardListener();
	mHasFeature = context.getPackageManager().hasSystemFeature(
			PackageManager.FEATURE_INPUT_METHODS);
	mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);
	// 判断是否为低内存模式
	mIsLowRam = ActivityManager.isLowRamDeviceStatic();

	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,
			PendingIntent.FLAG_IMMUTABLE);

	mShowOngoingImeSwitcherForPhones = false;

	mNotificationShown = false;
	int userId = 0;
	try {
		userId = ActivityManager.getService().getCurrentUser().id;
	} catch (RemoteException e) {
		Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e);
	}

	mLastSwitchUserId = userId;

	// mSettings should be created before buildInputMethodListLocked
	mSettings = new InputMethodSettings(
			mRes, context.getContentResolver(), mMethodMap, userId, !mSystemReady);

	updateCurrentProfileIds();
	AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
	mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
			mSettings, context);
}
  • 上述代码中IMMS获取了许多其他服务的代理对象(WindowManager、PackageManager、InputManager等等),通过它们获取相关功能。从这里也可以看出,合理的功能模块划分,是有利于代码的开发维护。
IMM的初始化
  • IMM是一个单例类,在每个应用中有一个实例。应用通过IMM请求IMMS启动输入法,IMMS通过Callback形式通知到IMM,进而告知应用相关输入法状态。
    Android Framework系列---输入法服务_第5张图片
  • 添加Window时会实例化ViewRootImpl,在ViewRootImpl中会初始化IMM。
// ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
	this(context, display, WindowManagerGlobal.getWindowSession(),
			false /* useSfChoreographer */);
}

// WindowManagerGlobal.java
public static IWindowSession getWindowSession() {
	synchronized (WindowManagerGlobal.class) {
		if (sWindowSession == null) {
			try {
				// Emulate the legacy behavior.  The global instance of InputMethodManager
				// was instantiated here.
				// TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
				InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
				IWindowManager windowManager = getWindowManagerService();
				sWindowSession = windowManager.openSession(
						new IWindowSessionCallback.Stub() {
							@Override
							public void onAnimatorScaleChanged(float scale) {
								ValueAnimator.setDurationScale(scale);
							}
						});
			} catch (RemoteException e) {
				throw e.rethrowFromSystemServer();
			}
		}
		return sWindowSession;
	}
}
  • 上面的代码调用了 InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary(),在这个函数中对IMM进行了初始化。
// 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中根据displayID查找 imm,如果已经创建则返回。
		InputMethodManager instance = sInstanceMap.get(displayId);
		if (instance != null) {
			return instance;
		}
		
		// 创建IMM实例
		instance = createInstance(displayId, looper);
		// For backward compatibility, store the instance also to sInstance for default display.
		if (sInstance == null && isDefaultDisplay) {
			sInstance = instance;
		}
		
		// IMM实例放入缓存map
		sInstanceMap.put(displayId, instance);
		return instance;
	}
}

// InputMethodManager.java
private static InputMethodManager createRealInstance(int displayId, Looper looper) {
	final IInputMethodManager service;
	try {
		// 取得IMMS服务对象。这里个INPUT_METHOD_SERVICE,就是IMMS初始化时push到binder中的service标志。
		service = IInputMethodManager.Stub.asInterface(
				ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
	} catch (ServiceNotFoundException e) {
		throw new IllegalStateException(e);
	}
	
	// 创建IMM实例对象
	final InputMethodManager imm = new InputMethodManager(service, displayId, looper);
	final long identity = Binder.clearCallingIdentity();
	try {
		// 将Client告知IMMS。IMMS内部会管理多个Client(每个应用都会有一个Client)
		service.addClient(imm.mClient, imm.mIInputContext, displayId);
	} catch (RemoteException e) {
		e.rethrowFromSystemServer();
	} finally {
		Binder.restoreCallingIdentity(identity);
	}
	return imm;
}
  • 到此创建了IMM对象,并获取了与IMMS服务交互的代理对象。每个IMM通过IMMS的addClient将自己的相关信息告诉IMMS,包括 mClient、mIInputContext、displayId。对于DisplayID,就是屏幕的逻辑ID。那么其他两个是什么?
// InputMethodManager.java
private InputMethodManager(IInputMethodManager service, int displayId, Looper looper) {
	mService = service;
	mMainLooper = looper;
	mH = new H(looper);
	mDisplayId = displayId;
	// mIInputContext 实际上是IInputContext.Stub对象,输入法上下文。 这个对象会同过 IMMS 最终告知 IMS。通过这个对象,应用端接收输入的相关字符,让view进行处理。
	mIInputContext = new ControlledInputConnectionWrapper(looper, mDummyInputConnection, this,
			null);
}

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

// InputMethodManager.java
public abstract class IInputConnectionWrapper extends IInputContext.Stub {}

// InputMethodManager.java
// mClient实际上是IInputMethodClient.Stub对象,它作为Callback从IMMS获得输入法相关状态,使得应用可以做出相关动作。
final IInputMethodClient.Stub mClient = new IInputMethodClient.Stub() {}

// IInputMethodClient.aidl
/**
 * Interface a client of the IInputMethodManager implements, to identify
 * itself and receive information about changes to the global manager state.
 */
oneway interface IInputMethodClient {

// IInputContext.aidl
/**
 * Interface from an input method to the application, allowing it to perform
 * edits on the current input field and other interactions with the application.
 * {@hide}
 */
oneway interface IInputContext {
}
IMS的初始化
  • IMS运行在输入法进程中,是一个Application里的service。可以通过BindService获取IMS服务对象。如果系统有多款输入法,那么就会有多个IMS(可以通过 ime list -s查看系统当前支持的输入法服务)。以Android原始自带的LatinIME为例。
    Android Framework系列---输入法服务_第6张图片

  • AndroidManifest.xml中定义了Service



<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        coreApp="true"
        package="com.android.inputmethod.latin"
        android:versionCode="28">
    <application android:label="@string/english_ime_name"
            android:icon="@drawable/ic_launcher_keyboard"
            android:supportsRtl="true"
            android:allowBackup="true"
            android:defaultToDeviceProtectedStorage="true"
            android:directBootAware="true">

        
        <service android:name="LatinIME"
                android:label="@string/english_ime_name"
                android:permission="android.permission.BIND_INPUT_METHOD">
            <intent-filter>
                <action android:name="android.view.InputMethod" />
            intent-filter>
            <meta-data android:name="android.view.im" android:resource="@xml/method" />
        service>		
		
    application>
manifest>
  • LatinIME的实现类继承了InputMethodService,也就是实现了IMS。
/**
 * Input method implementation for Qwerty'ish keyboard.
 */
public class LatinIME extends InputMethodService implements KeyboardActionListener,
        SuggestionStripView.Listener, SuggestionStripViewAccessor,
        DictionaryFacilitator.DictionaryInitializationListener,
        PermissionsManager.PermissionsResultCallback { }
  • 点击文本输入框触发Focus焦点变更是,IMM会告知IMMS启动IMS(这个流程在下章会介绍,这个关注IMS自身的初始化。),IMMS通过BindServic初始化IMS服务。
///packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/LatinIME.java
public void onCreate() {
	// LatinIME会进行自身的一些初始化,这里主要关注其InputMethodService的初始化。
	super.onCreate();
}

// 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);

	// 判断是否为车载系统
	mIsAutomotive = isAutomotive();
	mAutomotiveHideNavBarForKeyboard = getApplicationContext().getResources().getBoolean(
			com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard);

	// TODO(b/111364446) Need to address context lifecycle issue if need to re-create
	// for update resources & configuration correctly when show soft input
	// in non-default display.
	mInflater = (LayoutInflater)getSystemService(
			Context.LAYOUT_INFLATER_SERVICE);
			
	// 创建输入法窗口(Dialog类型)
	mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState,
			WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false);
	mWindow.getWindow().getAttributes().setFitInsetsTypes(statusBars() | navigationBars());
	mWindow.getWindow().getAttributes().setFitInsetsSides(Side.all() & ~Side.BOTTOM);
	mWindow.getWindow().getAttributes().setFitInsetsIgnoringVisibility(true);

	// IME layout should always be inset by navigation bar, no matter its current visibility,
	// unless automotive requests it. Automotive devices may request the navigation bar to be
	// hidden when the IME shows up (controlled via config_automotiveHideNavBarForKeyboard)
	// in order to maximize the visible screen real estate. When this happens, the IME window
	// should animate from the bottom of the screen to reduce the jank that happens from the
	// lack of synchronization between the bottom system window and the IME window.
	if (mIsAutomotive && mAutomotiveHideNavBarForKeyboard) {
		mWindow.getWindow().setDecorFitsSystemWindows(false);
	}
	mWindow.getWindow().getDecorView().setOnApplyWindowInsetsListener(
			(v, insets) -> v.onApplyWindowInsets(
					new WindowInsets.Builder(insets).setInsets(
							navigationBars(),
							insets.getInsetsIgnoringVisibility(navigationBars()))
							.build()));

	// For ColorView in DecorView to work, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS needs to be set
	// by default (but IME developers can opt this out later if they want a new behavior).
	mWindow.getWindow().setFlags(
			FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
	// 初始化View相关内容
	initViews();
	mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);

	mInlineSuggestionSessionController = new InlineSuggestionSessionController(
			this::onCreateInlineSuggestionsRequest, this::getHostInputToken,
			this::onInlineSuggestionsResponse);
}

// SoftInputWindow.java
public SoftInputWindow(Context context, String name, int theme, Callback callback,
		KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState,
		int windowType, int gravity, boolean takesFocus) {
	super(context, theme);
	mName = name;
	mCallback = callback;
	mKeyEventCallback = keyEventCallback;
	mDispatcherState = dispatcherState;
	mWindowType = windowType;
	mGravity = gravity;
	mTakesFocus = takesFocus;
	initDockWindow();
}

// SoftInputWindow.java
private void initDockWindow() {
	WindowManager.LayoutParams lp = getWindow().getAttributes();
	// mWindowType是 WindowManager.LayoutParams.TYPE_INPUT_METHOD,可以通过改这里改变输入法WindowType,进行影响默认层级。
	lp.type = mWindowType;
	lp.setTitle(mName);

	lp.gravity = mGravity;
	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里面,不获取焦点。
	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);
}
  • 上面对于IMS进行了一些初始化,主要是设置输入法窗口的一些属性。下面看一下,IMS通过onBind接口返回的Binder对象。Client端通过onBind时返回的对象与IMS服务交互。IMS继承了AbstractInputMethodService,onBind的 实现定义在这个类中。
// AbstractInputMethodService.java
public abstract class AbstractInputMethodService extends Service {
    final public IBinder onBind(Intent intent) {
        if (mInputMethod == null) {
            mInputMethod = onCreateInputMethodInterface();
        }
        // IMMS通过这个对象控制 输入法服务(IMS)。IInputMethodWrapper 实际上是IInputMethod.Stub类型。
        return new IInputMethodWrapper(this, mInputMethod);
    }
}

// IInputMethodWrapper.java
class IInputMethodWrapper extends IInputMethod.Stub {}

// IInputMethod.aidl
oneway interface IInputMethod {}
  • 综上,IMS启动完成。返回 IInputMethod.stub对象给IMMS用于操作IMS。
输入法的启动
  • 上面的内容,主要关注 IMM、IMS、IMMS的初始化过程。在应用中点击文本输入框会弹出输入法界面。下面主要对这个流程进行分析。
    Android Framework系列---输入法服务_第7张图片

  • 点击文本输入框后,控件获取焦点,会触发ViewRootImpl的焦点变更流程。这个流程会调用IMM的startInput函数启动输入法。

// ViewRootImpl.java
public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
	Message msg = Message.obtain();
	msg.what = MSG_WINDOW_FOCUS_CHANGED;
	mHandler.sendMessage(msg);
}
// ViewRootImpl.java
public void handleMessage(Message msg) 
	// 省略
	case MSG_WINDOW_FOCUS_CHANGED: {
	handleWindowFocusChanged();
	} break;
}

// ViewRootImpl.java
private void handleWindowFocusChanged() {
	if (mAdded) {
		// Note: must be done after the focus change callbacks,
		// so all of the view state is set up correctly.
		mImeFocusController.onPostWindowFocus(mView.findFocus(), hasWindowFocus,
				mWindowAttributes);
	}
}

// ImeFocusController.java
void onPostWindowFocus(View focusedView, boolean hasWindowFocus,
		WindowManager.LayoutParams windowAttribute) {
	// 没有焦点的话,不弹出输入法
	if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
		return;
	}

	// 获取Delegate对象(包装了IMM)
	boolean forceFocus = false;
	final InputMethodManagerDelegate immDelegate = getImmDelegate();

	// 请求启动输入法
	immDelegate.startInputAsyncOnWindowFocusGain(viewForWindowFocus,
			windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
}

// InputMethodManager.java
public void startInputAsyncOnWindowFocusGain(View focusedView,
		@SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) {
	if (controller.checkFocus(forceNewFocus, false)) {
		// We need to restart input on the current focus view.  This
		// should be done in conjunction with telling the system service
		// about the window gaining focus, to help make the transition
		// smooth.
        //  通常情况下会走到这里
		if (startInput(StartInputReason.WINDOW_FOCUS_GAIN,
				focusedView, startInputFlags, softInputMode, windowFlags)) {
			return;
		}
	}
}

// InputMethodManager.java
public boolean startInput(@StartInputReason int startInputReason, View focusedView,
		@StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
		int windowFlags) {

	// 这些代码是在UIThread中执行的
	return startInputInner(startInputReason,
			focusedView != null ? focusedView.getWindowToken() : null, startInputFlags,
			softInputMode, windowFlags);
}

// InputMethodManager.java
boolean startInputInner(@StartInputReason int startInputReason,
		@Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
		@SoftInputModeFlags int softInputMode, int windowFlags) {
	final View view;
	synchronized (mH) {
		view = getServedViewLocked();
	}

	// Okay we are now ready to call into the served view and have it
	// do its stuff.
	// Life is good: let's hook everything up!
	// 记录编辑器相关信息的对象,输入法根据这些信息显示不同的效果
	EditorInfo tba = new EditorInfo();

	tba.packageName = view.getContext().getOpPackageName();
	tba.autofillId = view.getAutofillId();
	tba.fieldId = view.getId();
	// 创建InputConnection,调用的是TextView中的对应函数。创建了EditableInputConnection类型对象
	// 后续利用InputConnection对目标控件进行相关字符串操作
	InputConnection ic = view.onCreateInputConnection(tba);

	synchronized (mH) {
	 
		if (ic != null) {
			// 这个对象实际上是 IInputContext.stub对象。上面创建的InpuConnection传给这个对象。
			// IMS与 IInputContext.stub交互, IInputContext.stub通过 InpuConnection与控件交互。
			servedContext = new ControlledInputConnectionWrapper(
					icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view);
		} else {
			servedContext = null;
			missingMethodFlags = 0;
		}
		mServedInputConnectionWrapper = servedContext;

		try {
			// 真正启动输入法的地方,返回的InputBindResult是一个Parcelable
			final InputBindResult res = mService.startInputOrWindowGainedFocus(
					startInputReason, mClient, windowGainingFocus, startInputFlags,
					softInputMode, windowFlags, tba, servedContext, missingMethodFlags,
					view.getContext().getApplicationInfo().targetSdkVersion);

			if (res == null) {
				Log.wtf(TAG, "startInputOrWindowGainedFocus must not return"
						+ " null. startInputReason="
						+ InputMethodDebug.startInputReasonToString(startInputReason)
						+ " editorInfo=" + tba
						+ " startInputFlags="
						+ InputMethodDebug.startInputFlagsToString(startInputFlags));
				return false;
			}
			
			if (res.id != null) {
				// 设置InputChannel
				setInputChannelLocked(res.channel);
				mBindSequence = res.sequence;
				// IInputMethodSession类型对象,这个对象是IMS的Binder代理。通过它与IMS直接交互。
				// 这样应用端就拿到了与IMS直接交互的对象
				mCurMethod = res.method;
				// 当前输入法的ID(不同输入法ID值不一样)
				mCurId = res.id;
			} else if (res.channel != null && res.channel != mCurChannel) {
				res.channel.dispose();
			}

		} catch (RemoteException e) {
			Log.w(TAG, "IME died: " + mCurId, e);
		}
	}

	return true;
}
  • 如果startInputInner执行成功的话,应用端的IMM中便会持有 IInputMethodSession类型对象,通过它与IMS进行交互。上面的mService是IMMS的客户端代理,在其startInputOrWindowGainedFocus函数会启动输入法。
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) {


	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);
		}
	}

	return result;
}

// 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) {

	// 计算IME的TargeWindow,输入法窗口会根据TargetWindow动态计算显示层级
	// 此函数会调用到WMS,并调用到DisplayContent::computeImeTarget函数中。
	if (!mWindowManagerInternal.isInputMethodClientFocus(cs.uid, cs.pid,
			cs.selfReportedDisplayId)) {
		// Check with the window manager to make sure this client actually
		// has a window with focus.  If not, reject.  This is thread safe
		// because if the focus changes some time before or after, the
		// next client receiving focus that has any interest in input will
		// be calling through here after that change happens.
		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;
	}
	
	// 判断是否是相同的Window获得了Focus
	final boolean sameWindowFocused = mCurFocusedWindow == windowToken;
	// 判断是不是文本编辑器
	final boolean isTextEditor = (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0;
	// 启动要因是否为得到焦点
	final boolean startInputByWinGainedFocus =
			(startInputFlags & StartInputFlags.WINDOW_GAINED_FOCUS) != 0;
	
	// 如果焦点window一样,并且是本文编辑器。表示之前已经启动了输入法,直接启动。
	if (sameWindowFocused && isTextEditor) {
		if (DEBUG) {
			Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client
					+ " attribute=" + attribute + ", token = " + windowToken
					+ ", startInputReason="
					+ InputMethodDebug.startInputReasonToString(startInputReason));
		}
		if (attribute != null) {
			return startInputUncheckedLocked(cs, inputContext, missingMethods,
					attribute, startInputFlags, startInputReason);
		}
		return new InputBindResult(
				InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
				null, null, null, -1, null);
	}


	// We want to start input before showing the IME, but after closing
	// it.  We want to do this after closing it to help the IME disappear
	// more quickly (not get stuck behind it initializing itself for the
	// new focused input, even if its window wants to hide the IME).
	boolean didStart = false;
	// 判断android:windowSoftInputMode 
	InputBindResult res = null;
	switch (softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE) {
		// 默认情况下走这里
		case LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
			if (!sameWindowFocused && (!isTextEditor || !doAutoShow)) {
				if (LayoutParams.mayUseInputMethod(windowFlags)) {
					// There is no focus view, and this window will
					// be behind any soft input window, so hide the
					// soft input window if it is shown.
					if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
					hideCurrentInputLocked(
							mCurFocusedWindow, InputMethodManager.HIDE_NOT_ALWAYS, null,
							SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);

					// If focused display changed, we should unbind current method
					// to make app window in previous display relayout after Ime
					// window token removed.
					// Note that we can trust client's display ID as long as it matches
					// to the display ID obtained from the window.
					if (cs.selfReportedDisplayId != mCurTokenDisplayId) {
						unbindCurrentMethodLocked();
					}
				}
			} else if (isTextEditor && doAutoShow
					&& (softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
				// There is a focus view, and we are navigating forward
				// into the window, so show the input window for the user.
				// We only do this automatically if the window can resize
				// to accommodate the IME (so what the user sees will give
				// them good context without input information being obscured
				// by the IME) or if running on a large screen where there
				// is more room for the target window + IME.
				if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
				if (attribute != null) {
					// 启动输入法
					res = startInputUncheckedLocked(cs, inputContext, missingMethods,
							attribute, startInputFlags, startInputReason);
					didStart = true;
				}
				// 显示输入法
				showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
						SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
			}
			break;
		case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
			//  后面的代码省略。遇到问题时,可以根据具体情况,加log分析。
	}

	if (!didStart) {
		// 如果没有启动的话,这里会做一下保护。感兴趣的可以看源码研究一下。
	}
	return res;
}

// InputMethodManagerService.java
InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,
		@MissingMethodFlags int missingMethods, @NonNull EditorInfo attribute,
		@StartInputFlags int startInputFlags, @StartInputReason int startInputReason) {
	// If no method is currently selected, do nothing.
	// 如果当前没有输入法,直接返回
	if (mCurMethodId == null) {
		return InputBindResult.NO_IME;
	}

	// 启动没有ready,直接返回
	if (!mSystemReady) {
		// If the system is not yet ready, we shouldn't be running third
		// party code.
		return new InputBindResult(
				InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
				null, null, mCurMethodId, mCurSeq, null);
	}

	// 得到显示输入法的DisplayID
	final int displayIdToShowIme = computeImeDisplayIdForTarget(cs.selfReportedDisplayId,
			mImeDisplayValidator);

	// Check if the input method is changing.
	// We expect the caller has already verified that the client is allowed to access this
	// display ID.
	// 走到这个判断里面,基本上就是已经绑定过输入法了。直接返回结果就行。
	if (mCurId != null && mCurId.equals(mCurMethodId)
			&& displayIdToShowIme == mCurTokenDisplayId) {
		
	}

	// 没有绑定过,则重新开发绑定输入法。
	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());
	mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
			com.android.internal.R.string.input_method_binding_label);
	mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
			mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
			PendingIntent.FLAG_IMMUTABLE));
	// 实际上是调用BindService,获取输入法服务(IMS)
	if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
		mLastBindTime = SystemClock.uptimeMillis();
		mHaveConnection = true;
		mCurId = info.getId();
		mCurToken = new Binder();
		mCurTokenDisplayId = displayIdToShowIme;
		try {
			if (DEBUG) {
				Slog.v(TAG, "Adding window token: " + mCurToken + " for display: "
						+ mCurTokenDisplayId);
			}
			
			// 添加用于显示输入法的Token
			mIWindowManager.addWindowToken(mCurToken, LayoutParams.TYPE_INPUT_METHOD,
					mCurTokenDisplayId);
		} catch (RemoteException e) {
		}
		// 成功:返回正在等待绑定IMS
		return new InputBindResult(
				InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
				null, null, mCurId, mCurSeq, null);
	}
	
	mCurIntent = null;
	Slog.w(TAG, "Failure connecting to input method service: " + mCurIntent);
	return InputBindResult.IME_NOT_CONNECTED;
}

// InputMethodManagerService.java
// BindService成功后的回调
public void onServiceConnected(ComponentName name, IBinder service) {
	synchronized (mMethodMap) {
		if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
			//得到IInputMethod对象,IMMS通过这个对象与IMS交互。
			mCurMethod = IInputMethod.Stub.asInterface(service);
			
			//初始化输入法
			executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(
					MSG_INITIALIZE_IME, mCurTokenDisplayId, mCurMethod, mCurToken));
			scheduleNotifyImeUidToAudioService(mCurMethodUid);
			if (mCurClient != null) {
				// 接上述流程,此时有客户端等待。先清理session,然后创建session。session用于应用与输入法交互。
				clearClientSessionLocked(mCurClient);
				requestClientSessionLocked(mCurClient);
			}
		}
	}
}

// InputMethodManagerService.java
void requestClientSessionLocked(ClientState cs) {
	if (!cs.sessionRequested) {
		if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs);
		InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString());
		cs.sessionRequested = true;
		executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO(
				MSG_CREATE_SESSION, mCurMethod, channels[1],
				new MethodCallback(this, mCurMethod, channels[0])));
	}
}


// IInputMethodWrapper.java
public void createSession(InputChannel channel, IInputSessionCallback callback) {
	mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_SESSION,
			channel, callback));
}
// IInputMethodWrapper.java
public void executeMessage(Message msg) {
	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
public abstract class AbstractInputMethodImpl implements InputMethod {
	/**
	 * Instantiate a new client session for the input method, by calling
	 * back to {@link AbstractInputMethodService#onCreateInputMethodSessionInterface()
	 * AbstractInputMethodService.onCreateInputMethodSessionInterface()}.
	 */
	@MainThread
	public void createSession(SessionCallback callback) {
		// 走这里,把session通知回去(IMS给IMMS通知)
		callback.sessionCreated(onCreateInputMethodSessionInterface());
	}
}

// InputMethodService.java
// InputMethodSessionImpl 这个对象,在IInputMethodWrapper.java中被 被InputMethodSessionCallbackWrapper包装成 IInputMethodSessionWrapper 对象。
// IInputMethodSessionWrapper 是IInputMethodSession.Stub 类型。
public AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface() {
     return new InputMethodSessionImpl();
 }
  • IMS创建了将IInputMethodSession的代理,并通过Callback返回给IMMS。
// InputMethodManagerService.java
// callback.sessionCreated 通过Binder回调到IMMS端的这个函数。
void onSessionCreated(IInputMethod method, IInputMethodSession session,
		InputChannel channel) {
	synchronized (mMethodMap) {
	
		if (mCurMethod != null && method != null
				&& mCurMethod.asBinder() == method.asBinder()) {
			if (mCurClient != null) {
				clearClientSessionLocked(mCurClient);
				// 这个Client是IMM 通过addClient告知 IMMS的。它对应着某个应用端
				mCurClient.curSession = new SessionState(mCurClient,
						method, session, channel);
                // 可以真正启动输入法了!!!
				InputBindResult res = attachNewInputLocked(
						StartInputReason.SESSION_CREATED_BY_IME, true);
				if (res.method != null) {
					// method 是 InputSession。如果非空,代表IMS已经创建了一个会话,那么 将这个会话与对应的应用Client端绑定。实际上调用了IInputMethodClient 的onBindMethod,将Parcelabled对象告知应用端。
					executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO(
							MSG_BIND_CLIENT, mCurClient.client, res));
				}
				return;
			}
		}
	}
}

// InputMethodManagerService.java
InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) {
	if (!mBoundToMethod) {
		// 将客户端绑定到IME(IMS),将InputConnection告知IMS。
		// 调用InputMethod的bindInput API
		executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
				MSG_BIND_INPUT, mCurMethod, mCurClient.binding));
		mBoundToMethod = true;
	}


	// 启动输入法(告知IMS显示输入法)
	final SessionState session = mCurClient.curSession;
	executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(
			MSG_START_INPUT, mCurInputContextMissingMethods, initial ? 0 : 1 /* restarting */,
			startInputToken, session, mCurInputContext, mCurAttribute));
	if (mShowRequested) {
		// 显示输入法,调用了InputMethod的showSoftInput
		if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
		showCurrentInputLocked(mCurFocusedWindow, getAppShowFlags(), null,
				SoftInputShowHideReason.ATTACH_NEW_INPUT);
	}
	return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
			session.session, (session.channel != null ? session.channel.dup() : null),
			mCurId, mCurSeq, mCurActivityViewToScreenMatrix);
}

// InputMethodManagerService.java
public boolean handleMessage(Message msg) {
	case MSG_START_INPUT: {
		final int missingMethods = msg.arg1;
		final boolean restarting = msg.arg2 != 0;
		args = (SomeArgs) msg.obj;
		final IBinder startInputToken = (IBinder) args.arg1;
		final SessionState session = (SessionState) args.arg2;
		final IInputContext inputContext = (IInputContext) args.arg3;
		final EditorInfo editorInfo = (EditorInfo) args.arg4;
		try {
			setEnabledSessionInMainThread(session);
			session.method.startInput(startInputToken, inputContext, missingMethods,
					editorInfo, restarting, session.client.shouldPreRenderIme);
		} catch (RemoteException e) {
		}
		args.recycle();
		return true;
	}
}
  • 到这里,输入法启动的大部分流程已经完成。当客户端的 onBindMethod被触发(InputMethodManager.java)应用客户端就收到了输入法对象,后续做了绑定以及再次请求启动输入法(此时已经启动过了)等操作。这些操作,遇到相关问题 时看代码分析即可。
输入法组件图
  • 综上,总结一下IMS、IMM和IMMS的组件图。通过组件图可以了解个模块间的交互接口。
  1. IInputMethodManager: IMM通过它请求IMMS
  2. IInputMethodClient: IMMS通过它告知IMM相关通知及状态(包括Session对象)
  3. IInputMethod: IMMS用来请求IMS的对象
  4. IInputMethodSessionCallback: IMS通过这个Callback,把
  5. IInputMethodSession告知IMMS,进而告知IMM
  6. InputContext:IMM通过IMMS告知IMS的对象,IMS通过这个对象回调IMM
  7. IInputMethodSession:IMM用来请求IMS的对象
    Android Framework系列---输入法服务_第8张图片

输入法调试

  • 可以通过一下方式配置系统输入法(PS:原生Setting中有输入法设置画面,但实际项目中原始Setting一般都会被禁用或只能 以Debug方式启动。)
通过配置文件修改默认输入法
  • 在framework的res文件中,定义def_input_method和config_default_input_method的值,并在DatabaseHelper.java的loadSecureSettings中加载定义的默认值(前提是输入法应用已被打包到系统)

<string name="def_input_method" translatable="false">xxxxstring>
<string name="def_enabled_input_methods" translatable="false">xxxxxstring> 
// /frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java

 private void loadSecureSettings(SQLiteDatabase db) {
 	loadStringSetting(stmt, Settings.Secure.ENABLED_INPUT_METHODS,R.string.def_enabled_input_methods);
	loadStringSetting(stmt,Settings.Secure.DEFAULT_INPUT_METHOD,R.string.def_input_method);
 }
通过ime命令调试
  • 通过ime命令,配置当前系统的输入法
# xxx.apk 是输入法安装包
# root和remount非必须命令
adb root
adb remount 
adb install xxx.apk

# 启用输入法,否则ims list -s 看不到输入法
adb shell
# 比如 com.android.inputmethod.leanback/.service.LeanbackImeService
# 根据自己安装的输入法信息设置
# 实在不知道的,可以通过 dumpsys package 包名 | grep Service 确认
ime enable 包名/.Service名
ime set 包名/.Service名

# 点击输入法测试即可

##### 查看输入法相关状态
dumpsys input_method

你可能感兴趣的:(Android,android,输入法,inputmethod,ime,调试,ims,pinyin)