背景
公司提交代码需要提交Screenshot test(UI test的一种,就是将做好的view截图下来,之后每次提PR都要run一次跟之前的对比是否有影响,有改动.使用的是Facebook的screenshot tools,支持像素级别的对比校验.)小伙伴有一个需求就是将UI库中的iconCardView(自定义的view)中的icon(AppCompatImageView
)的layout_width
和layout_height
由32dp
改成36dp
,本来只是一个很小的改动,但是在Screenshot test的时候发现一个神奇的事情,没有用到iconCradView的业务,报错了说run出来的截图跟之前保存起来的不一致.经过查看之后发现共同点就是,都一样用了同一个res
(同一个SVG xml资源文件,width
和height
均为20dp
)然后就有了这篇文章的出现,分析Android SVG VectorDrawable cache缓存机制.
猜想
- 现象
将iconCardView
的layout_width
和layout_height
由32dp
改成36dp
,Screenshot test会报错不通过,影响范围是共用svg xml资源(尽管没有使用iconCardView
)的业务截图 - 问题分析
更改view的layout_width
和layout_height
为什么会影响同用到svg xml资源的view的显示 - 猜想
根据 How VectorDrawable works(需翻墙)提到的SVG xml file->Android VectorDrawable->bitmap->view可知.我们引入svg xml file后会利用VectorDrawable
rasterize(栅格化)为bitmap,之后再用在App里面.
所以就去了解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 page
和36 page
.normal page
用于模拟问题中的受影响的界面,36page
用于模拟将32dp
改成36dp
的那个自定义view.-
normal page
layout code
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity2"> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="356dp" android:text="20dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.461" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/img" android:layout_width="20dp" android:layout_height="20dp" android:layout_marginStart="172dp" android:layout_marginTop="176dp" android:layout_marginBottom="41dp" android:src="@drawable/ic_card" app:layout_constraintBottom_toTopOf="@+id/textView2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
36 page
layout code
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity3"> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/img" android:layout_width="36dp" android:layout_height="36dp" android:layout_marginTop="196dp" android:src="@drawable/ic_card" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
实验步骤
- 打开
normal page
,截图命名为P1
- 关闭
normal page
后,再次打开normal page
,截图命名为P2
- 打开
36page
- 关闭
36page
后,再次打开normal page
,截图命名为P3
- 关闭
normal page
后,再次打开normal page
,截图命名为P4
- 重新安装没有
36page
的版本APP - 打开
normal page
,截图命名为P5
- 实验结果对比意义
-
P1
vsP2
,可以验证同一个APP下,多次打开是否会影响VectorDrawable
的draw结果 -
P2
vsP3
,可以验证打开width
andheight
更大的AppCompatImageView
是否会对原本页面的VectorDrawable
的draw结果有影响 -
P3
vsP4
,意义与P1
vsP2
相同. -
P5
实验对照组,可以不同APP下,是否会影响VectorDrawable
的draw结果
-
- 实验结果对比图
由实验对比图可知,P1
P2
P5
均相同,P3
P4
相同,P2
P3
不同.
则可得出,- 不同APP和多次打开均不会影响
VectorDrawable
的draw结果 - 打开
width
andheight
更大的AppCompatImageView
会对原本页面的VectorDrawable
的draw结果有影响
原图请在附件中下载验证.
- 不同APP和多次打开均不会影响
分析
- 根据实验结果可确定,当view的宽高一致时,
VectorDrawable
会使用已经存在的缓存,没有会重新创建.当view宽高变得更大的时候,VectorDrawable
会更新缓存.当需要重新打开宽高较小的其他view时,会使用更新后的缓存.所以会导致P2
和P3
不一致,但是P3
和P4
相同. - 源码分析.
- 根据源码VectorDrawable.java的
draw()
417行可知会call C++层的Draw()
- 转而分析VectorDrawable.cpp的
Draw()
,主要看注释和447行的outCanvas->drawVectorDrawable(this)
由注释可知bitmap的大小由bounds和canvas scale决定.
- 分析447行的
outCanvas->drawVectorDrawable(this)
,根据SkiaCanvas.cpp可知,还是调用vectorDrawable.cpp
的drawStaging()
- 分析
vectorDrawable.cpp->drawStaging()
可知,如果redrawNeeded
或mStagingCache.dirty
为true则会调用updateBitmapCache()
去update cache.
- 因为我们遇到的问题是跟宽高有关,所以我们就先看这个给
redrawNeeded
赋值的allocateBitmapIfNeeded()
,根据599行canReuseBitmap(cache.bitmap.get(), width, height)
,我们可以看到612,613行方法return bitmap && width <= bitmap->width() && height <= bitmap->height()
敲黑板!终于来了!!!就是这里,当前请求的width
和height
均小于等于bitmapCache
的width
和height
时才可以reuse,否则就需要调用updateBitmapCache()
去update cache.
- 根据源码VectorDrawable.java的
延伸(挖坑)
-
图像失真与模糊
根据vectorDrawable.cpp
的canReuseBitmap()
我们可以知道它的处理逻辑,但是为什么它这么写呢?是人性的扭曲还是道德的沦丧.敬请期待图像失真与模糊
附件
参考
Ps
小伙伴说现在的标题要这么起,决定搞个A/B测试,发布了两篇文章,内容一样,标题不同,看看哪篇文章阅读量高一点.