一次应用层的渲染问题排查记录

自己平时就爱打开GPU呈现模式,看看自家的App和别家App的应用渲染性能怎么样,去思考他们可能会存在的业务逻辑.当然这个过程还是以自家App为主,不过奈何自己是业务线上的一名普通员工,日常开发功能迭代排期满满,根本无暇顾及性能,更别提所谓优化与思考.不过终于在一件事情之后,我有了时间去关注性能和相关的学习.下面进入正题:

一.发现问题

Screenshot_20220312-150527.png 如上图所示这是一个普通瀑布流展示的推荐信息列表,我们可以清楚直观地发现在渲染过程中黄条耗时过长,经过查阅官方文档,说明是处理/交换缓冲区的问题.

官方对问题的描述很直白:

当此分段较长时 有一点必须要注意:GPU 与 CPU 是并行工作的。Android 系统向 GPU 发出绘制命令,然后继续执行下一个任务。GPU 从队列中读取并处理这些绘制命令。

如果 CPU 发出命令的速度快于 GPU 处理命令的速度,这两个处理器之间的通信队列就会被占满。出现这种情况时,CPU 会阻塞并等待,直到队列中有位置来放置下一个命令。这种队列占满状态通常出现在“交换缓冲区”阶段,因为此时已提交了整个帧的命令。

但是官方的解决方案描述的就是非常模糊:

缓解此问题的关键是降低 GPU 工作的复杂度,就像您在“发出命令”阶段所做的那样。

这句话的白话含义是:你自己干了啥你不清楚吗?好好排查下. 个人理解,其实到了GPU这一步,系统其实是无法区分出上一层级是个啥东西,你上一层是普通应用使用的系统SDK API, 还是自定义OpenGL结合SurfaceView去自行绘制,还是一个游戏应用,系统在这一步是不清楚的,系统只知道这一步,你提交了一些东西交给GPU去渲染,但是任务量比较大.所以文档中并没有提供明确的排查方案.这导致我在面对该问题时,无从下手.

二.思路分析

虽然官方并没有提供明确的排查方向,但是既然是GPU工作负担过重,我们可以直面问题,是哪些情况导致GPU负担过重,根据Android 渲染机制——原理篇(显示原理全过程解析),初期的猜测是应用侧因为频繁调用setLayoutParams()方法导致的频繁更新DisPlayList导致,或者因为是推荐列表图片较多时,使用Glide方式不当导致的.那么顺着这个思路,我做了如下的排查处理:

三.排查过程

初步排查

这里就不展示排查过程中的GPU呈现模式的截图了,而且以下的分析过程并不是按时间顺序排序,这里仅仅是做个排查过程中的总结,同时因为对应业务逻辑简单,排查手法就比较简单粗暴,每一个都是单独分析,没有叠加分析.

  • 干掉业务代码中所有调用setLayoutParams()方法,进行分析,调优效果不理想
  • 干掉业务代码中除了设置图片以外的其他代码实现,调优效果不理想
  • 干掉业务代码中设置图片的代码实现,调优效果好,暂时达到心中目标,但与业务违背
  • 干掉业务代码中滑动监听的代码实现,调优效果不理想

这里发现在干掉设置图片的代码后,性能有所提升,按照影响性来讲,很可能是这里的问题.虽然存在单个性能影响不大,多个性能问题叠加影响大的场景,但是因为本例中设置图片的前后性能差别较大,还是适合单一性能问题.所以接下来就进入针对性排查

针对性排查:图片相关

这里代码并没有什么,仅仅是一行图片加载库的简单调用:

ImageLoader.with(getContext()).load(ecItem.picUrl).placeholder(R.color.colorFAFAFA).into(((MineRecommendViewHolder) holder).ivItem);
复制代码

因为业务比较简单, ImageLoader类仅仅是对Glide库的简单封装,所以,我即使直接使用Glide库进行图片加载,也没有对性能造成影响.但是性能问题出现在这里,让我开始认真审视这句代码. 这句代码传入了ImageView给Glide,这不禁让我好奇,Glide是调用了哪个方法将图片设置上去的.于是,我在 ImageView的源码中加入断点,发现Glide最终是调用了ImageViewsetImageDrawable()方法设置的.

public class DrawableImageViewTarget extends ImageViewTarget<Drawable> {

  public DrawableImageViewTarget(ImageView view) {
    super(view);
  }

  /** @deprecated Use {@link #waitForLayout()} instead. */
  // Public API.
  @SuppressWarnings({"unused", "deprecation"})
  @Deprecated
  public DrawableImageViewTarget(ImageView view, boolean waitForLayout) {
    super(view, waitForLayout);
  }

  @Override
  protected void setResource(@Nullable Drawable resource) {
    view.setImageDrawable(resource);
  }
}
复制代码

而在ImageViewsetImageDrawable(resource)源码中:

public void setImageDrawable(@Nullable Drawable drawable) {
    if (mDrawable != drawable) {
        mResource = 0;
        mUri = null;

        final int oldWidth = mDrawableWidth;
        final int oldHeight = mDrawableHeight;

        updateDrawable(drawable);

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();
    }
}
复制代码

我发现在debug时每次调用这个方法,都会调用requestLayout()方法,这让我是十分不解,是不是Glide使用方式不对导致每次设置图片都要调用requestLayout(),于是,我打算搜一搜,这一搜,让我找到了性能元凶.

四.发现问题

我利用搜索引擎搜索“glide requestLayout”, 搜索后的其中一条结果是NineGridImageView的onLayout死循环, 而这条结果给了我灵感,会不会我的问题也出现在自定义View上,因为本例中的图片显示View非官方的ImageView而是一个早期引入的自定义View,所以我把当前的列表中的自定义View替换成官方的ImageView,结果渲染性能马上就好了:

Screenshot_20220313-145825.png

从GPU呈现模式上看,黄色变浅而且条基本都缩短在绿线附近,而不像之前在红线附近.

五.问题分析

这个案例中使用的自定义View叫NBImageView,是由项目早期一个自大的开发者引入的一个网上的自定义View,该View可以实现对图片的一些圆形、圆角、边框处理,根据该自定义View中的一些注释,我搜到了NBImageView 网络上对应的原始版本,叫NiceImagerView项目链接,发现该项目创建于3、4年前,与本项目的起点时间近乎相同,且人气不高.该项目应该仅仅是其作者的一个小Demo,以至于后期没有对android9及其后续版本进行适配.

该项目的核心是NiceImagerView类,这个类是继承自系统ImageView,并加入了自身的一些处理,在其中的onDraw()方法中,是这样的:

override fun onDraw(canvas: Canvas) {
    // 使用图形混合模式来显示指定区域的图片
    canvas.saveLayer(srcRectF, null, Canvas.ALL_SAVE_FLAG)
    if (!isCoverSrc) {
        val sx = 1.0f * (_width - 2 * borderWidth - 2 * innerBorderWidth) / _width
        val sy = 1.0f * (_height - 2 * borderWidth - 2 * innerBorderWidth) / _height
        // 缩小画布,使图片内容不被borders覆盖
        canvas.scale(sx, sy, _width / 2.0f, _height / 2.0f)
    }
    super.onDraw(canvas)
    paint.reset()
    path.reset()
    if (isCircle) {
        path.addCircle(_width / 2.0f, _height / 2.0f, radius, Path.Direction.CCW)
    } else {
        path.addRoundRect(srcRectF, srcRadii, Path.Direction.CCW)
    }

    paint.isAntiAlias = true
    paint.style = Paint.Style.FILL
    paint.xfermode = xfermode

    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
        canvas.drawPath(path, paint)
    } else {
        srcPath.reset()
        srcPath.addRect(srcRectF, Path.Direction.CCW)
        // 计算tempPath和path的差集
        srcPath.op(path, Path.Op.DIFFERENCE)
        canvas.drawPath(srcPath, paint)
    }
    paint.xfermode = null

    // 绘制遮罩
    if (maskColor != 0) {
        paint.color = maskColor
        canvas.drawPath(path, paint)
    }
    // 恢复画布
    canvas.restore()
    // 绘制边框
    drawBorders(canvas)
}
复制代码

onDraw()方法一上来就调用了canvas.saveLayer()方法,这个方法是做什么的呢? 根据《Android自定义开发入门与实战》第一版中的描述,该方法:

会生成一块全新的画布(Bitmap),这块画布的大小就是我们指定的所要保存区域的大小.新生成的画布是全透明的,在调用saveLayer()函数后所有的绘图操作都是在这块画布上进行的.

看到这里,感觉上流程是没有问题,但是当我点进saveLayer()方法里面查看源码时,却发现官方的注释是这样的:

Note: this method is very expensive, incurring more than double rendering cost for contained content. Avoid using this method, especially if the bounds provided are large. It is recommended to use a hardware layer on a View to apply an xfermode, color filter, or alpha, as it will perform much better than this method.

这注释说的再明确不过了,调用该方法会多出两倍的渲染消耗,并推荐我们使用硬件加速去处理自定义View.

到此,本次的渲染问题排查就暂告一断落.

六.总结

(一).和本例相关的

1.本例中的渲染性能瓶颈到底找到没有

可以说只抓到了一个点.第一在通用层,我们看到自定义ViewNBImageViewonDraw()方法中的一处性能瓶颈,至于该方法中其他代码,该类其他代码还有没有性能瓶颈不清楚,这需要详细分析该类代码才能得知,第二在业务层,我们看到替换官方ImageView后的GPU渲染呈现条也没有很优秀的表现,这需要结合业务继续深挖分析.

2.怎么证明本文中分析的自定义View就是本例的性能瓶颈

其实上文中描述的就像我标题写的仅仅是一次排查记录,描述了根据以往经验,我的思路和根据思路我的排查实践.没有任何数据支持本例中的性能瓶颈就是自定义View,其实更科学的方式是使用性能分析工具(例如Systrace)来数据化各个方法的耗时,以此进行分析,得出结论.而自己是在分析列表性能瓶颈时无意间发现了saveLayer()方法的渲染瓶颈,单纯的觉得应该将整个过程记录下来.而且就算按照下面第3小节的方案去处理,所提升的性能不一定能比肩到本例中直接使用ImageView的替换方案,需要实践,这个后续我可能还会出一篇博客进行分析.

3.如果正如排查所讲是自定义View问题,该如何解决本例问题

其实针对本例来讲,使用该View无非是想实现图片的圆角处理,android应用开发发展到现在,已经有很多框架能够实现这一功能,无非是开发者自身是否知道了解以及使用习惯的问题.

  • 继续使用该自定义View,分析整个自定义View类的代码,更改替换其中性能瓶颈的代码
  • 替换成其他性能良好的第三方自定义View库
  • 自定义Glide库中的DrawTransformation去实现该功能
  • 使用官方SDK提供的相关类

(二).其他思考

应用开发者在需要引入任何目标三方库的时候,都应该对目标三方库进行评估:

  • 该库的社区口碑如何,是否正在维护
  • 该库的功能是否能够完全满足我现在所需,是否有超出我需要范围之外的功能,是否在未来确定时间会添加我期望的功能
  • 该库的可自定义扩展性如何
  • 该库的性能是怎样的,有无数据安全、线程安全问题等

Supongo que te gusta

Origin juejin.im/post/7074503156234715172
Recomendado
Clasificación