Android滑动返回实现

August 25, 2019

背景

滑动退出最早出现在 iOS7,是系统提供的标准功能,一直沿用到现在。

Android 来说,更多是因为iOS出现该功能,产品经理为统一移动终端的 user experience 而跟进。相比 iOS在系统层完美的实现方式,Android无论用哪种方式,都需要在内存、处理器性能、流畅性之一作出牺牲。

方案

现时讨论最多的两种方案是:

  • 截屏图片模仿透明背景;
  • Window及Activity透明处理;

截屏图片

截屏图片 是截取当前屏幕的图片,打开新界面的时候传递给新界面。新界面把该图片作为底下一层界面的透视图。很多技术不错的同行提起过这种方案,不过这种方案不具备可用性。

Android图片多使用 RGB565 内存定义,按照流行屏幕尺寸 1920*1080 宽高各减半算内存占用:

(5 + 6 + 5) / 8 * (1920 / 2) * (1080 / 2) = 1, 036, 800Bytes = 1013KB

每个页面为保存截图都要占用近1MB内存,那截图时处理器占用,图片传递延迟,有考虑过吗?

Window及Activity透明处理

如果只能在两个不完美的方案里挑一个,我选这个方案。

流行开源库 SwipeBackLayout 也选择这种方案。不过这个工程从 2018.07.10 后再也没有维护,在此前提出的问题都没有修复,更不用说,对源码迁移到 AndroidX 这种要求。

除了对好些异常没有处理,本身透明逻辑定义的时间点也是有问题的。先看界面正常生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
I/MainActivity@bc1f0e: onCreate
I/MainActivity@bc1f0e: onStart
I/MainActivity@bc1f0e: onResume

I/MainActivity@bc1f0e: onPause
I/MainActivity@2e5c3a1d: onCreate
I/MainActivity@2e5c3a1d: onStart
I/MainActivity@2e5c3a1d: onResume
I/MainActivity@bc1f0e: onStop

I/MainActivity@2e5c3a1d: onPause
I/MainActivity@2a2ef203: onCreate
I/MainActivity@2a2ef203: onStart
I/MainActivity@2a2ef203: onResume
I/MainActivity@2e5c3a1d: onStop

I/MainActivity@2a2ef203: onPause
I/MainActivity@3d0b6459: onCreate
I/MainActivity@3d0b6459: onStart
I/MainActivity@3d0b6459: onResume
I/MainActivity@2a2ef203: onStop

以上周期大家都很熟悉,不再赘述。

SwipeBackLayout 异常生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
I/DemoActivity@bd11e3a: onCreate
I/DemoActivity@bd11e3a: onStart
I/DemoActivity@bd11e3a: onResume

I/DemoActivity@bd11e3a: onPause
I/DemoActivity@24fbcaab: onCreate
I/DemoActivity@24fbcaab: onStart
I/DemoActivity@24fbcaab: onResume

I/DemoActivity@24fbcaab: onPause
I/DemoActivity@378d89c8: onCreate
I/DemoActivity@378d89c8: onStart
I/DemoActivity@378d89c8: onResume

I/DemoActivity@378d89c8: onPause
I/DemoActivity@20264021: onCreate
I/DemoActivity@20264021: onStart
I/DemoActivity@20264021: onResume

I/DemoActivity@20264021: onPause
I/DemoActivity@2a17ef06: onCreate
I/DemoActivity@2a17ef06: onStart
I/DemoActivity@2a17ef06: onResume

所有已启动界面!所有已启动界面!在打开新界面后都不会走到 onStop,也就是说所有应该在 onStop 停止的操作、释放的资源,都没有机会触发。导致的结果是:当打开简单页面数量超过7个,就会出现新页面进场卡顿。

独立调研后,发现我的解决方案没法基于 SwipeBackLayout 进行修改。所以实现新开源库 phantomVK/SlideBack 达到期望技术目标。

新库对 反射操作页面过度绘制内存占用,和 生命周期异常 修复后的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
I/MainActivity@3b063dbb: onCreate
I/MainActivity@3b063dbb: onStart
I/MainActivity@3b063dbb: onResume

I/MainActivity@2304ed15: onStop
I/MainActivity@3b063dbb: onPause
I/MainActivity@2941f6d1: onCreate
I/MainActivity@2941f6d1: onStart
I/MainActivity@2941f6d1: onResume

I/MainActivity@3b063dbb: onStop
I/MainActivity@2941f6d1: onPause
I/MainActivity@b2f0dd7: onCreate
I/MainActivity@b2f0dd7: onStart
I/MainActivity@b2f0dd7: onResume

I/MainActivity@2941f6d1: onStop
I/MainActivity@b2f0dd7: onPause
I/MainActivity@238b744d: onCreate
I/MainActivity@238b744d: onStart
I/MainActivity@238b744d: onResume

和前文日志对比可以发现,新方案只有当前界面的下一级界面生命周期没有走到 onStop。具体原因可看 透明Activity生命周期变化。优化后界面都能正确释放资源,无论打开多少个新页面,不再有卡顿问题。

改进

反射优化

反射在Java操作中耗费性能,而且基于应用场景的原因不能在子线程内运行。对此有以下改进,把类方法查找操作放在静态初始化块内完成,后续无需每次使用都查找一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TranslucentHelper {

    private static Method sOptionsMethod;
    private static Method sInvokeMethod;
    private static Method sRevokeMethod;

    static {
        try {
            init();
        } catch (Throwable ignore) {
        }
    }

    private static void init() throws NoSuchMethodException {
        if (SDK_INT < KITKAT) return;

        Class<?>[] classes = Activity.class.getDeclaredClasses();
        Class<?> clz = null;
        for (final Class c : classes) {
            if (c.getSimpleName().equals("TranslucentConversionListener")) {
                clz = c;
                break;
            }
        }

        if (clz == null) return;
        if (SDK_INT >= LOLLIPOP) {
            sOptionsMethod = Activity.class.getDeclaredMethod("getActivityOptions");
            sOptionsMethod.setAccessible(true);

            sInvokeMethod = Activity.class.getDeclaredMethod("convertToTranslucent", clz, ActivityOptions.class);
            sInvokeMethod.setAccessible(true);
        } else {
            sInvokeMethod = Activity.class.getDeclaredMethod("convertToTranslucent", clz);
            sInvokeMethod.setAccessible(true);
        }

        sRevokeMethod = Activity.class.getDeclaredMethod("convertFromTranslucent");
        sRevokeMethod.setAccessible(true);
    }

    ....
}

生命周期

phantomVK/SlideBack 比SwipeBackLayout最大优化主要是生命周期的处理,即打开新 Activity 时把当前页面的透明背景取消掉。

而最合适的时间点,莫过于在 startActivityForResult()

1
2
3
4
5
@Override
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
    mManager.startActivityForResult(intent, requestCode, options);
    super.startActivityForResult(intent, requestCode, options);
}

页面对用户可操作时设置透明:

1
2
3
4
5
@Override
protected void onResume() {
    super.onResume();
    mManager.onResume();
}

个人认为在用户拖动边缘时才设置透明方案不妥当,因为反射对主线程的阻塞,会给用户拖动操作带来迟滞的感觉,最好是界面可见时就具备操作的能力。

其他优化

  • 源码库已迁移并适配 AndroidX
  • 继承父类为 AppCompatActivity
  • 内部已捕获异常 ArrayIndexOutOfBoundsException
  • 使用基于 Android28 定制 ViewDragHelper 工具类;
  • 更细致功能启用、停用开关控制,避免冗余内存开销;
  • 重写 findViewById 方法支持泛型;