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

作者: 小鱼人爱编程 | 来源:发表于2021-11-11 13:09 被阅读0次

    前言

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

    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过程(中)

        本文链接:https://www.haomeiwen.com/subject/vkawzltx.html