硬件加速浅谈

作者

大家好,我叫大圣;

本人于2018年5月加入37手游安卓团队,曾经就职于爱拍等互联网公司;

目前是37手游安卓团队的国内负责人,主要负责相关业务开发和一些日常业务统筹等。

背景

最近在处理一个游戏内嵌社区页面卡顿问题的过程中,发现在Manifest的Activity中有将硬件加速关闭的配置,如果将其开启,那么页面卡顿的问题就解决了。对这一块的知识还不是很了解,也不知道直接打开硬件加速对游戏是否有影响。于是乎在网上查了一波别人的文章,做了一个简单的总结。

CPU与GPU

在了解什么是硬件加速之前,先了解一下什么是CPU和GPU。
CPU(Centr(l Processing Unit,中央处理器)是计算机设备核⼼器件,⽤于执⾏
程序代码,软件开发者对此都很熟悉;GPU(Gr(phics Processing Unit,图形处理
器)主要⽤于处理图形运算,通常所说“显卡”的核⼼部件就是GPU。

16209e544e11314d.png

  • 黄色的Control为控制器,用于协调控制整个CPU的运行,包括取出指令、控制其他模块的运行等;
  • 绿色的ALU(Arithmetic Logic Unit)是算术逻辑单元,用于进行数学、逻辑运算;
  • 橙色的Cache和DRAM分别为缓存和RAM,用于存储信息。
    从上面的结构图可以看出,CPU的控制器较为复杂,擅长各种复杂的逻辑运算;GPU的控制器比较简单,但包含了大量ALU,擅长大量的数学运算。

硬件加速的主要原理,就是通过底层软件代码,将CPU不擅长的图形计算转换成GPU专用指令,由GPU完成。

显卡和GPU不是同一个概念,显卡指的是包含GPU的一个完整的卡,GPU是单指显卡的核心芯片,是一颗芯片;
听说比特币挖矿的机器都是采用GPU运算的,是不是和上面的原理一样,挖矿是大量的数学计算,GPU更加擅长呢?

Android中的硬件加速

接下来看看Android中如何使用硬件加速的。

如何开启硬件加速

从 Android 3.0(API 级别 11)开始,Android 2D 渲染管道⽀持硬件加速,也
就是说,在 View 的画布上执⾏的所有绘制操作都会使⽤ GPU。启⽤硬件加速需要
更多资源,因此应⽤会占⽤更多内存。

如果您的⽬标 API 级别为 14 及更⾼级别,则硬件加速默认处于启⽤状态,但也可以
明确启⽤该功能。如果您的应⽤仅使⽤标准视图和 Dr(w(ble,则全局启⽤硬件加速
不会造成任何不良绘制效果。

您可以在以下级别控制硬件加速:

  • 应⽤
  • Activity
  • 窗⼝
  • 视图

应⽤级别

在 Android 清单⽂件中,将以下属性添加到 标记中,为整个应⽤启⽤硬件加速:

<application android:hardwareAccelerated="true" ...>

Activity 级别

如果全局启用硬件加速后,您的应用无法正常运行,则您也可以针对各个 Activity 控制硬件加速。要在 Activity 级别启用或停用硬件加速,您可以使用 元素的 android:hardwareAccelerated 属性。以下示例展示了如何为整个应用启用硬件加速,但为一个 Activity 停用硬件加速:

<application android:hardwareAccelerated="true">
	<activity ... />
	<activity android:hardwareAccelerated="false" />
</application>

窗口级别

如果您需要实现更精细的控制,可以使用以下代码为给定窗口启用硬件加速:

getWindow().setFlags(
	WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

视图级别

您可以使用以下代码在运行时为单个视图停用硬件加速:

myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

绘制流程上面的优化

在说完以上CPU和GPU的特性,以及Android如何控制硬件加速之后,来看看在界面View在刷新和绘制过程中,具体是哪些关键点来做了硬件加速的控制。

硬件加速情况下Canvas生成的是DisplayListCanvas对象

在View的绘制过程中会沿着draw(canvas,parent,drawingTime) –> draw(canvas) –> onDraw –> dispachDraw 这条递归路径往下走,在draw(canvas,parent,drawingTime)中,如果是硬件加速情况下,会走updateDisplayListIfDirty()。

if (drawingWithRenderNode) {
    renderNode = updateDisplayListIfDirty();
    if (!renderNode.isValid()) {
        // ...
        renderNode = null;
        drawingWithRenderNode = false;
    }
}

在updateDisplayListIfDirty()方法中,生成了DisplayListCanvas对象,然后调用draw(canvas)方法,此时Canvas已经不是普通的Canvas了,而是DisplayListCanvas。

final DisplayListCanvas canvas = renderNode.start(width, height);
canvas.setHighContrastText(mAttachInfo.mHighContrastText);

try {
        //...
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }
        } else {
            draw(canvas);
        }
    }
} finally {
    renderNode.end(canvas);
    setDisplayListProperties(renderNode);
}

在调用DisplayListCanvas的drawXXX方法过程中,并没有执行真正的绘制,而是用构建DisplayList,并保存在RenderNode中,通过renderNode.end(canvas)进行该操作。

/**
* Ends the recording for this display list. A display list cannot be
* replayed if recording is not finished. Calling this method marks
* the display list valid and {@link #isValid()} will return true.
*
* @see #start(int, int)
* @see #isValid()
*/
public void end(DisplayListCanvas canvas) {
    long displayList = canvas.finishRecording();
    nSetDisplayList(mNativeRenderNode, displayList);
    canvas.recycle();
}

JNI层将DisplayListCanvas的操作转化成DisplayList

一个RenderNode包含若干个DisplayList,通常一个RenderNode对应一个View,包含View自身及其子View的所有DisplayList。

在ViewRootImpl中,如果是支持硬件加速,会执行mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this)方法。

// ViewRootImpl.java
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
    mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
    //...
}

ThreadedRenderer的draw过程,会调用到updateRootDisplayList,然后调用updateViewTreeDisplayList,此过程会更新整个View树的DisplayList,最终执行ThreadedRenderer.nSyncAndDrawFrame来启动线程对DisplayList进行最终的绘制操作

// ThreadedRenderer.java
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
    ...
    updateRootDisplayList(view, callbacks);
    ...
    int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);

}

由于绘制流程的不同,硬件加速在界面内容发生重绘的时候绘制流程可以得到优化,避免了一些重复操作,从而大幅提升绘制效率。在updateDisplayListIfDirty方法从名字可以看出,更新Displaylist是发生在脏区域。

在硬件加速关闭时,绘制内容会被 CPU 转换成实际的像素,然后直接渲染到屏幕。具体来说,这个「实际的像素」,它是由 Bitmap 来承载的。在界面中的某个 View 由于内容发生改变而调用 invalidate() 方法时,如果没有开启硬件加速,那么为了正确计算 Bitmap 的像素,这个 View 的父 View、父 View 的父 View 乃至一直向上直到最顶级 View,以及所有和它相交的兄弟 View,都需要被调用 invalidate()来重绘。一个 View 的改变使得大半个界面甚至整个界面都重绘一遍,这个工作量是非常大的。

而在硬件加速开启时,前面说过,绘制的内容会被转换成 GPU 的操作保存下来(承载的形式称为 display list,对应的类也叫做 DisplayList),再转交给 GPU。由于所有的绘制内容都没有变成最终的像素,所以它们之间是相互独立的,那么在界面内容发生改变的时候,只要把发生了改变的 View 调用 invalidate() 方法以更新它所对应的 GPU 操作就好,至于它的父 View 和兄弟 View,只需要保持原样。那么这个工作量就很小了。

image-20210322002222220.png

在默认情况下,View的clipChildren的属性为true,即每个View绘制区域不能超出其父View的范围。如果设置一个页面根布局的clipChildren属性为false,则子View可以超出父View的绘制区域。

上图中右边的ViewGroup的clipChildren属性即为false,其子View的绘制超出了自身范围。

当一个View触发invalidate,且没有播放动画、没有触发layout的情况下:

  • 对于全不透明的View,其自身会设置标志位PFLAG_DIRTY,其父View会设置标志位PFLAG_DIRTY_OPAQUE。在draw(canvas)方法中,只有这个View自身重绘。

    上图中的TextView背景全不透明,此时调用TextView的invalidate,自由其自身重绘制。

  • 对于可能有透明区域的View,其自身和父View都会设置标志位PFLAG_DIRTY

    • clipChildren为true时,脏区会被转换成ViewRoot中的Rect,刷新时层层向下判断,当View与脏区有重叠则重绘。如果一个View超出父View范围且与脏区重叠,但其父View不与脏区重叠,这个子View不会重绘。

      上图中如果TextView背景透明,那么脏区域会从TextView投影至ViewRoot上面的Rect,也就是绿色区域,此时在这个投影之上的View都会被触发绘制(即TextView、左边ViewGroup、ViewRoot)

    • clipChildren为false时,ViewGroup.invalidateChildInParent()中会把脏区扩大到自身整个区域,于是与这个区域重叠的所有View都会重绘。

      上图中右边ViewGroup的clipChildren为false,那么脏区域即为右边ViewGroup的整个投影的区域,与此重叠的View都会被重新绘制。

实际问题解决

背景中提到的游戏内嵌web页面卡顿的问题, 在开启硬件加速后卡顿的问题解决了。但是由于对游戏引擎不了解,所以不清楚开启硬件加速是否会影响到游戏的运行,比如游戏界面绘制会不会错乱,耗电量会不会增加等等问题。其实从上面第三小节提到的,Android不仅仅可以对整个应用开启硬件加速还支持对单个Activity进行设置,那么这个问题其实就很好解决了, 直接将显示Web的页面写成一个Activity单独开启硬件急速即可,这样完全不会影响到游戏。

总结

在硬件加速过程中,充分利用CPU和GPU各自的特性,将逻辑部分交个CPU处理,将大量的数学计算交个GPU处理,在硬件层面对整个过程效率提升。界面刷新过程中,CPU只会更新需要重绘的DisplayList,进一步提高绘制效率。

上文的介绍只是在一些关键的代码上面做了讲解,未深入介绍细节;对于DisplayListCanvas如何生成DisplsyList的过程,属于JNI层的操作,文档并未涉及。对于RenderNode是如何组织DiaplayList,以及View树的各个RenderNode是如何组织的都未阐明,文章充分借鉴了一些大神文章的内容,文末有参考链接,需要深入了解可以自行查阅。

结束语

过程中有问题或者需要交流的同学,可以添加微信号AndroidAssistant37,然后进群进行问题和技术的交流等。

参考链接

  1. juejin.cn/post/684490…
  2. developer.android.com/guide/topic…
  3. www.codeleading.com/article/337…
  4. juejin.cn/post/684490…

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY1Ynuxa' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片