Android硬件加速从基础到原理

3,786 阅读18分钟

硬件加速的主要原理是通过将CPU不擅长的图形计算转换成GPU专用指令,让更擅长图形计算的GPU来完成渲染。硬件加速改变了android的绘图模型,能提高绘图的性能。

从API 级别 11开始,Android 2D 渲染管道支持硬件加速,API 级别为 14 及更高级别,则硬件加速默认处于启用状态。

最近项目中使用到了复杂路径clipPath切割导致部分机型硬件加速兼容导致出现闪烁的问题,因此借此机会将收集到的硬件加速相关知识汇总到这里。

一、硬件和软件加速模型

硬件加速,直观上说就是依赖GPU实现图形绘制加速,软硬件加速的区别主要是图形的绘制究竟是GPU来 处理还是CPU,如果是GPU,就认为是硬件加速绘制,反之,则是软件加速。

不过相对于普通的软件绘制,硬件加速还做了其他方面优化, 不仅仅限定在绘制方面,绘制之前,在如何构建绘制区域上,硬件加速也做出了很大优化。

软件绘制同硬件加速的区别主要是在绘制上,内存分配、图层合成等整体流程是一样的, 只不过硬件加速相比软件绘制算法更加合理,同时采用单独的渲染线程,减轻了主线程的负担。  

1、软件加速模型

在软件绘制模型中,绘制视图分为以下两步:

1. 调用Invalidate对层次结构进行无效化处理

2. 按照层次结构递归绘制

每当应用需要更新其界面的一部分时,就会对内容已发生更改的所有视图调用 invalidate()(或其变体之一)。 无效化消息会一直传播到视图层次结构上层,以计算需要重新绘制的屏幕区域(脏区域),然后Android 系统会绘制层次结构中与脏区域交互的所有视图 。

1.1缺点

这种绘制模型具有以下两个缺点:

1.每次绘制时该模型都需要执行大量代码

例如,如果您的应用对某个按钮调用 invalidate() 且该按钮位于另一个视图上方,那么即使该视图未发生更改,Android 系统仍会重新绘制该视图。

2.该绘制模型会隐藏应用中的错误

由于 Android 系统会在视图与脏区域交互时重新绘制视图,因此系统可能会重新绘制内容发生更改的视图,即使未对其调用 invalidate() 也是如此。如果发生这种情况,您要依赖其他经过无效化处理的视图才能获得正确的行为。每次修改应用时,此行为都可能会发生更改。因此,每次修改会影响视图绘制代码的数据或状态后,您都要对自定义视图调用 invalidate()。

2、硬件加速模型

Android 系统仍会使用 invalidate() 和 draw() 请求屏幕更新和渲染视图,但会采用其他方式处理实际绘制过程。 Android 系统不会立即执行绘制命令,而是将这些命令记录在显示列表中,这些列表中包含视图层次结构绘制代码的输出。 另一项优化是,Android 系统只需要记录和更新被 invalidate() 调用标记为脏视图的视图的显示列表。

新绘制模型包含以下三个阶段:

1. Invalidate the hierarchy 对层次结构进行无效化处理

2. Record and update display lists 记录并更新显示列表

3. Draw the display lists 绘制显示列表

在未启用硬件加速的应用中,系统会再次执行列表及其父级的绘制代码。

二、硬件加速原理

硬件加速绘制包括两个阶段:构建阶段+绘制阶段,所谓构建就是递归遍历所有视图,将需要的操作缓存下来,之后再交给单独的Render线程利用OpenGL渲染。

1、构建阶段

1.1 构建绘制命令树

硬件加速构建绘制命令树的过程是从View控件树的根节点DecorView触发,递归调用每个子View节点的 updateDisplayListIfDirty 函数, 最终完成绘制树的创建。

ViewRootImpl判断使用硬件绘制还是软件绘制:

/*frameworks/base/core/java/android/view/ViewRootImpl.java*/
private boolean draw(boolean fullRedrawNeeded) {
	...
	if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
		...
		// 如果开启并支持硬件绘制加速,则走硬件绘制的流程(从Android 4.+开始,默认情况下都是支持跟开启了硬件加速的)
		mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
	} else {
		// 否则走drawSoftware软件绘制的流程
		if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
						scalingRequired, dirty, surfaceInsets)) {
					return false;
		 }
	}
}

ThreadedRenderer接管构建绘制命令树:

/*frameworks/base/core/java/android/view/ThreadedRenderer.java*/
	void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
		...
		// 1.从DecorView根节点出发,递归遍历View控件树,记录每个View节点的绘制操作命令,完成绘制操作命令树的构建
		updateRootDisplayList(view, callbacks);
		...
		// 2.JNI调用同步Java层构建的绘制命令树到Native层的RenderThread渲染线程,并唤醒渲染线程利用OpenGL执行渲染任务;
		int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
		...
	}
private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
		// 原生标记构建View绘制操作命令树过程的systrace tag
		Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
		// 递归子View的updateDisplayListIfDirty实现构建DisplayListOp
		updateViewTreeDisplayList(view);
		...
		if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
			// 获取根View的SkiaRecordingCanvas
			RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);
			try {
				...
				// 利用canvas缓存DisplayListOp绘制命令
				canvas.drawRenderNode(view.updateDisplayListIfDirty());
				...
			} finally {
				// 将所有DisplayListOp绘制命令填充到RootRenderNode中
				mRootNode.endRecording();
			}
		}
		Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

private void updateViewTreeDisplayList(View view) {
		...
		// 从DecorView根节点出发,开始递归调用每个View树节点的updateDisplayListIfDirty函数
		view.updateDisplayListIfDirty();
		...
}

/*frameworks/base/core/java/android/view/View.java*/
public RenderNode updateDisplayListIfDirty() {
	 ...
	 // 1.利用`View`对象构造时创建的`RenderNode`获取一个`SkiaRecordingCanvas`“画布”;
	 final RecordingCanvas canvas = renderNode.beginRecording(width, height);
	 try {
		 ...
		 if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
			  // 如果仅仅是ViewGroup,并且自身不用绘制,直接递归子View
			  dispatchDraw(canvas);
			  ...
		 } else {
			  // 2.利用SkiaRecordingCanvas,在每个子View控件的onDraw绘制函数中调用drawLine、drawRect等绘制操作时,创建对应的DisplayListOp绘制命令,并缓存记录到其内部的SkiaDisplayList持有的DisplayListData中;
			  draw(canvas);
		 }
	 } finally {
		 // 3.将包含有`DisplayListOp`绘制命令缓存的`SkiaDisplayList`对象设置填充到`RenderNode`中;
		 renderNode.endRecording();
		 ...
	 }
	 ...
}

public void draw(Canvas canvas) {
	...
	// draw the content(View自己实现的onDraw绘制,由应用开发者自己实现)
	onDraw(canvas);
	...
	// draw the children
	dispatchDraw(canvas);
	...
}

/*frameworks/base/graphics/java/android/graphics/RenderNode.java*/
public void endRecording() {
		...
		// 从SkiaRecordingCanvas中获取SkiaDisplayList对象
		long displayList = canvas.finishRecording();
		// 将SkiaDisplayList对象填充到RenderNode中
		nSetDisplayList(mNativeRenderNode, displayList);
		canvas.recycle();
}

简述流程如下:

1. 利用View对象构造时创建的RenderNode获取一个SkiaRecordingCanvas“画布”;

2. 利用SkiaRecordingCanvas,在每个子View控件的onDraw绘制函数中调用drawLine、drawRect等绘制操作时,创建对应的DisplayListOp绘制命令,并缓存记录到其内部的SkiaDisplayList持有的DisplayListData中;

3. 将包含有DisplayListOp绘制命令缓存的SkiaDisplayList对象设置填充到RenderNode中;

4. 最后将根View的缓存DisplayListOp设置到RootRenderNode中,完成构建。

1.2 View树关键类

在Android硬件加速框架中,View视图被抽象成RenderNode节点,View中的绘制都会被抽象成一个个 DrawOp(DisplayListOp), 比如View中drawLine,构建中就会被抽象成一个DrawLintOp,drawBitmap操作会被抽象成DrawBitmapOp,每个子View的绘制被抽象成DrawRenderNodeOp, 每个DrawOp有对应的OpenGL绘制命令,同时内部也握着绘图所需要的数据

如此以来,每个View不仅仅握有自己DrawOp List,同时还拿着子View的绘制入口,如此递归, 便能够统计到所有的绘制Op,很多分析都称为Display List,源码中也是这么来命名类的,不过 这里其实更像是一个树,而不仅仅是List。

1.3 DisplayList

Disaplay Lists是一个绘制命令缓冲区,也就是说,当View的成员函数onDraw被调用时, 我们调用通过参数传递进来的Canvas的drawXXX成员函数绘制图形时,我们实际上只是将对应的绘制命令以及参数保存在一个Display List中。

接下来再通过Display List Renderer执行这个Display List的命令,这个过程称为Display List Replay 

引进Display List的概念有什么好处呢?

1. 第一个好处是在下一帧绘制中,如果一个View的内容不需要更新,那么就不用重建它的DisplayList,也就是不需要调用它的onDraw成员函数。

2. 第二个好处是在下一帧中,如果一个View仅仅是一些简单的属性发生变化,例如位置和Alpha值发生变化,那么也无需要重建它的Display List, 只需要在上一次建立的DisplayList中修改一下对应的属性就可以了,这也意味着不需要调用它的onDraw成员函数。

这两个好处使用在绘制应用程序窗口的一帧时,省去很多应用程序代码的执行,也就是大大地节省了CPU的执行时间

2、绘制阶段

构建完成后,就可以将这个绘图Op树交给Render线程进行绘制,这里是同软件绘制很不同的地方, 软件绘制时,View一般都在主线程中完成绘制,而硬件加速,除非特殊要求,一般都是在单独线程中完成绘制,如此以来就分担了主线程很多压力,提高了UI线程的响应速度。

具体实现为UI线程利用RenderProxy向RenderThread线程发送一个DrawFrameTask任务请求,RenderThread被唤醒, 将UI线程构建的DisplayListOp绘制命令树同步到RenderThread,渲染线程开始渲染:

2.1 UI线程唤醒渲染线程

public int syncAndDrawFrame(@NonNull FrameInfo frameInfo) {
	// JNI调用native层的相关函数
	return nSyncAndDrawFrame(mNativeProxy, frameInfo.frameInfo, frameInfo.frameInfo.length);
}

/*frameworks/base/libs/hwui/jni/android_graphics_HardwareRenderer.cpp*/
static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz,
		jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize) {
	...
	RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
	env->GetLongArrayRegion(frameInfo, 0, frameInfoSize, proxy->frameInfo());
	return proxy->syncAndDrawFrame();
}

/*frameworks/base/libs/hwui/renderthread/RenderProxy.cpp*/
int RenderProxy::syncAndDrawFrame() {//RenderProxy 对象是用来跟渲染线程进行通信的句柄
	// 唤醒RenderThread渲染线程,执行DrawFrame绘制任务
	return mDrawFrameTask.drawFrame();
}

/*frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp*/
int DrawFrameTask::drawFrame() {
	...
	postAndWait();
	...
}

2.2 渲染线程同步View构建树和执行绘制

...
void DrawFrameTask::postAndWait() {
	AutoMutex _lock(mLock);
	// 向RenderThread渲染线程的MessageQueue消息队列放入一个待执行任务,以将其唤醒执行run函数
	mRenderThread->queue().post([this]() { run(); });
	// UI线程暂时进入wait等待状态
	mSignal.wait(mLock);
}

void DrawFrameTask::run() {//DrawFrameTask执行在渲染线程
	// 原生标识一帧渲染绘制任务的systrace tag
	ATRACE_NAME("DrawFrame");
	...
	{
		TreeInfo info(TreeInfo::MODE_FULL, *mContext);
		//1.将UI线程构建的DisplayListOp绘制命令树同步到RenderThread渲染线程
		canUnblockUiThread = syncFrameState(info);
		...
	}
	...
	// 同步完成后则可以唤醒UI线程
	if (canUnblockUiThread) {
		unblockUiThread();
	}
	...
	if (CC_LIKELY(canDrawThisFrame)) {
		// 2.执行draw渲染绘制动作
		context->draw();
	} else {
		...
	}
	...
}
void CanvasContext::draw() {
	...
	// 1.调用OpenGL库使用GPU,按照构建好的绘制命令完成界面的渲染
	bool drew = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, &mLayerUpdateQueue,
									  mContentDrawBounds, mOpaque, mLightInfo, mRenderNodes,
									  &(profiler()));
	...
	// 2.将前面已经绘制渲染好的图形缓冲区Binder上帧给SurfaceFlinger合成和显示
	bool didSwap =
			mRenderPipeline->swapBuffers(frame, drew, windowDirty, mCurrentFrameInfo, &requireSwap);
	...
}

大致流程如下:

1. syncFrameState中遍历View树上每一个RenderNode,执行prepareTreeImpl函数,实现同步绘制命令树的操作;

2. 调用OpenGL库API使用GPU硬件,按照构建好的绘制命令完成界面的渲染(具体过程,由于本文篇幅所限,暂不展开分析);

3. 将前面已经绘制渲染好的图形缓冲区Binder上帧给SurfaceFlinger合成和显示; 

三、硬件加速状态查询

应用有必要了解当前是否经过硬件加速,尤其是对于自定义视图等内容。 如果您的应用执行大量自定义绘制,但并非所有操作都得到新渲染管道的正确支持,这就会特别有用 两种不同的方式检查应用是否经过硬件加速: 

1. 如果 View 已附加到硬件加速窗口,则 View.isHardwareAccelerated() 会返回 true。

2. 如果 Canvas 经过硬件加速,则 Canvas.isHardwareAccelerated() 会返回 true

四、硬件加速启停

平时用的时候可能是直接在Application中用,一锅端,这并不严谨, 因为硬件加速还没法做到支持所有的绘制操作(比如复杂的自定义View),这样的话就会造成一定的影响:

1. 像素错位等视觉问题

2. 不同设备版本API兼容问题

解决这些问题官方给了解决方案:使用四种级别控制是否硬件加速

四种级别控制

应用级别

<application
	android:handwareAccelerated="true"
/>

Activity级别

Activity为单独页面设置
<application android:hardwareAccelerated="true">
        <activity ... />
        <activity android:hardwareAccelerated="false" />
</application>

Window级别

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

视图级别

单独的view级别关闭加速(View目前不支持动态启动硬件加速)

view.setLayerType(View.LAYER_TYPE_SOFTTYPE..)

五、视图层

在所有 Android 版本中,视图能够通过以下两种方式渲染到屏幕外缓冲区:使用视图的绘制缓存或使用 Canvas.saveLayer()。 屏幕外缓冲区或层具有多种用途。在为复杂的视图添加动画效果或应用合成效果时,您可以使用屏幕外缓冲区或层获得更好的效果。优化思路就是将视图暂时渲染到层,也就是暂时渲染到屏幕外缓冲区,然后将其合成回屏幕上。从 Android 3.0(API 级别 11)开始,您可以通过 View.setLayerType() 方法更好地控制如何及何时使用层。

1、视图可以选择使用以下三种层类型

LAYER_TYPE_NONE:

视图正常渲染,不受屏幕外缓冲区支持。这是默认行为。

LAYER_TYPE_HARDWARE:

如果应用经过硬件加速,视图在硬件中渲染为硬件纹理。如果应用未经过硬件加速,此层类型的行为方式与 LAYER_TYPE_SOFTWARE 相同。

LAYER_TYPE_SOFTWARE:视图在软件中渲染为位图。

要使用何种层类型取决于您的目标:

性能:

使用硬件层类型可将视图渲染为硬件纹理。将视图渲染为层后,在该视图调用 invalidate() 之前,无需执行其绘制代码,当然如果手动调用invalidate也会重发重绘。然后,可将 Alpha 动画等部分动画直接应用到层,GPU 可非常高效地完成此操作。

视觉效果:

使用硬件层或软件层类型和 Paint 可将特殊视觉处理应用到视图。例如,您可以使用 ColorMatrixColorFilter 绘制黑白视图。

兼容性:

使用软件层类型可强制在软件中渲染视图。如果经过硬件加速的视图(例如,如果整个应用都经过硬件加速)遇到渲染问题,采用这种方法可轻松解决硬件渲染管道的局限性。

2、硬件层支持的属性

如果视图由硬件层提供支持,则其部分属性可通过在屏幕上合成层的方式处理。 设置这些属性有助于提高效率,因为它们不需要先对视图进行无效化处理后再重新绘制。下面列出的属性会影响层的合成方式。 针对以下任何属性调用 setter 方法会得到最佳无效化效果,且 无需重新绘制目标视图

alpha:更改层的不透明度

x、y、translationX、translationY:更改层的位置

scaleX、scaleY:更改层的大小

rotation、rotationX、rotationY:更改层在 3D 空间里的方向 pivotX、pivotY:更改层的转换原点

3、区分硬件加速和视图层

注意视图层是作为一种辅助硬件加速的配置,不能完全混为一谈,硬件加速下才可以使用LAYER_TYPE_HARDWARE,没有硬件加速那设置了也没用,同时可以通过LAYER_TYPE_SOFTWARE停止硬件加速。

如果应用经过硬件加速,硬件层能够提供更快且更顺畅的动画。但是尽管如此,在为需要发出大量绘制操作的复杂视图添加动画效果时,以 60 帧/秒的速度运行动画并非总能实现。

使用硬件层将视图渲染为硬件纹理可在一定程度上解决此问题( !也就是在硬件加速模型的第三个绘制阶段增加了屏幕外缓冲区用于优化渲染?)。  

六、有效利用GPU的技巧

切换到硬件加速的 2D 图形可立即提升性能,但您仍应按照以下建议设计应用,以便有效利用 GPU。

1. 减少应用中的视图数量

系统需要绘制的视图越多,运行速度越慢。这也适用于软件渲染管道。减少视图是优化界面最简单的方法之一。

2. 避免过度绘制

请勿在彼此上方绘制过多层。移除所有被上方的其他不透明视图完全遮挡的视图。如果您需要在彼此上方混合绘制多个层,请考虑将它们合并为一个层。对于目前的硬件来说,绘制的层数最好不超过屏幕上每帧像素数的 2.5 倍(透明像素,以位图计数!)。

3. 请勿在绘制方法中创建渲染对象

一个常见的错误是,每次调用渲染方法时都创建新的 Paint 或 Path。这会强制垃圾回收器更频繁地运行,同时还会绕过硬件管道中的缓存和优化。

4. 请勿过于频繁地修改形状

例如,使用纹理遮罩渲染复杂的形状、路径和圆圈。每次创建或修改路径时,硬件管道都会创建新的遮罩,成本可能比较高。

5. 请勿过于频繁地修改位图

每次更改位图的内容时,系统都会在您下次绘制时将其作为 GPU 纹理再次上传。

6. 谨慎使用 Alpha

当您使用 setAlpha()、AlphaAnimation 或 ObjectAnimator 将视图设置为半透明时,该视图会在屏幕外缓冲区渲染,导致所需的填充率翻倍。在超大视图上应用 Alpha 时,请考虑将视图的层类型设置为 LAYER_TYPE_HARDWARE。 

七、硬件加速的兼容问题处理

 并非所有 2D 绘制操作都支持硬件加速,因此启用硬件加速可能会影响您的部分自定义视图或绘制调用。

例如针对clipPath兼容的处理办法我可以尝试如下办法:

1. 低版本放弃硬件加速

2. 低版本放弃clipPath()方法

如果是复杂动画,在关闭硬件加速的情况下,会失去动画的流畅性。这时候我们只能放弃clipPath()方法,利用其他方法来实现类似的效果

3. 不放弃 clipPath()方法也不放弃硬件加速

在前面分析开启硬件加速的时候,Android是通过硬件模式来绘制,onDraw()中我们写入的绘制代码都不会立刻执行,而会一条一条保存在DisplayList中,也就是通过onDraw()传入参数canvas调用的绘制方法,最后会使用硬件加速来绘制。

那么,对于使用了GPU不支持的2DUI绘制命令的View,例如我们要实现通过ClipPath切割一个圆角头像图片,但又由于兼容问题在部分机型不支持clipPath,我们的做法是创建一个新的Canvas,这个Canvas的底层是一个Bitmap,也就是说,我们那些不支持的2DUI绘制都发生在这个Bitmap上。

绘制完成之后,再把这个Bitmap再被记录在其Parent View的Display List中。而当Display List的命令被执行时,记录在里面的Bitmap再通过Open GL命令来绘制。

也就是利用硬件加速复杂的clipPath可能不支持,但是Bitmap肯定是支持的?

void onDraw(Canvas canvas) {
	super.onDraw(canvas);

	mTempCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

	mTempCanvas.clipPath(mClipPath);

	mTempCanvas.drawBitmap(mTargetBitmap, null, mRect, null);

	Rect rect = new Rect(0, 0, sceneWidth, sceneHeight);

	canvas.drawBitmap(mTempBitmap, rect, rect, null);

}

4. 尝试开启硬件加速后附加设置硬件层解决

项目中有个页面使用复杂路径通过Canvas.clipPath做路径切割,导致部分P版本机型出现刷新闪烁的问题,设置硬件层后解决。

	...
	if (!enableClipPath) {
		...
		if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
			setLayerType(View.LAYER_TYPE_NONE, null)
		}
	} else {
		...
		if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
			//表示视图具有硬件层,硬件加速前提下,附加使用硬件层可用于将复杂视图树缓存到纹理并降低绘图操作的复杂性。
			setLayerType(View.LAYER_TYPE_HARDWARE, null)
		}
	}




八、其他问题记录

  • 启用硬件加速需要更多资源 ,因此应用会占用更多内存

  • view.isHardwareAccelerated()一定要在attached到window以后调用才能得到,在onCreate(),onResume()都是获取不到的

  • 做动画或者复杂的显示效果的时候,总是设置LAYER_TYPE_HARDWARE,肯定是没有问题的

    View.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
    animator.addListener(new AnimatorListenerAdapter() {
    	@Override
    	public void onAnimationEnd(Animator animation) {
    		view.setLayerType(View.LAYER_TYPE_NONE, null);
    	}
    });
    animator.start();
    
  • 项目中有个页面非常卡,fps很低,发现是给一个很大的view设置了alpha,然后加上硬件层后性能发生了翻天覆地的改善

    view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    

九、参考资料

【深度好文:理解Android硬件加速原理】

baijiahao.baidu.com/s?id=170917…

【Android硬件加速原理与实现简介-美团技术团队 ​】 blog.csdn.net/caizehui/ar…

【android canvas 卡顿,Android Canvas 硬件加速引起的clipPath失效问题】 blog.csdn.net/weixin\_398…

【android 动画硬件加速】【ClipPath】 blog.csdn.net/weixin\_269… 

【Android-硬件加速】

blog.csdn.net/goldenfish1…

【官方文档】【硬件加速】

developer.android.com/guide/topic…