作者
大家好,我叫大圣;
本人于2018年5月加入37手游安卓团队,曾经就职于爱拍等互联网公司;
目前是37手游安卓团队的国内负责人,主要负责相关业务开发和一些日常业务统筹等。
背景
最近在处理一个游戏内嵌社区页面卡顿问题的过程中,发现在Manifest的Activity中有将硬件加速关闭的配置,如果将其开启,那么页面卡顿的问题就解决了。对这一块的知识还不是很了解,也不知道直接打开硬件加速对游戏是否有影响。于是乎在网上查了一波别人的文章,做了一个简单的总结。
CPU与GPU
在了解什么是硬件加速之前,先了解一下什么是CPU和GPU。
CPU(Centr(l Processing Unit,中央处理器)是计算机设备核⼼器件,⽤于执⾏
程序代码,软件开发者对此都很熟悉;GPU(Gr(phics Processing Unit,图形处理
器)主要⽤于处理图形运算,通常所说“显卡”的核⼼部件就是GPU。
- 黄色的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,只需要保持原样。那么这个工作量就很小了。
在默认情况下,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
,然后进群进行问题和技术的交流等。
参考链接