震惊!Android SVG 渲染机制居然如此...

背景

公司提交代码需要提交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 APINote可知,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
    <?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>
    
  • 实验步骤

  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.javadraw() 417行可知会call C++层的Draw()
      VectorDrawable.java draw()
    2. 转而分析VectorDrawable.cppDraw(),主要看注释和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

Ps

小伙伴说现在的标题要这么起,决定搞个A/B测试,发布了两篇文章,内容一样,标题不同,看看哪篇文章阅读量高一点.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342