Android 自定义View之Draw过程(中)
前言
Draw 过程系列文章
上篇分析了自定义View绘制流程及其常用方法:
Android 自定义View之Draw过程(上)
本篇将以硬件加速绘制与软件绘制入口为切入点,通过本篇文章,你将了解到:
1、什么是硬件加速
2、硬件加速的开启与关闭
3、硬件加速绘制与软件绘制分道扬镳的地方
4、初步认识LayerType
什么是硬件加速
Android 3.0 之前,绘制操作通过CPU完成的,而在Android 3.0(含)之后,Android 2D 渲染管道支持硬件加速,也就是说Canvas绘制操作会使用GPU渲染。
image.png
硬件加速的作用
使用软件绘制的时候,绘制操作都是通过CPU计算并写入Bitmap,最终Bitmap直接渲染到屏幕上。当某个View需要需要刷新的时候,计算刷新的脏区域,有相交的地方都需要重新绘制,也就是重走Draw过程。
使用硬件绘制的时候,绘制操作先将操作记录到RenderNode里,当渲染的时候将这些操作集合交给GPU处理,GPU更擅长处理浮点相关的运算。
当某个View需要刷新的时候,只需要重新生成与之相关的操作指令集,也就是Draw过程。甚至当修改透明度等属性的时候都不需要重走Draw过程,大大减少了无效的绘制请求,节约了CPU时间,提升程序运行流畅度。
当然,硬件加速需要更多资源,因此应用会占用更多内存。
另外有些Canvas API并不支持硬件加速,具体请参考官方文档:https://developer.android.google.cn/guide/topics/graphics/hardware-accel
硬件加速的开启与关闭
硬件加速控制层级
硬件加速分为4个层级来控制,分别为:
1、Application
2、Activity
3、Window
4、View
Application
在Application标签下,添加如下字段:
//关闭硬件加速
<application
android:hardwareAccelerated="false">
</application>
或
//开启应将加速
<application
android:hardwareAccelerated="true">
</application>
Android 4.0(含)之后默认开启硬件加速,也就是:
默认 android:hardwareAccelerated="true"
Activity
在Activity 标签下,添加如下字段:
//关闭硬件加速
<activity android:hardwareAccelerated="false">
</activity>
或
//开启硬件加速
<activity android:hardwareAccelerated="true">
</activity>
和Application一样,Activity也是默认开启硬件加速。
Window
//开启硬件加速
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
目前来说,无法直接关闭Window的硬件加速。(1)后续解释。
View
//禁用硬件加速
View.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
目前来说,无法直接开启View的硬件加速。(2)后续解释。
各个层级关系
按照控制的范围来说:
Application>Activity>Window>View
来看看各个层级开关硬件加速之间的相互影响。
1、当Application 开启了硬件加速
Activity/Window/View 也开启了硬件加速
当然,Activity/View 可以选择关闭硬件加速
2、当Application 关闭了硬件加速
Activity/Window 也关闭了硬件加速
当然,Activity/Window 可以单独开启硬件加速
3、当Activity 开启了硬件加速
Window/View 也开启了硬件加速
当然,View 也可以选择关闭硬件加速
4、当Activity 关闭了硬件加速
View 也关闭了硬件加速
也许你还是觉得比较疑惑,尤其是针对Window和View,到底怎么控制这两个层级的硬件加速呢?
前面说过,硬件加速用在绘制上,而绘制的核心是Canvas,Canvas分为支持硬件加速的Canvas:RecordingCanvas和普通Cavas。因此只需要找到什么时候用
RecordingCanvas,就知道是否支持了硬件加速。
循着这个点,接下来分析代码里是如何控制硬件绘制与软件绘制的。
硬件加速绘制与软件绘制分道扬镳的地方
当View 添加到Window后,会调用ViewRootImpl->setView(xx)方法。
关于过程细节请移步:Window/WindowManager 不可不知之事
#ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
...
//mSurfaceHolder 初始为空
if (mSurfaceHolder == null) {
//使能硬件加速
enableHardwareAcceleration(attrs);
}
...
}
}
}
private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {
//初始标记
mAttachInfo.mHardwareAccelerated = false;
mAttachInfo.mHardwareAccelerationRequested = false;
//判断是否开启硬件加速
final boolean hardwareAccelerated =
(attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0;
if (hardwareAccelerated) {
//判断渲染是否可用
if (!ThreadedRenderer.isAvailable()) {
return;
}
//默认没有设置该标记
final boolean fakeHwAccelerated = (attrs.privateFlags &
WindowManager.LayoutParams.PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED) != 0;
final boolean forceHwAccelerated = (attrs.privateFlags &
WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED) != 0;
if (fakeHwAccelerated) {
mAttachInfo.mHardwareAccelerationRequested = true;
} else if (!ThreadedRenderer.sRendererDisabled
|| (ThreadedRenderer.sSystemRendererDisabled && forceHwAccelerated)) {
if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.destroy();
}
//创建渲染线程 ThreadedRenderer
//并赋值给mAttachInfo->mThreadedRenderer
mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent,
attrs.getTitle().toString());
if (mAttachInfo.mThreadedRenderer != null) {
//将mAttachInfo->mHardwareAccelerated 标记位true
mAttachInfo.mHardwareAccelerated =
mAttachInfo.mHardwareAccelerationRequested = true;
}
}
}
}
以上代码重点关注两个点:
1、硬件加速标记存放在WindowManager.LayoutParams 的flags参数里
2、如果支持硬件加速,则将标记与渲染线程对象记录到View.AttachInfo里
我们知道View展示三大流程是在 performTraversals(xx)里,在该方法里,依次进行Measure、Layout、Draw过程,来看看Draw过程的开启:
#ViewRootImpl.java
private void performTraversals() {
//若有必要初始化渲染线程
hwInitialized = mAttachInfo.mThreadedRenderer.initialize(
mSurface);
//1、Measure
...
//2、Layout
...
//3、Draw
performDraw();
}
private boolean draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
...
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
//mAttachInfo.mThreadedRenderer.isEnabled()->true 前边初始化过了
//硬件加速绘制入口
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
//软件绘制入口
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
return useAsyncReport;
}
以上代码重点关注两个点:
1、在ViewRootImpl->draw(xx)方法里开始了绘制的分歧
2、分歧的依据是View.AttachInfo != null
因此现在的问题简化为:
WindowManager.LayoutParams里的flags 硬件加速标记决定绘制是否走硬件加速流程
WindowManager.LayoutParams 从哪来
回顾一下View是如何添加到Window的:
1、获取WindowManager对象
2、设置LayoutParams属性
3、将View添加到Window里
当调用:
wm.addView(textView, layoutParams);
后续调用如下:
image.png
可以看出,layoutParams 经过层层传递最后到达ViewRootImpl里的setView(xx),也即是上边分析的方法。
因此,我们有理由相信,layoutParams.flags 有关硬件加速的标记一定在某个步骤被赋值了。
首先第一个可能赋值的地方:
layoutParams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
wm.addView(textView, layoutParams);
此过程即为为Window 开启硬件加速。
再来看看剩下的步骤,发现只有WindowManagerGlobal addView(xx)会给layoutParams.flags 赋值:
#WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
//该分支是Activity、Dialog addView(xx) 会走
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
//该分支是直接WindowManager.addView(xx)
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
//(context.getApplicationInfo().flags 是在Application 层级设置的标记
//如果设置了该标记,那么将该标记存储在wparams
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
...
synchronized (mLock) {
try {
...
//此时,wparams 记录了是否有硬件加速
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
}
}
}
继续来看看adjustLayoutParamsForSubWindow:
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
...
//1、mHardwareAccelerated 表示该Window是否支持硬件加速,该值是由Activity 在xml里配置的硬件加速标记决定的
//2、mWindowAttributes.flags & FLAG_HARDWARE_ACCELERATED) 表示之前是否给Window 设置了硬件加速标记
if (mHardwareAccelerated ||
(mWindowAttributes.flags & FLAG_HARDWARE_ACCELERATED) != 0) {
//如果满足,记录在WindowManager.LayoutParams 里
wp.flags |= FLAG_HARDWARE_ACCELERATED;
}
}
从以上两段代码可以看出:
1、Window与Activity关联,如果该Activity开启了硬件加速,那么该Window也开启了硬件加速。
2、Window不与Activity 关联,如果Application 开启了硬件加速,那么该Window也开启了硬件加速。
要关闭Window层级的硬件加速,只需要Activity/Application 禁用硬件加速即可。
实际上,不管Activity/Application/Window 如何设置硬件加速标记,都是反馈到WindowManager.LayoutParams 上,进而反馈到View.AttachInfo,最终反馈到Canvas
用图表示其中的关系:
初步认识LayerType
在所有 Android 版本中,视图能够通过以下两种方式渲染到屏幕外缓冲区(离屏缓存):
1、Canvas.saveLayer()
2、使用视图的绘制缓存 。
屏幕外缓冲区或层具有多种用途。在为复杂的视图添加动画效果或应用合成效果时,我们可以使用离屏缓存获得更好的效果。
此处介绍第二种:
View是通过Canvas绘制的,而Canvas既可以硬件加速绘制也可以软件绘制,于是View同样就拥有了这两种选择。
通过前面的分析可以看出,只要Application/Activity/Window 开启了硬件加速,那么View 也就开启了硬件加速,那么如何关闭View的硬件加速呢?
View.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
该方法简化如下:
#View.java
int mLayerType = LAYER_TYPE_NONE;
public void setLayerType(@LayerType int layerType, @android.annotation.Nullable Paint paint) {
...
mLayerType = layerType;
...
}
public int getLayerType() {
return mLayerType;
}
View里用Int 表示LayerType,可以理解为绘制离屏缓存。
LAYER_TYPE_NONE-->不使用绘制缓存
LAYER_TYPE_SOFTWARE-->使用软件绘制缓存
LAYER_TYPE_HARDWARE-->使用硬件绘制缓存
当使用LAYER_TYPE_SOFTWARE时,就"顺道"禁用了硬件加速,因此View.setLayerType(View.LAYER_TYPE_SOFTWARE, null) 产生了两个作用:
1、启用软件绘制缓存
2、禁用硬件加速
软件绘制、硬件加速绘制、启用离屏缓存 三者对于Draw流程的影响将会在下篇分析,敬请关注。
本文基于 Android 10.0