共计 4721 个字符,预计需要花费 12 分钟才能阅读完成。
作为一款 VR 实时操作游戏 App,咱们须要依据重力感应零碎,实时监控手机的角度,并渲染出相应地位的 VR 图像,因而在不同 Android 设施之间,因为应用的芯片组和不同架构的 GPU,游戏性能会因而受到影响。举例来说:游戏在 Galaxy S20+ 上可能以 60fps 的速度渲染,但它在 HUAWEI P50 Pro 上的体现可能与前者天壤之别。因为新版本的手机具备良好的配置,而游戏须要思考基于底层硬件的运行状况。
如果玩家遇到帧速率降落或加载工夫变慢,他们很快就会对游戏失去趣味。
如果游戏耗尽电池电量或设施过热,咱们也会散失处于长途旅行中的游戏玩家。
如果提前预渲染不必要的游戏素材,会大大增加游戏的启动工夫,导致玩家失去急躁。
如果帧率和手机不能适配,在运行时会因为手机自我爱护机制造成闪退,带来极差的游戏体验。
基于此,咱们须要对代码进行优化以适配市场上不同手机的不同帧率运行。
所遇到的挑战
首先咱们应用 Streamline
获取在 Android 设施上运行的游戏的配置文件,在运行测试场景时将 CPU 和 GPU 性能计数器流动可视化,以精确理解设施解决 CPU 和 GPU 工作负载,从而去定位帧速率降落的次要问题。
以下的帧率剖析图表显示了应用程序如何随工夫运行。
在上面的图中,咱们能够看到执行引擎周期与 FPS 降落之间的相关性。显然 GPU 正忙于算术运算,并且着色器可能过于简单。
为了测试在不同设施中的帧率状况,应用 友盟 +U-APM测试不同机型上的卡顿情况,发现在 onSurfaceCreated
函数中进行渲染时呈现卡顿,应证了前文的剖析,能够确定 GPU 是在算数运算过程中产生了卡顿:
因为不同设施有不同的性能预期,所以须要为每个设施设置本人的性能估算。例如,已知设施中 GPU 的最高频率,并且提供指标帧速率,则能够计算每帧 GPU 老本的相对限度。
数学公式: $ 每帧 GPU 老本 = GPU 最高频率 / 指标帧率 $
CPU 到 GPU 的调度存在肯定的束缚,因为调度上存在限度所以咱们无奈达到目标帧率。
另外,因为 CPU-GPU 接口上的工作负载序列化,渲染过程是异步进行的。
CPU 将新的渲染工作放入队列,稍后由 GPU 解决。
数据资源问题
CPU 管制渲染过程并且实时提供最新的数据,例如每一帧的变换和灯光地位。然而,GPU 解决是异步的。这意味着数据资源会被排队的命令援用,并在命令流中停留一段时间。而程序中的 OpenGL ES 须要渲染以反映进行绘制调用时资源的状态,因而在援用它们的 GPU 工作负载实现之前无奈批改资源。
调试过程
咱们曾做出尝试,对援用资源进行代码上的编辑优化,然而当咱们尝试批改这部分内容时,会触发该局部的新正本的创立。这将可能肯定水平上实现咱们的指标,然而会产生大量的 CPU 开销。
于是咱们应用 Streamline
查明高 CPU 负载的实例。在图形驱动程序外部 libGLES_Mali.so
门路函数, 视图中看到极高的占用工夫。
因为咱们心愿在不同手机上适配不同帧率运行,所以须要查明 libGLES_Mali.so 是否在不同机型的设施上都产生了极高的占用工夫,此处采纳了 友盟 +U-APM来检测用户在不同机型上的函数占用比例。
经 友盟 + U-APM自定义异样测试,下列机型会产生高 libGLES_Mali.so
占用的问题,因而咱们须要基于底层硬件的运行状况来解决流畅性问题,同时因为存在问题的机型不止一种,咱们须要从内存层面着手,思考如何调用较少的内存缓存区并及时开释内存。
解决方案及优化
基于前文的剖析,咱们首先尝试从缓冲区动手进行优化。
单缓冲区计划
• 应用glMapBufferRange 和 GL_MAP_UNSYNCHRONIZED
. 而后应用单个缓冲区内的子区域构建旋转。这防止了对多个缓冲区的需要,然而这一计划依然存在一些问题,咱们仍须要解决治理子区域依赖项,这一部分的代码给咱们带来了额定的工作量。
多缓冲区计划
• 咱们尝试在零碎中创立多个缓冲区,并以循环形式应用缓冲区。通过计算咱们失去了适宜的缓冲区的数目,在之后的帧中,代码能够去从新应用这些循环缓冲区。因为咱们应用了大量的循环缓冲区,那么大量的日志记录和数据库写入是十分有必要的。然而有几个因素会导致此处的性能不佳:
1. 产生了额定的内存应用和 GC 压力
2. Android 操作系统实际上是将日志音讯写入日志而并非文件,这须要额定的工夫。
3. 如果只有一次调用,那么这里的性能耗费微不足道。然而因为应用了循环缓冲区,所以这里须要用到屡次调用。
咱们会在基于 c# 中的 Mono 分析器中启用内存调配跟踪函数用于定位问题:
$ adb shell setprop debug.mono.profile log:calls,alloc
咱们能够看到该办法在每次调用时都破费工夫:
Method call summary Total(ms) Self(ms) Calls Method name 782 5 100 MyApp.MainActivity:Log (string,object[]) 775 3 100 Android.Util.Log:Debug (string,string,object[]) 634 10 100 Android.Util.Log:Debug (string,string)
在这里定位到咱们的日志记录破费了大量工夫,咱们的下一步方向可能须要改良单个调用,或者寻求全新的解决方案。
log:alloc
还让咱们看到内存调配;日志调用间接导致了大量的不合理内存调配:
Allocation summary Bytes Count Average Type name 41784 839 49 System.String 4280 144 29 System.Object[]
硬件加速
最初尝试引入硬件加速,取得了一个新的绘图模型来将应用程序渲染到屏幕上。它引入了 DisplayList
构造并且记录视图的绘图命令以放慢渲染速度。
同时,能够将 View
渲染到屏幕外缓冲区并得心应手地批改它而不必放心被援用的问题。此性能次要实用于动画,非常适合解决咱们的帧率问题, 能够更快地为简单的视图设置动画。
如果没有图层,在更改动画属性后,动画视图将使其有效。对于简单的视图,这种生效会流传到所有的子视图,它们反过来会重绘本人。
在应用由硬件反对的视图层后,GPU 会为视图创立纹理。因而咱们能够在咱们的屏幕上为简单的视图设置动画,并且使动画更加晦涩。
代码示例:
// Using the Object animator view.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 20f); objectAnimator.addListener(new AnimatorListenerAdapter() {@Override public void onAnimationEnd(Animator animation) {view.setLayerType(View.LAYER_TYPE_NONE, null); } }); objectAnimator.start(); // Using the Property animator view.animate().translationX(20f).withLayer().start();
另外还有几点在应用硬件层中仍需注意:
(1)在应用之后进行清理:
硬件层会占用 GPU 上的空间。在下面的 ObjectAnimator
代码中,侦听器会在动画完结时移除图层。在 Property animator
示例中,withLayers()
办法会在开始时主动创立图层并在动画完结时将其删除。
(2)须要将硬件层更新可视化:
应用开发人员选项,能够启用“显示硬件层更新”。
如果在利用硬件层后更改视图,它将使硬件层有效并将视图从新渲染到该屏幕外缓冲区。
硬件加速优化
然而由此带来了一个问题是,在不须要疾速渲染的界面,比方滚动栏, 硬件层也会更快地渲染它们。当将 ViewPager
滚动到两侧时,它的页面在整个滚动阶段会以绿色突出显示。
因而当我滚动 ViewPager
时,我应用 DDMS
运行 TraceView
,按名称对办法调用进行排序,搜寻“android/view/View.setLayerType”
,而后跟踪它的援用:
ViewPager#enableLayers(): private void enableLayers(boolean enable) {final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) {final int layerType = enable ? ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; ViewCompat.setLayerType(getChildAt(i), layerType, null); } }
该办法负责为 ViewPager
的孩子启用 / 禁用硬件层。它从 ViewPaper#setScrollState()
调用一次:
private void setScrollState(int newState) {if (mScrollState == newState) {return;} mScrollState = newState; if (mPageTransformer != null) {enableLayers(newState != SCROLL_STATE_IDLE); } if (mOnPageChangeListener != null) {mOnPageChangeListener.onPageScrollStateChanged(newState); } }
正如代码中所示,当滚动状态为 IDLE
时硬件被禁用,否则在 DRAGGING
或 SETTLING
时启用。PageTransformer
旨在“应用动画属性将自定义转换利用于页面视图”(Source)。
基于咱们的需要,只在渲染动画的时候启用硬件层,所以我想笼罩ViewPager
办法,但因为它们是公有的,咱们无奈批改这个办法。
所以我采取了另外的解决方案:在 ViewPage#setScrollState()
上,在调用 enableLayers()
之后,咱们还会调用
OnPageChangeListener#onPageScrollStateChanged()
。所以我设置了一个监听器,当 ViewPager
的滚动状态不同于 IDLE
时,它将所有 ViewPager
的孩子的图层类型重置为 NONE
:
@Override public void onPageScrollStateChanged(int scrollState) {// A small hack to remove the HW layer that the viewpager add to each page when scrolling. if (scrollState != ViewPager.SCROLL_STATE_IDLE) {final int childCount = <your_viewpager>.getChildCount(); for (int i = 0; i < childCount; i++) <your_viewpager>.getChildAt(i).setLayerType(View.LAYER_TYPE_NONE, null); } }
这样,在 ViewPager#setScrollState()
为页面设置了一个硬件层之后——我将它们从新设置为 NONE
,这将禁用硬件层,因而而导致的帧率区别次要显示在 Nexus
上。