作者:陈一萌
本文出自于“「2021 友盟+ 挪动利用性能挑战赛」” 中的参赛作品,该文章表述了作者如何借助友盟+ U-APM工具进行了性能优化。
背景
作为一款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.而后应用单个缓冲区内的子区域构建旋转。这防止了对多个缓冲区的需要,然而这一计划依然存在一些问题,咱们仍须要解决治理子区域依赖项,这一部分的代码给咱们带来了额定的工作量。
多缓冲区计划
• 咱们尝试在零碎中创立多个缓冲区,并以循环形式应用缓冲区。通过计算咱们失去了适宜的缓冲区的数目,在之后的帧中,代码能够去从新应用这些循环缓冲区。因为咱们应用了大量的循环缓冲区,那么大量的日志记录和数据库写入是十分有必要的。然而有几个因素会导致此处的性能不佳:
- 产生了额定的内存应用和GC压力
- Android 操作系统实际上是将日志音讯写入日志而并非文件,这须要额定的工夫。
- 如果只有一次调用,那么这里的性能耗费微不足道。然而因为应用了循环缓冲区,所以这里须要用到屡次调用。
咱们会在基于c#中的 Mono 分析器中启用内存调配跟踪函数用于定位问题:
$ adb shell setprop debug.mono.profile log:calls,alloc
咱们能够看到该办法在每次调用时都破费工夫:
Method call summaryTotal(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 animatorview.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 animatorview.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:
@Overridepublic 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上。
本文出自于“「2021 友盟+ 挪动利用性能挑战赛」” 中的参赛作品,该文章表述了作者如何借助友盟+ U-APM工具进行了性能优化。