cover

奇怪的Bug

几个星期前,系统开发工程师上报了一个很奇怪的问题:在某512内存的方案X上打开我们的应用A,从A启动系统设置应用的时间设置Activity去修改时间,修改后返回到A,A挂掉了。

这个问题奇怪之处,据他说:

  • 不联网情况下,修改了时间,返回到应用A,应用A正常;
  • 联网情况下,不修改时间的值,返回到应用A,应用A正常;
  • 只有在联网+修改了时间的值的情况下,返回到应用A,应用A挂掉;

经验证,还发现一些“匪夷所思”的线索:

  • 其他(包括同等硬件配置的)方案均没复现到该问题;
  • 应用A有另外一处启动系统设置应用查看信息然后返回A的地方,这一处有概率在方案X上有概率会挂,其他方案正常;

最终问题原因得以查明,这是因为:

从应用A启动应用B,当系统由于要回收内存而把应用A的 Activity 销毁后,按返回键返回到应用A的 Activity,应用A的Activity生命周期走的路径和正常情况走的路径不一样,应用A部分非永久性的数据没初始化就被使用,导致挂掉。

故顺便写出来记录一下。

知识点讲解

首先得从Activity生命周期讲起,最经典的莫过于官方的图解了:

full-img

根据图解,Activity从出生到死亡会依次经历:

onCreate –> onStart –> onResume –> onPause –> onStop –> onDestroy

对各个方法的说明如下:

方法 说明
onCreate、 onStart onCreate方法会在第一次创建的时候执行,紧接着便会执行onStart方法,之后页面被完全遮挡会执行onStop方法,再返回的时候一般便会执行onRestart –> onStart方法, 但是如果如果这时候App内存不够需要更多的内存的时候,App便会杀死该进程,结束掉该Activity,所以这时候再返回的时候便会重新执行onCreate –> onStart –> onResume方法。
onPause、 onStop onPause是在整个窗口被半遮盖或者半透明的时候会执行,而onStop则是在整个窗口被完全遮盖才会触发, 触发onStop的方法之前必定会触发onPause方法。
onResume onResume方法一般用来”恢复之前的现场” 。执行到onStart方法时Activity用户可见,用户可以看到部分Activity但不能与它交互,但执行到onResume时Activity已经获得用户焦点,可以与用户交互。
onDestroy onDestory()方法是Activity的生命周期中的最后一步,执行该方法,系统将销毁了这个Activity的实例在内存中占据的空间。当重新进入此Activity的时候,会执行onCreate()方法重新创建该Activity。

相信很多人都已经知道以上方法与执行顺序,但是Activity还有其他方法,如onContentChanged, onPostCreate, onPostResume, onConfigurationChanged, onSaveInstanceState, onRestoreInstanceState,知道这些方法在Activity生命周期中什么时候执行很重要,所以我们还需要理解下图:

full-img

结合两张图解,得到应用从出生到死亡的详细执行过程为:

onCreate –> onContentChanged –> onStart –> onPostCreate –> onResume –> onPostResume –> onPause –> onStop –> onDestroy

对各个方法的说明如下:

方法 说明
onContentChanged onContentChanged方法是Activity中的一个回调方法。当Activity的布局改动时,即setContentView()或者addContentView()方法执行完毕时就会调用该方法, 例如,Activity中各种View的findViewById()方法都可以放到该方法中。
onPostCreate、onPostResume onPostCreate方法是指onCreate方法彻底执行完毕的回调,onPostResume类似,这两个方法官方说法是一般不会重写,现在知道的做法也就只有在使用ActionBarDrawerToggle的使用在onPostCreate需要在屏幕旋转时候等同步下状态。
onSaveInstanceState onSaveInstanceState字面理解就是保存实例的状态,当某个Activity变得“容易”被系统销毁时,该Activity的onSaveInstanceState就会被执行。除非该Activity是被用户主动销毁的,比如当用户按Back键的时候。
“容易被销毁”言下之意就是该activity还没有被销毁,而仅仅是一种可能性。这种可能性有这几种情况:
1. 当用户按下HOME键时。这是显而易见的,系统不知道你按下HOME后要运行多少其他的程序,自然也不知道activity A是否会被销毁,故系统会调用onSaveInstanceState,让用户有机会保存某些非永久性的数据。以下几种情况的分析都遵循该原则。
2. 长按HOME键,选择运行其他的程序时;
3. 按下电源按键(关闭屏幕显示)时;
4. 从activity A中启动一个新的activity时;
5. 屏幕方向切换时,例如从竖屏切换到横屏时。在屏幕切换之前,系统会销毁activity A,在屏幕切换之后系统又会自动地创建activity A,所以onSaveInstanceState一定会被执行。
总而言之,onSaveInstanceState的调用遵循一个重要原则,即当系统“未经你许可”时销毁了你的activity,则onSaveInstanceState会被系统调用,这是系统的责任,因为它必须要提供一个机会让你保存你的数据(当然你不保存那就随便你了)。
onRestoreInstanceState onSaveInstanceState字面理解就是恢复实例的状态, 需要注意的是,onSaveInstanceState方法和onRestoreInstanceState方法“不一定”是成对的被调用的,onRestoreInstanceState被调用的前提是,activity A“确实”被系统销毁了,而如果仅仅是停留在有这种可能性的情况下,则该方法不会被调用,例如,当正在显示activity A的时候,用户按下HOME键回到主界面,然后用户紧接着又返回到activity A,这种情况下activity A一般不会因为内存的原因被系统销毁,故activity A的onRestoreInstanceState方法不会被执行。不过大多数情况下也是很少使用onRestoreInstanceState方法的,经常我们还是在onCreate方法里直接恢复状态的,onCreate方法里本身会有一个Bundle参数的,很多时候我们是这样使用的。(onCreate在onStart之前调用,而onRestoreInstanceState是在onStart之后调用)

为了便于大家更好的理解,我简单的写了一个Demo,不明白Activity周期的朋友们,可以亲手实践一下:

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

结合具体的使用场景来分析Activity生命周期,可以得到下表:

使用场景 执行方法
第一次启动该应用 onCreate –> onContentChanged –> onStart –>
onPostCreate –> onResume –> onPostResume
按返回键离开当前Activity onPause –> onStop –> onDestroy
按Home键回到Launcher onPause –> onSaveInstanceState –> onStop
(续上)再次打开该应用 onRestart –> onStart –> onResume –> onPostResume
从ActivityA打开ActivityB onPause –> onSaveInstanceState –> onStop
(续上)在ActivityB按返回键回到ActivityA onRestart -> onStart -> onResume

Bug解析

理解了以上的知识点,再来分析咱们的问题的发生原因。

通过Memory Monitor以及查看logcat日志,发现若从A启动B时A被GC(又称内存回收)了,按返回键返回A时A会挂掉。

前文提到的某个系统开发工程师所说:

  • 不联网情况下,修改了时间,返回到应用A,应用A正常;
  • 联网情况下,不修改时间的值,返回到应用A,应用A正常;
  • 只有在联网+修改了时间的值的情况下,返回到应用A,应用A挂掉;

是因为该方案仍在研发当中,系统非常不稳定,经常占用内存过高,导致应用很容易被回收。而刚好"联网+修改时间"加大了系统的负担,触发了应用A被内存回收。

ActivityA打开ActivityB, 这时候ActivityA的生命周期的方法是这样的:onPause –> onSaveInstanceState –> onStop,这时候在ActivityB按返回键,一般ActivityA在正常情况下会执行:

onRestart -> onStart -> onResume

Activity在onPause或者onStop状态下都有可能遇到由于突发事件系统需要回收内存。当系统由于要回收内存而把应用A的Activity销毁时,之后的onDestroy方法便不会再执行,这时候会执行:

onCreate –> onStart –> onRestoreInstanceState –> onResume

这时应用A的Activity生命周期走的路径和正常情况走的路径不一样,开发人员在写代码时没有考虑到这种情况,应用A部分非永久性的数据没初始化就被使用,导致应用A挂掉。

修复Bug

知道了why,how也就简单了:

  • 重写onSaveInstanceState方法保存实例;

full-img

  • 又或者更简单粗暴的:检测到内存过低,在onCreate(Bundle savedInstanceState) 方法里将savedInstanceState的值设为null。

full-img

参考资料

  1. Activities | Android Developers
  2. Activity | Android Developers
  3. 两分钟彻底让你明白Android Activity生命周期(图文)!
支付宝扫码打赏 微信打赏

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

扫描二维码,分享此文章

蔡培培's Picture
蔡培培

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

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