Android 自定义View之Draw过程(中)

前言

Draw 过程系列文章

  • Android 自定义View之Draw过程(上)
  • Android 自定义View之Draw过程(中)
  • Android 自定义View之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标签下,添加如下字段:

//关闭硬件加速
    
    
或
//开启应将加速
    
    

Android 4.0(含)之后默认开启硬件加速,也就是:
默认 android:hardwareAccelerated="true"

Activity

在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
用图表示其中的关系:

image.png

初步认识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

你可能感兴趣的:(Android 自定义View之Draw过程(中))