cover

遇见

大概几个月前,测试可靠性的同事上报了Launcher一个稳定性问题:有极低几率在Launcher主界面下按Rc任意按键(除音量键、Home、Power键外)均没反应,重新开机后恢复正常。

问题的难点在于没有规律,没法复现,出现几率低,没法定位,我们只是零星遇到,但总有人吐槽。

调查、查明、再跟进

好奇心促使我调查下去,尝试了各种方法,包括分析logcat,查看进程级别,Monkey随机事件流模拟,开发工程师自检代码,自动化强度操作等。

最终问题原因得以查明,这是因为Laucnher在某种情景下,失去了焦点。而至于这种情景是什么?我们根据前面技术调研得到的线索,猜测是预装的多屏互动应用,为了让自己处于前台,防止被杀死的机制有关。

我简单写了个demo模拟了”案发现场“:

若你用手机安装并运行Demo,你会发现手机好像”死机“了,点击桌面上任何图标都没反应,Menu键无效,Back键无效,锁屏再开屏也没戏。当然,按HOME键后手机就好了。

若你用智能设备(Tv、Android表)安装Demo,你还得安装完毕后返回Launcher主界面,并在机器终端输入以下命令手动调起应用,才能100%还原”案发现场“。

有兴趣的朋友可以下载体验下:

http://pan.baidu.com/s/1dDtpVCt

原因解析

简单来说,就是为了被系统回收或被用户清理最近任务时清理掉,以及某些未知目的,让应用通过某种手段伪装成用户可感知的进程(实际用户感知不到),做到不被杀。

具体过程跟大家分享下,欢迎讨论:

1. 防杀机制的原理是什么?

做Android开发的都知道,Android的窗口机制是以层级方式来管理的,正在运行的Activity处在栈的最顶端,盖住下面几层的Activity。另一个原则是后进先出,列表靠后的窗口会优先显示。

添加窗口有两种模式,一种依赖于Activity,另一种不用依赖于Activity,它们之间的区别在于:

  • 每添加新的Activity窗口new进入屏幕最上端时,就会从现有窗口列表找到当前屏幕最上端正在显示的Activity窗口old,然后将new添加在old之上,old就会被压入第二层。效果就是new窗口盖住了old窗口,见下:

顺序说明 原来的窗口列表 之后的窗口列表
屏幕最顶端 old new
第二层 路人 old
第n层 …… ……

  • 每添加新的非Activity窗口new进入屏幕最上方时,就会从现在窗口列表最后开始找,直到找到一个BaseLayer值比自己小的或等于它的窗口,然后将new添加在old之后,效果就是new窗口显示在old窗口之上,见下:

顺序说明 原来的窗口列表 之后的窗口列表
屏幕最顶端 old new+old
第二层 路人 路人
第n层 …… ……

而我的Demo里的Activity实现方式,正好满足第2种情况,它与当前屏幕上应用窗口的BaseLayer值一致,最后会添加在当前显示的Activity窗口之上。以后就算启动新的Activity,按照第1种情况,新的Activity也只会插在最上层Activity之上,XX窗口之下,从而达到了显示在 Activity之上的目的。

在技术调研多屏互动时,通过查看processes信息,我发现该应用在后台时,是Perceptible级别(用户可感知)的进程,原因是有前台服务。当系统清理后台应用时,此级别进程不会被清理。所以为了防止用户调用任务清理,我也在我的Demo中加了个服务,在显示悬浮窗时,启动该服务并设置为前台。

综上,就强力保证了自己的进程重要性最高,只有当系统内存低到很低,已经到达极限边缘到无法继续运行这些进程时,为了让UI继续有反应的话,系统才不得不杀死这些进程。

2.这种机制在实际项目中怎么运作?

这种窗口并非一直显示,当进入该应用界面时会被移除,当退出时又会被添加。可以参考鹅厂的聊天工具以及浏览器,它们均采用了这种机制,当系统清理后台应用时,此类进程不会被清理。

“在Android 5.0之前,从Recent Task列表杀掉一个App进程时,该App进程fork出来的子进程不会被杀掉。但是从Android 5.0开始,系统会通过cgroup来记录App进程fork出来的子进程,因此这时候从Recent Task列表杀掉一个App进程时,该App进程fork出来的子进程也会被杀掉。这样想玩后台常驻进程的App就没撤了。”——from资深Android源码分析专家罗升阳老师

3.为什么这种这么Bug的窗口能被正常添加?

要理解这个首先要理解Android如何管理窗口。

Android用WindowManagerService.java来管理着各式各样的窗口,并用WindowManager.java根据窗口的类型对窗口进行了分类,有以下三种类型:

类型 常量范围 例子
APPLICATION_WINDOW 1-99 大部分的应用程序窗口
SUB_WINDOW 1000-1999 SurfaceView,在小窗口显示时设为MEDIA, 全屏显示时设为PANEL
SYSTEM_WINDOW 2000-2999 电量不足提醒窗口、360安全卫士的浮动精灵

而在咱们这个Bug中,应用所创建的窗口类型为-2,这是什么类型?又是如何添加成功的呢?

首先我们知道,每添加一个新窗口时,Android都会检查你是否有申请对应的权限,通过了才能被添加,而且对该窗口提供了开关来控制是否显示。

可是这个权限检查并不严谨,完全靠开发者自觉。见下:

它没有对小于2000或者大于2999的窗口进行类型识别,默认绿灯放行,并会设置为与应用窗口一个级别处理。这也就解答了为什么这么Bug的窗口能被正常添加的问题。

进一步技术调研发现,这个逻辑从android 1.5版本就一直如此,最新的5.1.0_r1的源码中仍没有对方法进行修改。( ╯□╰ )

结尾

综合以上三点,我们对这个问题应该比较清楚了,添加的目的未知,也许是为了防止被杀。

目前该Bug是否得到修复,还有待日常做测试的同事去验证。

其实解决这个Bug还有另外的思路,可以修改Android系统层的代码来解决,不过使用了Android此漏洞的应用会受到影响。

最后多说两句,解决这个bug的意义有多大?假如一家公司卖出了一亿台设备,五百分之一的比例出现类似此问题,会有多少人遇到?假定每个用户每天都使用设备,一年下来又会遇到多少次?这样的问题是否值得深究?

参考文档:

  1. Android窗口管理服务WindowManagerService计算窗口Z轴位置的过程分析
  2. Android 的窗口管理系统 (View, Canvas, WindowManager)
  3. Android的进程与线程
支付宝扫码打赏 微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章

蔡培培's Picture
蔡培培

精于规划,热爱研究,自我实现者。目前专注于安卓测试架构、测试开发与移动安全。

Guangzhou「广州」 http://androidtest.org