你们好,我叫大圣;java
本人于2018年5月加入37手游安卓团队,曾经就任于爱拍等互联网公司;android
目前是37手游安卓团队的国内负责人,主要负责相关业务开发和一些平常业务统筹等。web
最近在处理一个游戏内嵌社区页面卡顿问题的过程当中,发如今Manifest的Activity中有将硬件加速关闭的配置,若是将其开启,那么页面卡顿的问题就解决了。对这一块的知识还不是很了解,也不知道直接打开硬件加速对游戏是否有影响。因而乎在网上查了一波别人的文章,作了一个简单的总结。canvas
在了解什么是硬件加速以前,先了解一下什么是CPU和GPU。 CPU(Centr(l Processing Unit,中央处理器)是计算机设备核⼼器件,⽤于执⾏ 程序代码,软件开发者对此都很熟悉;GPU(Gr(phics Processing Unit,图形处理 器)主要⽤于处理图形运算,一般所说“显卡”的核⼼部件就是GPU。缓存
从上面的结构图能够看出,CPU的控制器较为复杂,擅长各类复杂的逻辑运算;GPU的控制器比较简单,但包含了大量ALU,擅长大量的数学运算。微信
硬件加速的主要原理,就是经过底层软件代码,将CPU不擅长的图形计算转换成GPU专用指令,由GPU完成。markdown
显卡和GPU不是同一个概念,显卡指的是包含GPU的一个完整的卡,GPU是单指显卡的核心芯片,是一颗芯片;
据说比特币挖矿的机器都是采用GPU运算的,是否是和上面的原理同样,挖矿是大量的数学计算,GPU更加擅长呢?app
接下来看看Android中如何使用硬件加速的。ide
从 Android 3.0(API 级别 11)开始,Android 2D 渲染管道⽀持硬件加速,也 就是说,在 View 的画布上执⾏的全部绘制操做都会使⽤ GPU。启⽤硬件加速须要 更多资源,所以应⽤会占⽤更多内存。oop
若是您的⽬标 API 级别为 14 及更⾼级别,则硬件加速默认处于启⽤状态,但也能够 明确启⽤该功能。若是您的应⽤仅使⽤标准视图和 Dr(w(ble,则全局启⽤硬件加速 不会形成任何不良绘制效果。
您能够在如下级别控制硬件加速:
在 Android 清单⽂件中,将如下属性添加到 标记中,为整个应⽤启⽤硬件加速:
<application android:hardwareAccelerated="true" ...>
复制代码
若是全局启用硬件加速后,您的应用没法正常运行,则您也能够针对各个 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
,而后进群进行问题和技术的交流等。
参考连接