×

​在 Android 上进行高刷新率渲染

消耗积分:2 | 格式:pdf | 大小:178.55KB | 2021-03-08

分享资料个

作者:Ady Abraham, Software Engineer
来源:谷歌开发者

长久以来,手机屏幕刷新率都是 60Hz。应用和游戏开发者也习惯了假定刷新率为 60Hz,也就是每 16.6ms 生成一帧,而且这样开发出来的应用和游戏都会正常进行。但现在的情况已经不同了。最新的旗舰级设备往往会搭载刷新率更高的屏幕,可以带来更流畅的动画效果、更低的延迟,从而获得更好的整体用户体验。还有一些设备支持可变刷新率,比如 Pixel 4,它支持 60Hz 和 90Hz 两种刷新率。

60Hz 的屏幕每 16.6ms 刷新一次显示内容。这意味着图像显示的时间是 16.6ms 的倍数 (16.6ms、33.3ms、50ms 等)。支持多种刷新率的屏幕则带来了更多的选择,这些屏幕能以不同的速度进行渲染,并且不会出现抖动。例如,一个无法维持 60fps 渲染的游戏,在 60Hz 的屏幕上必须一路降到 30fps 才能确保流畅无抖动 (因为显示器只能以 16.6ms 的倍数周期呈现图像,所以 60Hz 的下一档可用帧速是每 33.3ms 显示一帧,即 30fps)。而在 90Hz 设备上,同样的游戏只需要下降到 45fps (每帧 22.2ms) 即可,这就为用户带来了更流畅的体验。而同时支持 90Hz 和 120Hz 的设备,则可以用每秒 120、90、60 (120/2)、45 (90/2)、40 (120/3)、30 (90/3)、24 (120/5) 等帧率流畅地呈现内容。

高频率渲染

渲染频率越高,就越难维持帧率,因为只有更少的时间完成相同的工作量。要在 90Hz 下进行渲染,应用需要在 11.1ms 内生成一帧,与此相比,在 60Hz 时则有 16.6ms 来生成一帧。

为了详细说明这一点,我们来看看 Android UI 的渲染流水线。我们可以将帧渲染大致分为五个流水线阶段:
① 应用的 UI 线程处理输入事件,调用应用的回调,并更新视图 (View) 层次结构中记录的绘图命令列表;
② 应用的 RenderThread 将记录的命令发送到 GPU ;
③ GPU 绘制这一帧;
④ SurfaceFlinger 是负责在屏幕上显示不同应用窗口的系统服务,它会组合出屏幕应该最终显示出的内容,并将画面提交给屏幕的硬件抽象层 (HAL);
⑤ 屏幕最终呈现该帧的内容。

整个流水线由 Android Choreographer 控制。Choreographer 基于显示垂直同步 (vsync) 事件,它表示屏幕开始扫描出图像并更新显示像素的时间点。虽然 Choreographer 基于 vsync 事件,但对应用和 SurfaceFlinger 来说,其唤醒偏移量不同。下图展示了在 Pixel 4 设备上运行的流水线,应用在 vsync 事件后 2ms 被唤醒,SurfaceFlinger 则在 vsync 事件后 6ms 被唤醒。这样一来,应用产生一帧画面的时间为 20ms,SurfaceFlinger 组合画面内容的时间则为 10ms。


当以 90Hz 频率运行时,应用依然在 vsync 事件后 2ms 被唤醒。然而,SurfaceFlinger 在 vsync 事件后 1ms 被唤醒,同样有 10ms 的时间来合成屏幕内容。但这样一来应用只有 10ms 来渲染一帧画面,这时间就非常窘迫了:


为了缓解这种情况,Android 的 UI 子系统采用了预先渲染 (render ahead,指维持一帧的启动时间不变,但推迟其呈现时间) 来深化流水线,并将帧的呈现时间推迟一个 vsync。这样一来,应用可以有 21ms 的时间来生成一帧,同时确保维持 90Hz 的吞吐量。


一些应用,包括大多数游戏,都有自己自定义的渲染流水线。这些流水线可能会有更多或更少的阶段,具体取决于它们要完成的任务。一般来说,流水线越深,可以并行执行的阶段就越多,整体的吞吐量也会相应增加。但另一方面,这样可能会增加单帧的延迟 (延迟量为 number_of_pipeline_stages x longest_pipeline_stage)。这中间如何取舍需要开发者审慎考虑。

利用可变刷新率

如上所述,可变刷新率允许我们使用更多样的渲染频率。对于可以控制渲染速度的游戏,以及需要以特定速率呈现内容的视频播放器来说,这一点尤其有用。例如,要在 60Hz 的显示器上播放 24fps 的视频,我们需要使用 3:2 pulldown 算法,这就会产生抖动。但是,如果设备的屏幕可以原生显示 24fps 的内容 (24/48/72/120Hz),就无需使用 pulldown 算法,自然也就不会出现抖动了。

设备运行时的刷新率是由 Android 平台控制的。应用和游戏可以通过多种方法影响刷新率 (下面会有解释),但最终结果由平台决定。尤其是当屏幕上同时有多个应用时,这一点至关重要: 平台需要满足所有应用的刷新率需求。24fps 视频播放器就是一个很好的例子。24Hz 对于视频播放来说可能很好,但对于响应式 UI 来说就很糟糕了。如果一个推送通知的动画只有 24Hz,感觉就会很扎眼。在这种情况下,平台会选择让屏幕上的内容都显示良好的刷新率。

为此,应用可能需要知道当前设备的刷新率。可以通过以下方法来实现:

  • SDK
    通过 DisplayManager.DisplayListener 注册一个显示监听器,并通过 Display.getRefreshRate 查询刷新率。
  • NDK
    使用 AChoreographer_registerRefreshRateCallback 注册回调 (API 级别30)。

应用可以通过在其 Window 或 Surface 上设置帧率来影响设备刷新率。这是 Android 11 中引入的一个新功能,允许平台了解应用的渲染需求。应用可以调用以下方法之一:

关于如何使用这些 API,请参考帧率指南文档。

系统会根据 Window 或 Surface 上设置的帧率选择最合适的刷新率。

在较旧的 Android 版本 (Android 11 之前) 中并不存在 setFrameRate API,这时应用仍然可以通过直接将 WindowManager.LayoutParams.preferredDisplayModeId 设置为 Display.getSupportedModes 中的可用模式之一来影响刷新率。从 Android 11 开始,我们不建议大家采用这种方法,因为平台会不知道应用的渲染意图。例如,如果一个设备支持 48Hz、60Hz 和 120Hz,屏幕上有两个应用分别调用 setFrameRate(60, …) 和 setFrameRate(24, …),那么平台可以选择 120Hz 来同时满足这两个应用。而如果这些应用使用了 preferredDisplayModeId,它们很可能会把模式设置为 60Hz 和 48Hz,那这时平台就无法使用 120Hz 了。这时平台只能从 60Hz 或 48Hz 中选择一个,从而影响到另一个应用的显示效果。

总结

刷新率不一定是 60Hz——不要想当然地认为它一定会是 60Hz,也不要基于历史经验作出硬性假设。

刷新率并不总是恒定的——如果您想了解实际的刷新率,就需要注册一个回调来知晓刷新率的变动,并相应地更新您应用内部的数据。

如果您没有使用 Android UI 工具包,而使用自定义的渲染器,请考虑根据当前的刷新率来改变您的渲染流水线。通过使用 OpenGL 上的 eglPresentationTimeANDROID 或 Vulkan 上的 VkPresentationTimesInfoGOOGLE 设置一个呈现时间戳,即可深化流水线。设置呈现时间戳可以向 SurfaceFlinger 指示何时呈现图像。如果设置为未来的几帧,它就会按照设置的帧数加深流水线。前文例子中的 Android UI 将呈现时间设置成了 frameTimeNanos + 2 * vsyncPeriod。

注: frameTimeNanos 从 Choreographer 获取;vsyncPeriod 从 Display.getRefreshRate() 获取。

使用 setFrameRate API 告诉平台您的渲染意图,平台会选择合适的刷新率来匹配不同的需求。

您应该只在必要时才使用 preferredDisplayModeId: 当 setFrameRate API 不可用时,或是当您需要使用非常特定的模式时。

最后,请您深入了解一下 Android 的帧同步库。这个库可以为您的游戏妥善处理帧同步,并使用前文中的方法来处理多种刷新率。

本文转自: 谷歌开发者(Google_Developers),作者:Ady Abraham, Software Engineer,
转载此文目的在于传递更多信息,版权归原作者所有。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !

'+ '

'+ '

'+ ''+ '
'+ ''+ ''+ '
'+ ''+ '' ); $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code ==5){ $(pop_this).attr('href',"/login/index.html"); return false } if(data.code == 2){ //跳转到VIP升级页面 window.location.href="//m.obk20.com/vip/index?aid=" + webid return false } //是会员 if (data.code > 0) { $('body').append(htmlSetNormalDownload); var getWidth=$("#poplayer").width(); $("#poplayer").css("margin-left","-"+getWidth/2+"px"); $('#tips').html(data.msg) $('.download_confirm').click(function(){ $('#dialog').remove(); }) } else { var down_url = $('#vipdownload').attr('data-url'); isBindAnalysisForm(pop_this, down_url, 1) } }); }); //是否开通VIP $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code == 2 || data.code ==5){ //跳转到VIP升级页面 $('#vipdownload>span').text("开通VIP 免费下载") return false }else{ // 待续费 if(data.code == 3) { vipExpiredInfo.ifVipExpired = true vipExpiredInfo.vipExpiredDate = data.data.endoftime } $('#vipdownload .icon-vip-tips').remove() $('#vipdownload>span').text("VIP免积分下载") } }); }).on("click",".download_cancel",function(){ $('#dialog').remove(); }) var setWeixinShare={};//定义默认的微信分享信息,页面如果要自定义分享,直接更改此变量即可 if(window.navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){ var d={ title:'​在 Android 上进行高刷新率渲染',//标题 desc:$('[name=description]').attr("content"), //描述 imgUrl:'https://'+location.host+'/static/images/ele-logo.png',// 分享图标,默认是logo link:'',//链接 type:'',// 分享类型,music、video或link,不填默认为link dataUrl:'',//如果type是music或video,则要提供数据链接,默认为空 success:'', // 用户确认分享后执行的回调函数 cancel:''// 用户取消分享后执行的回调函数 } setWeixinShare=$.extend(d,setWeixinShare); $.ajax({ url:"//www.obk20.com/app/wechat/index.php?s=Home/ShareConfig/index", data:"share_url="+encodeURIComponent(location.href)+"&format=jsonp&domain=m", type:'get', dataType:'jsonp', success:function(res){ if(res.status!="successed"){ return false; } $.getScript('https://res.wx.qq.com/open/js/jweixin-1.0.0.js',function(result,status){ if(status!="success"){ return false; } var getWxCfg=res.data; wx.config({ //debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId:getWxCfg.appId, // 必填,公众号的唯一标识 timestamp:getWxCfg.timestamp, // 必填,生成签名的时间戳 nonceStr:getWxCfg.nonceStr, // 必填,生成签名的随机串 signature:getWxCfg.signature,// 必填,签名,见附录1 jsApiList:['onMenuShareTimeline','onMenuShareAppMessage','onMenuShareQQ','onMenuShareWeibo','onMenuShareQZone'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); wx.ready(function(){ //获取“分享到朋友圈”按钮点击状态及自定义分享内容接口 wx.onMenuShareTimeline({ title: setWeixinShare.title, // 分享标题 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享给朋友”按钮点击状态及自定义分享内容接口 wx.onMenuShareAppMessage({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 type: setWeixinShare.type, // 分享类型,music、video或link,不填默认为link dataUrl: setWeixinShare.dataUrl, // 如果type是music或video,则要提供数据链接,默认为空 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ”按钮点击状态及自定义分享内容接口 wx.onMenuShareQQ({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到腾讯微博”按钮点击状态及自定义分享内容接口 wx.onMenuShareWeibo({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ空间”按钮点击状态及自定义分享内容接口 wx.onMenuShareQZone({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); }); }); } }); } function openX_ad(posterid, htmlid, width, height) { if ($(htmlid).length > 0) { var randomnumber = Math.random(); var now_url = encodeURIComponent(window.location.href); var ga = document.createElement('iframe'); ga.src = 'https://www1.elecfans.com/www/delivery/myafr.php?target=_blank&cb=' + randomnumber + '&zoneid=' + posterid+'&prefer='+now_url; ga.width = width; ga.height = height; ga.frameBorder = 0; ga.scrolling = 'no'; var s = $(htmlid).append(ga); } } openX_ad(828, '#berry-300', 300, 250);