Android输入法IME(二)之 客户端(IMM)启动流程

2. IME初始化启动流程

2.1. IME客户端(IMM)初始化流程

涉及代码文件路径: frameworks/base/core/java/android/view/ViewRootImpl.java frameworks/base/core/java/android/view/WindowManagerGlobal.java frameworks/base/core/java/android/view/inputmethod/InputMethodManager.java frameworks/base/core/java/com/android/internal/view/IInputMethodClient.aidl frameworks/base/core/java/com/android/internal/view/IInputContext.aidl frameworks/base/core/java/com/android/internal/view/IInputMethodManager.aidl frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java

2.1.1. 函数流程梳理

# 每次新增窗口window时,都会实例化ViewRootImpl,而ViewRootImpl在获取IWindowSession时会检查输入法是否已经初始化
ViewRootImpl.java -- 初始化构造函数,调用WindowManagerGlobal.getWindowSession()

---> WindowManagerGlobal.java -- getWindowSession()调用InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary() 实例化全局调用InputMethodManager,即初始化IMM

---> InputMethodManager.java -- ensureDefaultInstanceForDefaultDisplayIfNecessary()调用forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper()),入参默认displayID和looper
        # 此处也说明,对于APP层,IMM有且只有一个实例,每次创建ViewRootImpl都会检查IMM是否实例化完成
        ---》 调用forContextInternal函数,先从缓存Map中查询是否有IMM实例,如果没有则创建IMM实例,并添加到Map中
        ---》 调用createInstance创建实例,然后在三目运算中默认固定调用createRealInstance(displayId, looper)
        ---》 调用createRealInstance函数,   (1)获取输入法服务service,即Context.INPUT_METHOD_SERVICE(service是AIDL接口文件IInputMethodManager.aidl);
                                            (2)new InputMethodManager(service, displayId, looper)创建实例
                                                    ---》 InputMethodManager构造函数
                                                    ---》 new IInputConnectionWrapper 创建虚拟的输入法上下文,主要用于监听输入法服务的激活状态,接受输入事件
                                             # 添加IMM实例到输入法service服务中
                                             # 此处两个入参都是AIDL接口类型的对象
                                             # (1)IInputMethodClient.aidl:输入法客户端, 主要用于报告输入法当前的状态, 让APP应用端的IMM做出相应的处理
                                             # (2)IInputContext.aidl:输入法上下文, 主要用于操作字符输入操作, 让当前接收字符的view进行处理
                                            (3)调用service.addClient(imm.mClient //[AIDL对象,即IInputMethodClient], imm.mIInputContext//[AIDL对象,IInputContext], displayId)

---> IInputMethodManager.aidl -- 调用addClient(跨进程通信到IMMS)

---> 服务端InputMethodManagerService.java "extends IInputMethodManager.Stub" --  调用addClient函数,创建ClientState对象
        ---》 调用内部静态类ClientState的构造函数,保存client相关状态属性

综上代码流程梳理,可以看出:

  1. 对于每个APP应用,IMM有且只有一个实例,并且每次创建ViewRootImpl时,都会检查IMM是否已经实例化成功
  2. 实例化IMM对象时,会涉及到两个AIDL接口文件,一个用于应用端IMM处理输入法当前状态,一个用于输入法上下文,创建一个虚拟的InputContext代表输入空间,用于监听输入法激活状态
  3. 实例化过程中会有个displayid,用于多屏幕显示(通常情况下默认是default display=0)
  4. 实例化最后,会通过AIDL的addClient接口函数,将IMM添加到IMMS中,如此IMM实例化完成

2.1.2. 代码详细说明

//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 {
                    //调用该函数,初始化IMM
                    InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
                    ......
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }

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

    private static InputMethodManager forContextInternal(int displayId, Looper looper) {
        final boolean isDefaultDisplay = displayId == Display.DEFAULT_DISPLAY;
        synchronized (sLock) {
            //从缓存Map中查找是否由default display的IMM实例
            InputMethodManager instance = sInstanceMap.get(displayId);
            //如果存在实例,则直接返回
            if (instance != null) {
                return instance;
            }
            //初始化创建实例
            instance = createInstance(displayId, looper);
            //如果是用于default display使用,则存储到sInstance中作为全局单例实例
            if (sInstance == null && isDefaultDisplay) {
                sInstance = instance;
            }
            //将IMM实例保存到Map中
            sInstanceMap.put(displayId, instance);
            return instance;
        }
    }

    private static InputMethodManager createInstance(int displayId, Looper looper) {
        //isInEditMode固定返回false,直接调用createRealInstance
        return isInEditMode() ? createStubInstance(displayId, looper)
                : createRealInstance(displayId, looper);
    }

    private static InputMethodManager createRealInstance(int displayId, Looper looper) {
        //IInputMethodManager是AIDL接口文件,用于跨进程通信到IMMS(InputMethodManagerService)
        final IInputMethodManager service;
        try {
            //获取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);
        //将PID/UID和每个IME客户端关联,然后作为跨进程服务端IPC使用梳理
        //如果作为同进程内调用梳理,则需要确保Binder.getCalling{Pid, Uid}()返回Process.my{Pid, Uid}()
        //无论哪种情况,都要调用Binder的{clear, restore}CallingIdentity()函数,对跨进程没有影响,对同进程可以满足需求实现
        final long identity = Binder.clearCallingIdentity();
        try {
            // 添加 IMM 实例到输入法服务
            // imm.mClient 是一个aidl对象, mClient即new IInputMethodClient.Stub(),AIDL接口
            // imm.mIInputContext 是一个aidl对象, IInputContext,AIDL接口
            service.addClient(imm.mClient, imm.mIInputContext, displayId);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
        return imm;
    }

//InputMethodManagerService.java
    //由每个APP应用进程调用,作为输入法开始与交互的准备
    @Override
    public void addClient(IInputMethodClient client, IInputContext inputContext,
            int selfReportedDisplayId) {
        //获取调用的uid和pid(即InputMethodManager实际运行所在的UID/PID)
        //两种情况下调用此方法:
        //1.IMM正在另一个进程中实例化
        //2.IMM正在同一个进程中实例化,
        final int callerUid = Binder.getCallingUid();
        final int callerPid = Binder.getCallingPid();
        synchronized (mMethodMap) {
            // TODO: Optimize this linear search.
            final int numClients = mClients.size();
            for (int i = 0; i < numClients; ++i) {
                final ClientState state = mClients.valueAt(i);
                if (state.uid == callerUid && state.pid == callerPid
                        && state.selfReportedDisplayId == selfReportedDisplayId) {
                    throw new SecurityException("uid=" + callerUid + "/pid=" + callerPid
                            + "/displayId=" + selfReportedDisplayId + " is already registered.");
                }
            }
            //利用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);
            }
            //此处不验证displayID,后续每当客户端需要与指定的交互时,就需要检查displayID
            //此处创建ClientState对象,将client和inputContext缓存进去,然后将该对象保存到缓存Map mClients中
            mClients.put(client.asBinder(), new ClientState(client, inputContext, callerUid,
                    callerPid, selfReportedDisplayId, deathRecipient));
        }
    }

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Android输入切换引导功能通常指的是在用户首次安装并启用输入应用时,系统会自动弹出一个输入切换引导界面,提示用户如何在输入时切换不同的输入。 具体实现方可以参考以下步骤: 1. 在输入应用的 AndroidManifest.xml 文件中,添加以下代码: ``` <activity android:name=".InputMethodSettingsActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.SHOW_INPUT_METHOD_PICKER" /> </intent-filter> </activity> ``` 2. 创建一个名为 InputMethodSettingsActivity 的 Activity,并在其中实现输入切换引导功能的逻辑。 3. 在 Activity 的 onCreate 方中,获取系统输入管理器 InputMethodManager,并调用其 showInputMethodPicker 方,显示输入选择界面。 ``` InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showInputMethodPicker(); ``` 4. 在 Activity 的 onResume 方中,判断当前是否为首次启动输入应用,如果是,则调用 showInputMethodPicker 方显示输入选择界面。同时,将一个标志位设置为已启动过,以便下次进入应用时不再弹出引导界面。 ``` private boolean mIsFirstLaunch = true; @Override protected void onResume() { super.onResume(); if (mIsFirstLaunch) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showInputMethodPicker(); mIsFirstLaunch = false; } } ``` 通过以上步骤,就可以实现 Android 输入切换引导功能了。需要注意的是,在实现过程中,还需要处理用户选择输入后的回调逻辑,并在应用中提供方便用户切换输入的方式,以提高用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛文旺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值