bug 描述
- 项目:AnkiDroid (Anki for Android)
- 简述:进出分屏模式时,EditText 中输入的文字出现变化。
- 详细:issue#5660
bug 分析
分屏模式是 Android 7.0 新增的功能,在手机上表现为两个 App 上下分开,同时显示在前台。但同一时间只能有一个 App 响应用户交互,用户可以点击切换可交互的 App。
进入和离开分屏模式时,Activity 会触发重建流程,保证在当前窗口尺寸中正确显示,简要的生命周期输出如下:
(找了两个之前的demo项目做测试,新建的空Activity,仅输出一些生命周期回调的日志)
进入分屏模式时:

- 1、全屏打开App1(webrtc)
- 2、使用 RECENT 进入多任务切换页面
- 3、对App1启用分屏模式,等待选择分屏显示的另一个App
- 4、点击图标启动App2(post)
分屏模式下的焦点切换:

离开分屏模式(将其中一个App拖动成全屏):

进入和离开分屏模式的时候都触发了Activity的重建流程,
EditText 在进出分屏模式的时候需要保存已输入内容并重新 setText,如果内容发生了变化,那应该是「存」或「取」的过程中发生的问题,可以从 onSaveInstanceState 和 onCreate 开始查起。
排查过程
- 复现问题,找到对应代码:NoteEditor(是一个Activity)
整个 Activity 虽然看起来不太复杂,但涉及多处数据修改,业务逻辑并不简单,代码量也不算小。由于 bug 只涉及两个文本输入框,应该不需要阅读全部代码,NoteEditor 中重写了 onSaveInstanceState,也有处理 onCreate 的参数,第一步就是输出保存的和读取到的数据。
(logcat 的截图丢了,调试代码已经删除,不想再写一遍了,onSaveInstanceState 存的值和 onCreate 中取到的值都是正确的)
有点疑惑,出入的数据居然没有问题,但页面上确实出现了偏差。搜索一下 FieldEditText 对象,发现使用的位置有点多,不适合逐个排查。(实际上还是做了一部分排查,但效果不好)
于是把思路转移到 FieldEditText 内部,如果通过调用 setText 的方式修改了文案,那可以重写 setText,通过在其中抛出异常的方式获取方法调用栈,从而快速定位问题。EditText 的 setText 方法有两个,只有一个参数的 setText 是 final 的,但内部还是调用到了两个参数的 setText。重写 setText 并加入异常代码:

因为setText会被多次调用,此处做了一个判断,我在 Front 的输入框输入 "front",Back 中输入 "back",按照 bug 描述,当 Front 输入框被赋值成 "back" 时就是产生 bug 的那次调用。运行起来:

居然不在 NoteEditor 里…怪不得之前的排查数据都没出现错误。看来问题出在 EditText 内部。那么问题变复杂了,如果只写一个 EditText,整个 Activity 重建时自动恢复的数据不会错误,错误的数据是哪里来的呢?这就需要研究 View 的保存和恢复机制了。
先不看源码,View 对象在 Activity 重建前后必然经历的回收和新建,在两个不直接相关的对象中传递数据,必定需要一个能连接二者的标识。NoteEditor 的代码中两个 EditText 是通过同一个 xml 文件创建的,结合 bug 的现象,初步猜测是通过 View 的 id 保存和恢复数据,所以保存时第二个 EditText 的 text 覆盖了第一个 EditText 的 text。
上源码验证猜想:
EditText 并未重写 onSaveInstanceState,直接跳到 TextView:

在需要保存的情况下,会保存 text 相关的内容,没发现能作为标识的值。再向上看一个 super。

保存的值中出现了 mAutofillViewId。这个 id 是为了统一用户手动设置(比如xml中指定)的 id 和自动生成的 id,是一个 int 变量。
然后是取值恢复,onRestoreInstanceState 的参数是之前保存的数据,那么调用 onRestoreInstanceState 的地方需要获取一个 state,根据之前的经验(保存是在 View.java 中做的),应该看 View 类中调用 onRestoreInstanceState 的地方。

再找 mID 的赋值处,发现其中之一是获取 xml 中的 id 属性:

可以确认,该 bug 是由于复用 xml 文件导致的 EditText 的 id 相同,在 onSaveInstanceState 时发生了数据的覆盖,恢复数据时两个 EditText 就变成了同样的内容。
解决方式应该需要自定义数据的保存和恢复,但这些内容 NoteEditor 已经处理过了,所以只需要把 FieldEditText 中的 onSaveInstanceState 禁用掉就没问题了。(禁用 onRestoreInstanceState 也行)

然后自测一下就可以提交PR了~
技术总结
- 调查一个方法的调用时机,除了Ctrl+左键和全局搜索,还有通过抛出异常直接查看调用栈这种更直接的方法。(搜索代码的方式有范围限制,不够全面)
- View 自身保存和恢复状态时是依据 id (应该还有所属 Activity)确认是否为同一 View 的。复用 xml 时需要注意自己实现数据的保存和恢复。
最后
春节快乐