Android SVG VectorDrawable cache缓存分析

背景

公司提交代码需要提交Screenshot test(UI test的一种,就是将做好的view截图下来,之后每次提PR都要run一次跟之前的对比是否有影响,有改动.使用的是Facebook的screenshot tools,支持像素级别的对比校验.)小伙伴有一个需求就是将UI库中的iconCardView(自定义的view)中的icon(AppCompatImageView)的layout_widthlayout_height32dp改成36dp,本来只是一个很小的改动,但是在Screenshot test的时候发现一个神奇的事情,没有用到iconCradView的业务,报错了说run出来的截图跟之前保存起来的不一致.经过查看之后发现共同点就是,都一样用了同一个res(同一个SVG xml资源文件,widthheight均为20dp)然后就有了这篇文章的出现,分析Android SVG VectorDrawable cache缓存机制.

猜想

  • 现象
    iconCardViewlayout_widthlayout_height32dp改成36dp,Screenshot test会报错不通过,影响范围是共用svg xml资源(尽管没有使用iconCardView)的业务截图
  • 问题分析
    更改view的layout_widthlayout_height为什么会影响同用到svg xml资源的view的显示
  • 猜想
    根据 How VectorDrawable works(需翻墙)提到的SVG xml file->Android VectorDrawable->bitmap->view可知.我们引入svg xml file后会利用VectorDrawable rasterize(栅格化)为bitmap,之后再用在App里面.
    svg到APP的流程.png

    所以就去了解VectorDrawable在Android是如何使用的.根据Google Android VectorDrawable API的Note可知,VectorDrawable会因为重画性能问题,会创建1个bitmap cache,然后也提到基于效率,会建议创建多个VectorDrawable,每个VectorDrawable对应不同的size.

Note: To optimize for the re-drawing performance, one bitmap cache is created for each VectorDrawable. Therefore, referring to the same VectorDrawable means sharing the same bitmap cache. If these references don't agree upon on the same size, the bitmap will be recreated and redrawn every time size is changed. In other words, if a VectorDrawable is used for different sizes, it is more efficient to create multiple VectorDrawables, one for each size.

但是这里就没有特别说明这个缓存机制是怎么样,怎么共用,怎么刷新,我们也无法确定就是这个缓存机制造成了现在的问题.所以我们会通过一系列实验去验证是否这个就是root cause.

实验

  • 实验元素
    模拟器:Pixel3a API 29
    MainActivity中有两个button去打开normal page36 page.normal page用于模拟问题中的受影响的界面,36page用于模拟将32dp改成36dp的那个自定义view.

    • normal page layout code
    
    
    
    
    
    
    
    
    • 36 page layout code
    
    
    
    
    
    
    
  • 实验步骤

  1. 打开normal page,截图命名为P1
  2. 关闭normal page后,再次打开normal page,截图命名为P2
  3. 打开36page
  4. 关闭36page后,再次打开normal page,截图命名为P3
  5. 关闭normal page后,再次打开normal page,截图命名为P4
  6. 重新安装没有36page的版本APP
  7. 打开normal page,截图命名为P5
  • 实验结果对比意义
    • P1vsP2,可以验证同一个APP下,多次打开是否会影响VectorDrawable的draw结果
    • P2vsP3,可以验证打开width and height更大的AppCompatImageView是否会对原本页面的VectorDrawable的draw结果有影响
    • P3vsP4,意义与P1vsP2相同.
    • P5实验对照组,可以不同APP下,是否会影响VectorDrawable的draw结果
  • 实验结果对比图
    由实验对比图可知,P1 P2 P5均相同,P3 P4相同,P2 P3不同.
    则可得出,
    1. 不同APP和多次打开均不会影响VectorDrawable的draw结果
    2. 打开width and height更大的AppCompatImageView会对原本页面的VectorDrawable的draw结果有影响
      原图请在附件中下载验证.
      实验对比图

分析

  1. 根据实验结果可确定,当view的宽高一致时,VectorDrawable会使用已经存在的缓存,没有会重新创建.当view宽高变得更大的时候,VectorDrawable会更新缓存.当需要重新打开宽高较小的其他view时,会使用更新后的缓存.所以会导致P2P3不一致,但是P3P4相同.
  2. 源码分析.
    1. 根据源码VectorDrawable.java的draw() 417行可知会call C++层的Draw()
      VectorDrawable.java draw()
    2. 转而分析VectorDrawable.cpp的Draw(),主要看注释和447行的outCanvas->drawVectorDrawable(this)
      由注释可知bitmap的大小由bounds和canvas scale决定.
      VectorDrawable.cpp Draw()
    3. 分析447行的outCanvas->drawVectorDrawable(this),根据SkiaCanvas.cpp可知,还是调用vectorDrawable.cppdrawStaging()
      SkiaCanvas.cpp的drawVectorDrawable()
    4. 分析vectorDrawable.cpp->drawStaging()可知,如果redrawNeededmStagingCache.dirty为true则会调用updateBitmapCache()去update cache.
      vectorDrawable.cpp->drawStaging()
    5. 因为我们遇到的问题是跟宽高有关,所以我们就先看这个给redrawNeeded赋值的allocateBitmapIfNeeded(),根据599行canReuseBitmap(cache.bitmap.get(), width, height),我们可以看到612,613行方法return bitmap && width <= bitmap->width() && height <= bitmap->height()敲黑板!终于来了!!!就是这里,当前请求的widthheight均小于等于bitmapCachewidthheight时才可以reuse,否则就需要调用updateBitmapCache()去update cache.
      allocateBitmapIfNeeded

延伸(挖坑)

  1. 图像失真与模糊
    根据vectorDrawable.cppcanReuseBitmap()我们可以知道它的处理逻辑,但是为什么它这么写呢?是人性的扭曲还是道德的沦丧.敬请期待图像失真与模糊

附件

  1. 实验数据 提取码: w5c5

参考

  1. How VectorDrawable works(需翻墙)
  2. Google Android VectorDrawable API
  3. screenshot-tests-for-android

你可能感兴趣的:(Android SVG VectorDrawable cache缓存分析)