关于android:Android-突破屏幕刷新的桎梏

42次阅读

共计 5729 个字符,预计需要花费 15 分钟才能阅读完成。

背景

随着智能手机的遍及,古代生存中,咱们慢慢解脱不了对手机的依赖。出行、购物、医疗、住房、社交等社会各个层面的需要都离不开借助智能手机去实现更高效、便捷的目标。短短十几年,依靠于智能手机的倒退也呈现了层出不穷的互联网公司、手机品牌厂商以及数不清的应用程序,而随着相干互联网产业市场慢慢达到人口瓶颈,基于存量市场的抢夺也迎来了白热化的阶段。产业内卷的一角,是各大手机品牌的竞争,除了以“换壳为主”作为次要的出新策略,各大厂商也纷纷搜索枯肠新陈代谢,诸如折叠屏、高刷新、快充、亿级相机像素等软硬件迭代也屡见于各种新机发布会上。据不齐全统计,仅 2021 年,Android 营垒的品牌厂商在寰球范畴内就公布了 502 款机型设施(数据起源:gsmarena)。先不探讨内卷的背地是对事实的焦虑,国产手机品牌对于新技术、新硬件的摸索以及略显激进的将其利用,集体认为曾经很“上进”了(此处不得不 cue 一下当初连壳都懒得换的 Apple【手动狗头.jpg】)。

回归本文的主题,作为一名 Android 开发者,除了对各类形形色色碎片化重大的机型和零碎进行适配以外,还有一块重点工作是让本人开发的应用程序能用上品牌厂商们主打的新个性来加强利用的用户体验。像对折叠屏的适配,利用多屏小窗口的个性来减少用户多任务的互动感。

(网络图,侵删 )

对相机超高像素的适配来丰盛用户拍照的体验以及针对反对高刷新率的手机屏幕,对利用和游戏进行适配,减少用户的晦涩体验。本文从这些新个性中选取了一点,来重点讲述基于 Android 零碎的刷新机制,如何来适配目前市面上的一些高刷新率手机,以取得更好的用户体验。

(网络图,侵删 )

家喻户晓,当初市面上大部分支流的机型的屏幕刷新率还停留在 60Hz,即屏幕以 1000ms/60 约为 16.6ms 的速度刷新一次。而当初一些高配手机曾经能够达到 90Hz 甚至是 120Hz,从上个动图也能够看出,不同的刷新率,频率越高,给用户的感官体验也更加顺滑。这里交叉一个小常识,大家晓得电影也是由一帧帧间断画面制作而成,那么电影的刷新帧率是多少?24fps!也就是说,只有用每秒 24 个画面的速度去播放间断单个画面片段,大脑就会主动联想成是一个间断的画面。那么为什么李安导演还要尝试拍摄 120fps 的《比利·林恩的中场战事》(《Billy Lynn’s Long Halftime Walk》)?从受众角度登程,120fps 电影带来的感触远比 24fps 来的震撼和深入人心,特地对于一些巨大的战争场面或者叙事布景,24fps 的高速画面在过渡的时候会呈现含糊(动静含糊,motion blur)景象,但 120fps 能让这些含糊过渡以更清晰的画面出现在观众背后,观众能更有代入感。对于一些反对高刷新的游戏也是同理。

屏幕刷新机制

在 Android 零碎中,针对屏幕的 UI 渲染刷新流水线(rendering pipeline)能够大抵分为 5 个阶段:

  • 阶段 1:利用的 UI 线程解决输出事件、调起属于利用的相干回调以及更新记录了相干绘画指令的 View 层次结构列表;
  • 阶段 2:利用的渲染线程(RenderThread)将解决后指令发送给 GPU;
  • 阶段 3:GPU 绘制该帧数据;
  • 阶段 4:SurfaceFlinger 是负责在屏幕上显示不同利用窗口的零碎服务,它会组合出屏幕应该最终显示出的内容,并将帧数据提交给屏幕的硬件形象层 (HAL);
  • 阶段 5:屏幕显示该帧内容。

Android 采纳的是双缓冲策略,通过垂直同步信号 vsync 来保障前后缓存的最佳替换机会。后面有提到两个概念,一个是帧率,一个是刷新频率。一种比拟现实的状态是,帧率和刷新频率保持一致,GPU 刚解决完一帧,刷到屏幕上,下一帧就筹备好了,这时候前后帧数据的间断残缺的。但数据从 CPU 传递给 GPU 过程不可控,当屏幕刷新时,如果取到的帧 buffer 并非是残缺 ready 的状态,就会呈现很早之前非智能手机或者老旧电视机常呈现的屏幕画面撕裂的问题(比方屏幕中上一半是之前的画面,下一半是新的画面 )。双缓冲的策略解决的就是这个问题,通过保护两个缓冲区,让前缓冲区负责将帧数据运送到屏幕上的同时,在后缓冲区筹备下一帧的渲染对象,通过 vsync 信号来开关是否将后缓冲区数据交换到前缓冲区。

咱们再来看一下 Android 的源码实现
Choreographer$FrameDisplayEventReceiver):

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
        
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;

    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {super(looper, vsyncSource);
    }
        
    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
        // 将 vsync 事件通过 handler 发送
        long now = System.nanoTime();
        if (timestampNanos > now) {Log.w(TAG, "Frame time is" + ((timestampNanos - now) * 0.000001f)
                        + "ms in the future!  Check that graphics HAL is generating vsync"
                        + "timestamps using the correct timebase.");
            timestampNanos = now;
        }
        // 判断是否还有挂起的信号未被解决
        if (mHavePendingVsync) {
            Log.w(TAG, "Already have a pending vsync event.  There should only be"
                        + "one at a time.");
        } else {mHavePendingVsync = true;}

        mTimestampNanos = timestampNanos;
        // 替换新的帧数据
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

    @Override
    public void run() {
        mHavePendingVsync = false;
        // 帧数据处理
        doFrame(mTimestampNanos, mFrame);
    }
}

咱们再从利用的角度通过 systrace 工具再直观的感受一下 Android 的刷新机制:

上图中,每一个垂直灰度区为一次 vsync 信号的同步,每个灰度区为 16.6ms。换句话说,只有 UI thread 以及 RenderThread 的一系列办法援用和执行在一个灰度区内执行实现了,那么这一帧的数据渲染就能够在 16.6ms 内实现。咱们再看下图将这些信号放大后的一段异样渲染逻辑,红框中的渲染显著曾经超过了一个 vsync 信号的边界,也就是说这帧数据的渲染耗时过长,在一个信号周期内没有执行实现,那么这一帧的执行反映到代码执行就是有问题的。Android 个别会通过剖析相干函数的堆栈来定位问题呈现的中央,如红框中的问题是由 RPDetectCoreView 这个自定义视图引起。如果利用运行一段时间内,这种状况频繁呈现,造成的用户观感就是利用的卡顿和掉帧景象。

高刷的适配

理解了 Android 的根本刷新机制,咱们再看一下如何来适配目前带了高刷属性的机型。利用或者游戏能够通过 Android 官网的 SDK/NDK API 来影响屏幕的刷新率,为什么说是“影响”而不是“决定”?后面也提到了 CPU/GPU 的写入速度是不可控的,所以这些办法也只能是去影响屏幕的帧率。

应用 Android 自带的 SDK

适配高刷的时候,咱们会像个别注册设施传感器那样,先晓得设施自身反对哪些传感器,再进行具体的采集动作。同样,对于屏幕刷新率,咱们也得先晓得屏幕反对的刷新率以及以后的刷新率,再去可控地调节成利用想要的刷新率。<br /

那么获取刷新率的办法有如下两种,两种都是通过注册监听器实现:

  1. DisplayManager.DisplayListener 通过 Display.getRefreshRate() 查问刷新率
// 注册屏幕变动监听器
public void registerDisplayListener(){DisplayManager displayManager = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
    displayManager.registerDisplayListener(new DisplayManager.DisplayListener() {
        @Override
        public void onDisplayAdded(int displayId) { }

        @Override
        public void onDisplayRemoved(int displayId) { }

        @Override
        public void onDisplayChanged(int displayId) {}}, dealingHandler);
}

// 获取以后刷新率
public double getRefreshRate() {return ((WindowManager) mContext
      .getSystemService(Context.WINDOW_SERVICE))
      .getDefaultDisplay()
      .getRefreshRate();}
  1. 还能够通过 NDK 的 AChoreographer_registerRefreshRateCallback API
void AChoreographer_registerRefreshRateCallback(
  AChoreographer *choreographer,
  AChoreographer_refreshRateCallback,
  void *data
)

void AChoreographer_unregisterRefreshRateCallback(
  AChoreographer *choreographer,
  AChoreographer_refreshRateCallback,
  void *data
)

在获取了屏幕可用刷新率之后,就能够尝试依据业务需要去设置刷新率,办法都很简略,这里不再做 sample code 阐明:

  1. 应用 SDK 的 setFrameRate() 办法

    1. Surface.setFrameRate
    2. SurfaceControl.Transaction.setFrameRate
  2. 应用 NDK 的 _setFrameRate 函数

    1. ANativeWindow_setFrameRate
    2. ASurfaceTransaction_setFrameRate

      FramePacingLibrary(FPL,帧同步库)

这里再介绍一下 FramePacingLibrary(别名 Swappy)。Swappy 能够帮忙基于 OpenGL 以及 Vulkan 渲染 API 的游戏进行晦涩的渲染和帧同步。这里又有一个帧同步的概念须要阐明一下。帧同步,上文咱们提到,Android 的整个渲染管道是 CPU 到 GPU 再到 HAL 屏幕显示硬件,这里的帧同步就是指 CPU 的逻辑运算和 GPU 的渲染与操作系统的显示子系统和底层显示硬件之间的同步。
为什么 Swappy 实用于游戏,因为游戏蕴含大量的 CPU 计算以及渲染工作,这些计算策略通过从 0 到 1 的开发显然是老本很高的一个工作,所以 Swappy 像市面上大多数的游戏引擎那样(Unity、Unreal 等等)提供了现成的策略机制来帮忙游戏更好更容易的进行开发。

它能够做到:

  • 给每帧渲染增加出现的工夫戳,按时来显示帧画面,弥补因为游戏帧较短而呈现的卡顿景象
  • 将锁机制注入到程序中,让显示的流水线能跟上进度,不会积攒过多导致长帧的卡顿和提早(同步栅栏)
  • 提供了帧统计信息来调试和分析程序

通过一些图片来简略形容一下它的原理:下图是一个现实的在 60Hz 设施上运行 30Hz 的帧同步,这里每一帧(A\B\C\D)都“恰到好处”地失常渲染到了屏幕上。 但事实中并不都是这种状况,对与一些短的游戏帧,如下图中的 C 帧,因为耗时更短,导致 B 帧还没有齐全显示应有的帧数就被 C 帧领先,同时 B 帧的 NB 信号又触发了 C 帧的再次显示,就像跑步的人被路上的小石子绊倒,以一个固定的姿态摔出几米一样,后续都会显示 C 帧,导致卡顿。

而 Swappy 库通过减少出现工夫戳解决了这个问题,就像给每一帧数据设置了一个闹钟,闹钟不响就不容许显示在屏幕上:

总结

尽管适配高刷的个性不须要做太多的代码适配,然而还是必须要认真思考上面几方面问题:

  1. 在通过 setFrameRate() 办法设置屏幕刷新率时,还是会存在设置无奈失效的状况,像有更高优先级的 Surface 有不同的帧率设置,或者设施处于省电模式等,因而咱们在开发程序时也必须思考到如果设置生效的状况下,程序也能失常运行才能够;
  2. 防止频繁调用 setFrameRate() 办法,在每帧过渡的时候,如果频繁调用会引起掉帧问题,咱们须要提前通过 API 获取应有的信息来一次性调整到正确的帧率;
  3. 不要固定写死特定的帧率,而应该依据理论的业务场景来调整帧率的设置策略,指标是无缝地在高下屏幕刷新率之间过渡。

参考

High Refresh Rate Rendering on Android

Frame Pacing Library

Android Frame-rate

作者:ES2049 / 黎明

文章可随便转载,但请保留此原文链接。

十分欢送有激情的你退出 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com。

正文完
 0