开发手机应用最主要的两个点是 1.处理用户屏幕触碰输入 2.界面效果展示(包含动画和各种反馈) ,但是在早期的 Android 并不是那么尽人意,给用户的感觉就是卡顿,系统处理图形能力差,开发者再怎么优化也是枉然。
从 Android 4.0+ 开始,以 “run fast, smooth, and responsively” 为核心目标对 UI 进行优化,应用默认都开启和使用硬件加速方式加速 UI 的绘制。首先讲解一下硬件加速相关概念。
Android 系统的 UI 从 绘制 到 显示在屏幕上 是分两个步骤的
第一步:在Android 应用程序这一侧进行的。(将 UI 构建到一个图形缓冲区 Buffer 中,交给SurfaceFlinger )
第二步:在SurfaceFlinger进程这一侧进行的。(获取Buffer 并合成以及显示到屏幕中。)
其中,第二步在 SurfaceFlinger 的操作一直是以硬件加速方式完成的,所以我们说的硬件加速一般指的是在 应用程序 图形通过GPU加速渲染 到 Buffer 的过程。
以下内容摘自 : 木叶57的博客,总结的很到位。
CPU : Central Processing Unit , 中央处理器,是计算机设备核心器件,用于执行程序代码。
GPU : Graphic Processing Unit , 图形处理器,主要用于处理图形运算,通常所说“显卡”的核心部件就是GPU。
下面是CPU和GPU的结构对比图。其中:
从结构图可以看出,CPU的控制器较为复杂,而ALU数量较少。因此CPU擅长各种复杂的逻辑运算,但不擅长数学尤其是浮点运算。
CPU是串行结构。以计算100个数字为例,对于CPU的一个核,每次只能计算两个数的和,结果逐步累加。
和CPU不同的是,GPU就是为实现大量数学运算设计的。从结构图中可以看到,GPU的控制器比较简单,但包含了大量ALU。GPU中的ALU使用了并行设计,且具有较多浮点运算单元。
硬件加速的主要原理,就是通过底层软件代码,将CPU不擅长的图形计算转换成GPU专用指令,由GPU完成。
Android 开发人员可以通过使用 OpenGL ES 来实现硬件加速渲染图形。
首先了解一下两个单词 : OpenGL ,OpenGL ES
这里我们首先要明确什么是硬件加速渲染,其实就是通过GPU来进行渲染。
GPU作为一个硬件,用户空间是不可以直接使用的,它是由GPU厂商按照Open GL规范实现的驱动间接进行使用的。
也就是说,如果一个设备支持GPU硬件加速渲染,那么当Android应用程序调用Open GL接口来绘制UI时,Android应用程序的 UI 就是通过硬件加速技术进行渲染的。
如果把开发者编写的应用程序图形效果展示过程当做是一次酣畅淋漓的画作过程,那么绘画过程中 Android 的各个角色又是怎么分工合作的 :
Android 应用里面的图形绘制分为2D和3D两种:2D是由Skia 来实现的,Skia 也会调用部分opengl 的内容来实现简单的3D效果;3D部分是由OpenGL|ES实现的,OpenGL|ES是Opengl的嵌入式版
首先图形绘制都是针对提供给应用 程序的一块内存填充数据
因此可以这么理解,我们使用画笔(Skia/Open GL)将内容绘制到画纸 Surface 上,绘制的过程中如果使用 Open GL渲染,那便是硬件加速,否则纯靠 CPU 绘制渲染栅格化的过程就叫软件绘制!
Skia
来绘制2D图形,也可以用OpenGL
来绘制2D / 3D图形(应用加速指的是应用内使用OpenGL处理和渲染图形)下面我们通过代码来分析软件绘制与硬件加速的不同点。
talk is cheap , read the resource code .
//ViewRootImpl
private void performDraw() {
``````
try {
draw(fullRedrawNeeded);
}
}
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
``````
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
//支持及开启了硬件加速
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);//硬件加速绘制
} else {
//软件绘制
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
}
// These can be accessed by any thread, must be protected with a lock.
// Surface can never be reassigned or cleared (use Surface.clear()).
final Surface mSurface = new Surface();//每个ViewRootImpl对应一个Surface
在上面有说道 : 「Window是View的直接管理者。」,Window 是 View的容器,每个窗口都会包含一个 Surface 。
//Surface 的三个构造函数
/**
* Create an empty surface, which will later be filled in by readFromParcel().
* @hide
*/
public Surface() {
}
public Surface(SurfaceTexture surfaceTexture) {
if (surfaceTexture == null) {
throw new IllegalArgumentException("surfaceTexture must not be null");
}
mIsSingleBuffered = surfaceTexture.isSingleBuffered();
synchronized (mLock) {
mName = surfaceTexture.toString();
setNativeObjectLocked(nativeCreateFromSurfaceTexture(surfaceTexture));
}
}
private Surface(long nativeObject) {
synchronized (mLock) {
setNativeObjectLocked(nativeObject);
}
}
Surface有3种构造函数,这里采用的是第一种构造函数,即创建一个空的Surface对象,并没有初始化该Surface的native层,其实应用程序进程的Surface创建过程是由WMS服务来完成,WMS服务通过Binder跨进程方式将创建好Surface返回给应用程序进程。
也就是说应用程序(在ViewRootImpl里)虽然创建了空的Surface对象,但实际上在 WMS 那一侧也会创建一个真正能用的 Surface 并赋值到这个空的 Surface 。了解更多 Surface 创建与赋值过程可以参考文章 : AndroidO 图形框架下应用绘图过程——Surface创建.
/**
* @return true if drawing was successful, false if an error occurred
*/
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
final Canvas canvas;
canvas = mSurface.lockCanvas(dirty);//获取Skia Canvas
try {
mView.draw(canvas);//图形绘制
} finally {
surface.unlockCanvasAndPost(canvas);//将绘制的结果进行提交
}
return true;
}
lockCanvas : (锁定界面中需要绘制的部分)每个窗口都关联一个Surface,当这个窗口需要绘制 UI 时,就会调用关联的 Surface 的 lockCanvas()
方法获得一个Canvas,(这个Canvas 封装了由 Skia 提供的 2D 图形绘制接口)并且向 SurfaceFlinger Dequeue 一个Graphic Buffer,绘制的内容都会输出到 Graphic Buffer 上再交由 SurfaceFlinger 对图形内容的合成及显示到屏幕上。
draw : 将View的内容绘制到Canvas上。
unlockCanvasAndPost : 绘制完成之后,调用unlockCanvasAndPost
请求将Canvas 显示到屏幕上,其本质上是向SurfaceFlinger服务Queue一个Graphic Buffe。
硬件加速优化点 :
硬件加速过程中包含两个步骤 :
参考:看书的小蜗牛,硬件加速原理分析的很透彻。
Android硬件加速过程中,View视图被抽象成
RenderNode
节点,View中的绘制操作都会被抽象成一个个DrawOp,比如View中drawLine,构建中就会被抽象成一个DrawLineOp,drawBitmap操作会被抽象成DrawBitmapOp
每个子View的绘制被抽象成DrawRenderNodeOp
,每个DrawOp有对应的OpenGL绘制命令,同时内部也握着绘图所需要的数据。如下所示:
每个View不仅仅握有自己DrawOp List,同时还拿着子View的绘制入口,如此递归,便能够统计到所有的绘制Op,源码中称为 Display List 。(也就是说根结点的RenderNode可以访问所有的绘制Op)
构建完成后,将绘图Op树 Display List 交给Render线程进行绘制,这里是同软件绘制很不同的地方,软件绘制时,View一般都在主线程中完成绘制,而硬件加速,除非特殊要求,一般都是在单独线程中完成绘制,如此以来就分担了主线程很多压力,提高了UI线程的响应速度。
将绘制操作命令构建在 Display List 的好处有 :
//ThreadedRenderer
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
updateRootDisplayList(view, callbacks);//构建View的DrawOp树
``````
//通知RenderThread线程绘制
int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
}
private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) {
//来构建参数view(DecorView)视图的Display List
updateViewTreeDisplayList(view);
//mRootNodeNeedsUpdate true表示要更新视图
//mRootNode.isValid() 表示已经构建了Display List
if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
//获取DisplayListCanvas
DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
try {
//ReorderBarrie表示会按照Z轴坐标值重新排列子View的渲染顺序
canvas.insertReorderBarrier();
//构建并缓存所有的DrawOp
canvas.drawRenderNode(view.updateDisplayListIfDirty());
canvas.insertInorderBarrier();
canvas.restoreToCount(saveCount);
} finally {
//将所有的DrawOp填充到根RootNode中,作为新的Display List
mRootNode.end(canvas);
}
}
}
ThreadedRenderer 的主要作用是在主线程中(CPU)构建视图,并将构建好的视图通知到RenderThread让其使用OpenGL绘制渲染。(调用nSyncAndDrawFrame()
)
在updateRootDisplayList()
方法中开始构建Display List,也就是构建Draw Op命令序列。该方法主要有以下流程 :
mRootNode.start()
)updateDisplayListIfDirty()
遍历获得Draw OP命令树,并且构建到DisplayListCanvas 上mRootNode.end(canvas)
)//View
View() {
mResources = null;
mRenderNode = RenderNode.create(getClass().getName(), this);
}
/**
* Gets the RenderNode for the view, and updates its DisplayList (if needed and supported)
*/
@NonNull
public RenderNode updateDisplayListIfDirty() {
final RenderNode renderNode = mRenderNode;
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
|| !renderNode.isValid()
|| (mRecreateDisplayList)) {
``````
if (renderNode.isValid() && !mRecreateDisplayList) {
//当前View是ViewGroup,并且自身不用重构,递归子View
dispatchGetDisplayList();
return renderNode;
}
final DisplayListCanvas canvas = renderNode.start(width, height);
try {
if (layerType == LAYER_TYPE_SOFTWARE) {
//软件绘制
buildDrawingCache(true);
Bitmap cache = getDrawingCache(true);
if (cache != null) {
canvas.drawBitmap(cache, 0, 0, mLayerPaint);
}
} else {
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
//当前为ViewGroup,并且自身不用绘制,递归子View
dispatchDraw(canvas);
} else {
//如果是ViewGroup会调用dispatchDraw从而递归调用子View的draw
draw(canvas);
}
}
} finally {
//将缓存的Draw Op(也就是Display List)填充到RenderNode中
renderNode.end(canvas);
setDisplayListProperties(renderNode);//设置renderNode属性
}
}
return renderNode;//构建完成
}
在三种情况下,View需要重新构建当前View包括子View的Display :
PFLAG_DRAWING_CACHE_VALID
位等于0,这表明上次构建的Display List已经失效。mRecreateDisplayList
的值等于true,这直接表明需要重新构建Display List。在View的updateDisplayListIfDirty()
流程中,构建过程如下 :
draw(canvas)
)renderNode.end(canvas)
)这样,一个应用程序窗口的Display List就构建完成了。这个构建完成的Display List对应的就是应用程序窗口的Root Render Node的Display List,并且这个Display List通过递归的方式包含了所有子View的Display List。
硬件加速过程中如何支持软件绘制的api
只有支持并开启硬件加速的View才会关联有RenderNode,同时GPU不是支持所有的2D UI 绘制命令具体可以查看android developer文档,所以GPU不支持的绘制命令只能通过软件方式来绘制渲染。
具体的做法是通过buildDrawingCache()
方法通过View缓存获得Bitmap,也就是说View的绘制都发生在这个Bitmap上。绘制完成之后,这个Bitmap会被记录到父VIew的 Display List中。而当Parent View的Display List的命令被执行时,记录在里面的Bitmap再通过Open GL命令来绘制。
View主要的 UI 操作是由成员函数onDraw实现的,通过onDraw方法参数可以获得一个canvas实现绘制API。但对View来说,它是不需要区别它是通过硬件渲染还是软件渲染的。
如果当前正在处理的View是一个View Group,会通过dispatchDraw遍历调用子View进行绘制。
此外,对于使用硬件渲染的View来说,它的Background也是抽象为一个Render Node绘制在宿主View关联的一个Render Node上的,相当于是将Background看作是一个View的子View。(通过View的drawBackground()
方法的drawRenderNode
体现)所以布局中去除重复或者不必要的background能够加快CPU的构建速度,同时也能减轻GPU渲染负担,具体可以在开发者选项中调试GPU过度绘制查看布局情况。
蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
Overdraw有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。
以上便是硬件加速过程中Display List (图形缓冲区)的构造过程,最后我们再来看一小段代码,领略一下 Display List 带来的如丝般顺滑体验的原理。(View 改变透明度)
//View
public void setAlpha(float alpha) {
if (mTransformationInfo.mAlpha != alpha) {
mTransformationInfo.mAlpha = alpha;
invalidateViewProperty(true, false);
mRenderNode.setAlpha(getFinalAlpha());
}
}
void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) {
if (!isHardwareAccelerated()
|| !mRenderNode.isValid()
|| (mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
//不支持硬件加速,刷新
invalidate(false);
} else {
damageInParent();//通知父View 更新当前View的属性
}
if (isHardwareAccelerated() && invalidateParent && getZ() != 0) {
damageShadowReceiver();
}
}
纯软件绘制 VS 硬件加速
渲染场景 | 纯软件绘制 | 硬件加速 | 加速效果分析 |
---|---|---|---|
页面初始化 | 绘制所有View | 创建所有DisplayList | GPU分担了复杂计算任务 |
在一个复杂页面调用背景透明TextView的setText(),且调用后其尺寸位置不变 | 重绘脏区所有View | TextView及每一级父View重建DisplayList | 重叠的兄弟节点不需CPU重绘,GPU会自行处理 |
TextView逐帧播放Alpha / Translation / Scale动画 | 每帧都要重绘脏区所有View | 除第一帧同场景2,之后每帧只更新TextView对应RenderNode的属性 | 刷新一帧性能极大提高,动画流畅度提高 |
修改TextView透明度 | 重绘脏区所有View | 直接调用RenderNode.setAlpha()更新 | 只触发DecorView.updateDisplayListIfDirty,不再往下遍历,CPU执行时间可忽略不计 |
硬件加速过程中构建阶段 CPU 负责把 UI 视图组件计算成Polygons,Texture纹理,然后交给 GPU 在独立线程中进行栅格化渲染。
在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着UI组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示更加复杂,需要先经过CPU换算成纹理,然后再交给GPU进行渲染,回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容。动画则是一个更加复杂的操作流程。
为了能够使得App流畅,我们需要在每一帧16ms以内处理完所有的CPU与GPU计算,构建,绘制渲染等操作。
最后附上 GPU 呈现模式的样式图,想必看完本篇文章对 Android 的屏幕绘制流程有了一个更进一步的认识。对于处理 UI 卡顿也会更得心应手。
Android 4.0,也就是Ice Cream Sandwich版本,要求设备默认支持Android应用程序UI硬件加速渲染,并且增加一个TextureView控件,该控件直接支持以Open GL纹理的形式来绘制UI。
10.25补充
参考 https://blog.csdn.net/jinzhuojun/article/details/44062175
SurfaceView 与 TextureView 的区别 :
●SurfaceView : 从Android 1.0(API level 1)时就有 。它继承自类View,因此它本质上是一个View。但与普通View不同的是,它有自己的Surface。
一般的Activity包含的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的。这个DecorView在WMS中有一个对应的WindowState。并且相应的在SF(SurfaceFlinger)中对应的Layer。而SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,在SF中也会有自己的Layer。如下图所示:
也就是说,虽然在App端它仍在View hierarchy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个Surface不在View hierarchy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。
2020.07.01补充
SurfaceView 优点:可以在一个独立的线程中进行绘制,不会影响主线程
使用双缓冲机制,播放视频时画面更流畅
缺点:Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中。SurfaceView 不能嵌套使用
●TextureView : 在4.0(API level 14)中引入。它可以将内容流直接投影到View中,可以用于实现Live preview等功能。和SurfaceView不同,它不会在WMS中单独创建窗口,而是作为View hierarchy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。
值得注意的是TextureView必须在硬件加速的窗口中。它显示的内容流数据可以来自App进程或是远端进程。TextureView继承自View,它与其它的View一样在View hierarchy中管理与绘制。
TextureView重载了draw()方法,其中主要把SurfaceTexture中收到的图像数据作为纹理更新到对应的HardwareLayer
中。SurfaceTexture.OnFrameAvailableListener用于通知TextureView内容流有新图像到来。SurfaceTextureListener接口用于让TextureView的使用者知道SurfaceTexture已准备好,这样就可以把SurfaceTexture交给相应的内容源。Surface为BufferQueue的Producer接口实现类,使生产者可以通过它的软件或硬件渲染接口为SurfaceTexture内部的BufferQueue提供graphic buffer。
Android应用程序窗口的View是通过树形结构来组织的。这些View不管是通过硬件加速渲染还是软件渲染,或者是一个特殊的TextureView,在它们的成员函数onDraw被调用期间,它们都是将自己的UI绘制在Parent View的Display List中。
其中,最顶层的Parent View是一个Root View,它关联的Root Node称为Root Render Node。也就是说,最终Root Render Node的Display List将会包含有一个窗口的所有绘制命令。在绘制窗口的下一帧时,Root Render Node的Display List都会通过一个Open GL Renderer真正地通过Open GL命令绘制在一个Graphic Buffer中。最后这个Graphic Buffer被交给SurfaceFlinger服务进行合成和显示。
2020.07.01补充
TextureView
优点:支持移动、旋转、缩放等动画,支持截图
缺点:必须在硬件加速的窗口中使用,占用内存比SurfaceView高,在5.0以前在主线程渲染,5.0以后有单独的渲染线程。
从性能和安全性角度出发,使用播放器优先选SurfaceView。
1、在android 7.0上系统surfaceview的性能比TextureView更有优势,支持对象的内容位置和包含的应用内容同步更新,平移、缩放不会产生黑边。 在7.0以下系统如果使用场景有动画效果,可以选择性使用TextureView
2、由于失效(invalidation)和缓冲的特性,TextureView增加了额外1~3帧的延迟显示画面更新
3、TextureView总是使用GL合成,而SurfaceView可以使用硬件overlay后端,可以占用更少的内存带宽,消耗更少的能量
4、TextureView的内部缓冲队列导致比SurfaceView使用更多的内存
5、SurfaceView: 内部自己持有surface,surface 创建、销毁、大小改变时系统来处理的,通过surfaceHolder 的callback回调通知。当画布创建好时,可以将surface绑定到MediaPlayer中。SurfaceView如果为用户可见的时候,创建SurfaceView的SurfaceHolder用于显示视频流解析的帧图片,如果发现SurfaceView变为用户不可见的时候,则立即销毁SurfaceView的SurfaceHolder,以达到节约系统资源的目的
更详细请参考老罗的源码分析之 Android应用程序UI硬件加速渲染的Display List构建过程分析
CPU / GUP 不同点 : Android硬件加速原理与实现简介
图形组件的概念区分 : 图形显示框架变化介绍
有深度的硬件加速分析文章 : 硬件加速原理
老罗的源码分析系列 : Android应用程序UI硬件加速渲染技术
Google 之Android性能优化典范译文 : Android性能优化典范