转至元数据结尾
转至元数据起始

研究背景

wps的word文档编辑的时候,界面会不显示。

首先从开发者角度自己开发一款输入法应用的角度,需要了解的一些原理知识:

Android 自带的输入法源码在:/packages/inputmethods里面,对应为LatinIME。Android的输入法框架比较复杂。从进程的角度来讲,相关功能主要分布在下面三个位置:

image-20240219-090907.png

客户端应用是一个包含有图形界面的应用,如地址本。图形界面上包含有能够接收输入的编辑框,如TextView。

输入法模块提供软键盘,将用户在软键盘上的按键输入根据某种算法(如Zi, T9, 国笔等)转换成单词,然后传递给客户端应用。

平台部分实现一些管理功能,负责装载某个输入法模块,启动,终止该模块等。

创建一个输入法,必须继承android.inputmethodservice.InputMethodService,它作为一个服务,监听所有EditText的事件。下面是实现一个基本的输入法程序的步骤。

  1. 建立一个继承自inputmethodservice.InputMethodService的类,称为输入法的服务类。

  2. 在xml 文件中配置这个服务类。

  3. 编写一个用于显示软键盘的布局文件。

  4. 覆盖InputMethodService类的 onCreateInputView 方法。

  5. onCreateInputView 方法需要返回与第3步建立的布局文件对应的View 对象。在返回之前,一般需要设置相应控件的事件,如软键盘按钮单击事件。

  6. 在输入法服务类或其他类中编写响应软键盘中按键事件的代码,如按钮单击事件、物理键盘事件等。

image-20240219-091127.png

在用户触发输入法显示的时候(点击输入框),InputMethodService启动,然后调用onCreate() 函数,该函数在输入法第一次启动的时候调用,适合用来做一些初始化的设置,不要在代码中直接调用该函数;

然后调用onCreateInputView() 函数,在该函数中创建KeyboardView并返回;然后调用onCreateCandidatesView()函数,在该函数中创建候选区实现并返回;

然后调用onStartInputView(EditorInfo attribute, boolean restarting)函数来开始输入内容,输入结束后调用onFinishInput()函数来结束当前的输入,如果移动到下一个输入框则重复调用onStartInputView和onFinishInput函数;

在输入法关闭的时候调用onDestroy() 函数。

以下是LatinIME 输入法简要分析:

输入法的设置在res/xml/method.xml的<input-method>标签中,主要设置两个属性:

android:settingsActivity,输入法的设置程序入口。

android:isDefault,这个输入法是不是系统的默认输入法。

LatinIME.java中LatinIME 继承 InputMethodService;生命周期中方法均在此文件中实现。

最基本的字母布局由res/xml/下面定义,其它的还有符号布局,数字布局等也都在这个文件夹下面。当用户进行操作时,程序就会在这些布局之间来回切换。

Row元素说明这是一行按键的定义,Key元素说明这是一个按键的定义。Key元素通过一些属性来定义每个按键,绘制Key的时候,主要绘制两个东西,label和icon。对于a,b,c,1,2,&等这样可以用字符来表示的键,就绘制它的label属性。对于Shift,Alt等这样无法用字符表示的键,就绘制它的icon属性。

下面是一些常用的属性介绍:

 Codes:代表按键对应的输出值,可以为unicode值或则逗号(,)分割的多个值,也可以为一个字符串。在字符串中通过“\\”来转义特殊字符,例如 '\\n' 或则 '\\uxxxx' 。Codes通常用来定义该键的键码,例如数字按键1对应的为49;如果提供的是逗号分割的多个值则和普通手机输入键盘一样在多个值之间切换。 

 keyLabel:代表按键显示的文本内容。 

 keyIcon:代表按键显示的图标内容,如果指定了该值则在显示的时候显示为图片不显示文本。 

keyWidth:代表按键的宽度,可以为精确值或则相对值,对于精确值支持多种单位,例如:像素,英寸 等;相对值为相对于基础取值的百分比,为以% 或则%p 结尾,其中%p表示相对于父容器。 

keyHeight:代表按键的高度,取值同上。 

horizontalGap:代表按键前的间隙(水平方向),取值同上。 

isSticky:指定按键是否为sticky的。例如Shift大小写切换按键,具有两种状态,按下状态和正常状态,取值为true或则false。 

isModifier:指定按键是否为功能键( modifier key ) ,例如 Alt 或则 Shift 。取值为true或则false。 

keyOutputText:指定按键输出的文本内容,取值为字符串。 

isRepeatable:指定按键是否是可重复的,如果长按该键可以触发重复按键事件则为true,否则为false。 

keyEdgeFlags:指定按键的对齐指令,取值为left或则right。

接下来进入正题,从framework层分析调用输入法后应用白屏原因:

起初对DisplayContent类和WindowState类对输入法窗口相关逻辑进行了大量调试分析(例如computeImeTarget、ImeContainer),发现被网上的相关资料引入到了误区。

后面再回过头来对InputMethodService和ViewRootImpl经过一番源码调试和分析才找到真正原因,简要罗列下面这些关键信息。

弹出输入法时应用内容区显示更新的其中一个关键的调用栈信息如下:

InputMethodService:    at android.inputmethodservice.InputMethodService.lambda$new$0$InputMethodService(InputMethodService.java:501)
InputMethodService:    at android.inputmethodservice.-$$Lambda$InputMethodService$8T9TmAUIN7vW9eU6kTg8309_d4E.onComputeInternalInsets(Unknown Source:2)
InputMethodService:    at android.view.ViewTreeObserver.dispatchOnComputeInternalInsets(ViewTreeObserver.java:1203)
InputMethodService:    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2994)
InputMethodService:    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1948)
InputMethodService:    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8177)
InputMethodService:    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
InputMethodService:    at android.view.Choreographer.doCallbacks(Choreographer.java:796)
InputMethodService:    at android.view.Choreographer.doFrame(Choreographer.java:731)
InputMethodService:    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
InputMethodService:    at android.os.Handler.handleCallback(Handler.java:938)
InputMethodService:    at android.os.Handler.dispatchMessage(Handler.java:99)
InputMethodService:    at android.os.Looper.loop(Looper.java:223)
InputMethodService:    at android.app.ActivityThread.main(ActivityThread.java:7665)
InputMethodService:    at java.lang.reflect.Method.invoke(Native Method)
InputMethodService:    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
InputMethodService:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:948)



InputMethodService类
onCreate()->initViews(){
......
mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer);//添加内部嵌入块大小计算的监听回调
......
}

final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {  //这里是一个lambda表达式,info的类型是InternalInsetsInfo,响应前面添加的OnComputeInternalInsetsListener监听回调onComputeInternalInsets。
        onComputeInsets(mTmpInsets);
......
//将计算后的插入块内容区和可见区的top值回传给ViewRootImpl进行遍历计算。这个地方是导致WPS拉起输入法白屏显示的关键逻辑。
//异常时两个值均为0,即认为content内容区全部被软键盘遮挡所以白屏不显示任何内容。正常显示输入法软键盘时的值是软键盘上方边缘(top)的Y值。这里直接使用输入法软键盘根view的整个高度作为软键盘可见top值实现对应用内容区的零遮挡。
info.contentInsets.top = rootView.getHeight();//mTmpInsets.contentTopInsets;
info.visibleInsets.top = rootView.getHeight();//mTmpInsets.visibleTopInsets;
......
}



Choreographer类
Choreographer::FrameDisplayEventReceiver类
收到刷新信号后执行run方法
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}

doFrame(...)->doCallbacks(...)
     
doCallbacks(...){
......
for (CallbackRecord c = callbacks; c != null; c = c.next) {
......
c.run(frameTimeNanos);//会调用到ViewRootImpl类对象通过mChoreographer.postCallback传过来的mTraversalRunnable
}
......
}

ViewRootImpl的TraversalRunnable内部类
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
ViewRootImpl类
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

final boolean computesInternalInsets =
                mAttachInfo.mTreeObserver.hasComputeInternalInsetsListeners()
                || mAttachInfo.mHasNonEmptyGivenInternalInsets;

TraversalRunnable类对象执行run方法调到doTraversal执行遍历操作
ViewRootImpl->doTraversal(...)->performTraversals(...){
......
if (computesInternalInsets) {
// Clear the original insets.
final ViewTreeObserver.InternalInsetsInfo insets = mAttachInfo.mGivenInternalInsets;
insets.reset();

// Compute new insets in place.
mAttachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets);
......
}
......
}

ViewTreeObserver类
final void dispatchOnComputeInternalInsets(InternalInsetsInfo inoutInfo) {
        final CopyOnWriteArray<OnComputeInternalInsetsListener> listeners =
                mOnComputeInternalInsetsListeners;
        if (listeners != null && listeners.size() > 0) {
            CopyOnWriteArray.Access<OnComputeInternalInsetsListener> access = listeners.start();
            try {
                int count = access.size();
                for (int i = 0; i < count; i++) {
                    access.get(i).onComputeInternalInsets(inoutInfo);
                }
            } finally {
                listeners.end();
            }
        }
    }
//通过onComputeInternalInsets回调(最前面InputMethodService在initViews注册了此监听),从InputMethodService得到软键盘的可见top值visibleTopInsets后,ViewRootImpl再执行performTraversals遍历更新应用的内容区的后续逻辑。