键盘显示白板的问题排查

759 阅读6分钟

用户在使用KK键盘时,反馈说弹出来的键盘是大白板,有时对着大白板还能打字。这个问题非必现,概率极低。

我开始认为是键盘在弹起时会有某些crash,进而导致大白板,于是我自己制作一个Exception让它crash,但是现象是KK键盘弹出来之后,马上就退出了,还和用户的反馈现象不同。

我的同事也给我反应说他遇到了这个问题,在认为是某些地方crash导致的情况下,我做了一个非常简单的crash搜集系统。大概是,当KK键盘crash时,把异常信息和崩溃栈记录到本地某个文件夹下,然后在KK键盘的app的用户反馈界面添加一个菜单按钮,点击后显示一个列表列出来所有的崩溃的信息。然后就可以查看它的详细信息或者把它通过分享系统发送给别人。

但是,我的同事说后来没有碰到键盘白板了。不仅如此,用户出现白板后,反馈说在那个列表中并没有看到有崩溃日志。也就是说没有crash。

另外一个同事的手机拿给我说,有白板,我点击了一下白板,发现她的手机的白板还能点击相应一些键盘上的交互事件。不过不全是可以交互,有的用户说出现白板时,不能点击交互。

我会想各种发生的问题。

键盘上显示表情(sticker),是使用的Glide库。于是我会怀疑,会不会是Glide大量加载图片资源(尤其gif图),然后要把gif图片显示到view上需要不断刷新,以至于view树的绘制太频繁导致的大白板(当时还不了解view树的绘制机制)。

然后我怀疑是OOM引起的,那么我就观察Profiler(没发现啥大毛病),那么就不断的申请大内存并且持有它,让它不被GC释放,直到达到进程的内存的上限,此时收起键盘抬起键盘,键盘也会因为OOM而crash,然而并没有发现键盘大白板。

之后我尝试当键盘弹起时在键盘的UI线程做耗时的操作(ANR),但此时发生的现象和用户描述还不一样,而是要么键盘弹起来特别晚,要么弹出来后键盘无法点击(但是可以看见),有时也存在大白板的现象(没有绘制呢,但是不可点击不可响应事件),不过有用户以及我自己以前也发现大白板时可以点击可以响应一些时间。

再接着我研究了view树的绘制apk的加固问题(曾经一度怀疑apk加固可能会导致整个问题),后来我还单做了一个没有加固的apk包供用户使用,但是有用户说没有加固的kk键盘依然会存在白板。

接着,后台bugly搜集上来很多崩溃日志,我发现了很多奇怪的framework层的崩溃栈,后来google后有人说是file descriptor泄漏导致的一些崩溃。于是再联想起来大白板的问题,我有点把怀疑对象放到文件句柄泄漏上了。

关于文件句柄泄漏,我知道的有几种原因(io流未关闭、handlerThread的启动、数据库cursor未关闭等),而一个进程的文件句柄个数记得上限是1024个,于是我在app刚启动时,不断循环创建handlerthread并且启动它。快到上限时,收起键盘,然后打开键盘,这个动作循环进行,我发现当文件句柄个数超出限制时,键盘就弹不出来了,等了一会儿,进程就重启了。在我所使用的手机上没有发现大白板的问题。

这个时候我有个紧急的任务要做,鸟哥接手这个问题并且开始研究。鸟哥把怀疑对象继续放到文件句柄泄漏上。发现了两个有价值的问题:1,键盘每次收起再抬起,进程的文件句柄个数就是上增2个;2,当通过不停创建handlerThread临近句柄个数上限时,收起键盘再抬起键盘,在oppo手机上发现了大白板。而这个大白板很像用户反馈的问题。

于是鸟哥就把键盘抬起和收起的代码逐一注释掉,通过排查去除法,找到了关键的方法调用会引起文件句柄泄漏,就是在键盘收起时,给逻辑层发送了一个消息导致的文件句柄泄漏。于是我和鸟哥去逻辑层找到执行这个消息的代码处,这个代码是调用内核来记忆用户输词的。我们把这款代码注释掉,再收起键盘抬起键盘,就没有发现文件句柄泄漏。

我写了一个自动化测试键盘点击的工具。该工具用python实现,通过python的uiautomator调用的adb,模拟用户点击行为,来快速的频繁的点击手机上的键盘。当我们把内核记忆用户输词的代码注掉后,跑该工具,模拟上万次点击键盘的时间,发现文件句柄个数上升的非常少。

于是我们怀疑是内核那个api的问题,后来鸟哥就联系搜狗的旧同事,帮忙找内核的问题。果然,搜狗那边说找到内核的那个api被调用时,有一处分支可能会导致文件io没有关闭。后来更得到消息,用户在平时敲击键盘时,内核也会默认保存用户输词,所以这时也有可能没有关闭文件io。

后来搜狗修改了内核,给我们提供了一个新的版本。我们把它应用上之后,经过了验证。然后把新版本的app投入到市场上。然后在之后的几天内没有得到用户的键盘版本的反馈,我们希望继续观察下去,直到最终验证我们的解决方案是否凑效。

结论

  1. 当出现文件句柄泄漏进而crash时,无法搜集到崩溃日志保存到本地。因为保存到本地需要建立io流,而文件句柄不够导致io流建立失败,所以无法保存崩溃日志到本地。
  2. 而出现大白板的原因,我这么理解的,文件句柄泄漏,导致进程分配的文件句柄已经耗尽,然后键盘抬起时,要把绘制的内容通过进程通信的方式传给Surface服务端,而进程通信依赖于文件句柄,导致整个传递失败了。