Optimizing Android Vector Images. Or Not?

September 18, 2018

An average working day of an Android developer involves doing something with UI. Pushing widgets around, changing text and images…

The colleges of mine convert SVG images to VectorDrawable via a specific converter and I use Android Studio. Which tool does the job better? Let’s find out!

Tools

Converters (do optimizations under the hood):

Optimizers:

💡 Zeplin exports SVG assets using SVGO under the hood.

Images

Octicons

These are examples of semi-complicated web icons. Image sizes are unconventional — more than 1024 dp. It is not advisable to use vector images of such size, but let’s look at it as a push-to-the-limit approach.

Material

I suspect that these icons were designed with mobile in mind and were pre-optimized for mobile rendering, but that might be completely false. Both of them are conventionally sized to 24 dp.

Comparison

XML

It is not surprising, but files produced by different tools are different. Especially when it comes to the most important thing — android:pathData. This <path> attribute serves as an instructions set in terms of go there, paint this.

It reminds me of the very first programming language I’ve used — Logo. It can be used to teach basic programming concepts using so-called turtle graphics.

📖 android:pathData uses the exact same format as d attribute in SVG files. Mozilla provides great documentation for it with neat samples.

Let’s see how it looks in action.

The SVG specification does not care about separators, so commas can be replaced with spaces and vice-versa. In other words, M20,4L4,4 from Android Studio is exactly the same as M20 4L4 4 from svg2vector.

Taking separators out of the picture, further changes go to squashing and replacing operations. For example, L4,4 4,6 from svg2android is the same as L4,4 v2 from Android Studio.

Operations squashing helps with optimizing drawing performance since the renderer can make more efficient decisions based on the instruction. At the same time, fewer commands mean more efficient execution. Actually, Android Lint has a VectorPath check for such cases.

📖 The maximum instructions count for Lint is 800. See the source code for details.

Do not forget that #apksizematters! Less instructions → less text → less file size.

Besides pathData differences, there are some minor deviations as well.

Performance

Method

Most of the time VectorDrawable goes straight into ImageView. Restarting application over and over to get necessary numbers is kind of tedious, especially if there are going to be thousands of passes. The benchmark code measures VectorDrawable#draw calls instead.

_Click to expand the benchmark code._
companion object {
    private const val ITERATIONS_COUNT = 10_000
}

private val random = Random()

private fun measure() {
    listOf(
            R.drawable.android_studio,
            R.drawable.svg2android,
            R.drawable.svg2vector,
            R.drawable.android_studio_and_avocado,
            R.drawable.svgo_and_android_studio
    ).forEach { drawableRes ->

        val drawableName = resources.getResourceEntryName(drawableRes)

        val measurements = (0 until ITERATIONS_COUNT)
            .map { measure(drawableRes) }

        val averageParseMillis = measurements
            .map { it.parseTimeMillis }
            .average()
        val averageRenderMillis = measurements
            .map { it.renderTimeMillis }
            .average()

        println(":: [$drawableName]. Parse: $averageParseMillis ms. Render: $averageRenderMillis ms.")
    }
}

private data class Measurement(val parseTimeMillis: Long, val renderTimeMillis: Long)

private fun measure(@DrawableRes drawableRes: Int): Measurement {
    lateinit var drawable: Drawable

    val parseTime = measureTimeMillis {
        // Unfortunately it seems to be impossible to disable internal caching.
        drawable = getDrawable(drawableRes)
    }

    if (drawable !is VectorDrawable) {
        throw IllegalArgumentException("Drawable is supposed to be VectorDrawable.")
    }

    // Change tint to prevent internal render internal caching.
    drawable.setTint(random.nextInt())

    val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)

    drawable.setBounds(0, 0, canvas.width, canvas.height)

    val renderTime = measureTimeMillis {
        drawable.draw(canvas)
    }

    return Measurement(parseTime, renderTime)
}

Environment

📖 Seems like in Android 7.0 VectorDrawable was re-implemented to use native rendering. Relative numbers remain the same, but be advised that absolute performance might differ between API versions.

Results

octicon-octoface

Tool Average Render Duration, ms Average Parse Duration, ms
Android Studio 24.5128 0.0594
svg2android 20.1871 0.0570
svg2vector 24.5424 0.0581
Android Studio + Avocado 24.5393 0.0596
SVGO + Android Studio 24.5721 0.0619

svg2android looks like the fastest one but it imported SVG incorrectly. Resulting VectorDrawable produced an invisible image.

octicon-repo

Tool Average Render Duration, ms Average Parse Duration, ms
Android Studio 18.1411 0.0575
svg2android 18.1608 0.0604
svg2vector 18.3110 0.0562
Android Studio + Avocado 18.1604 0.0554
SVGO + Android Studio 18.1699 0.0588

material-android

Tool Average Render Duration, ms Average Parse Duration, ms
Android Studio 0.0848 0.0241
svg2android 0.1197 0.0395
svg2vector 0.1275 0.0420
Android Studio + Avocado 0.1189 0.0348
SVGO + Android Studio 0.1287 0.0399

material-store

Tool Average Render Duration, ms Average Parse Duration, ms
Android Studio 0.0936 0.0408
svg2android 0.0892 0.0390
svg2vector 0.0915 0.0366
Android Studio + Avocado 0.0898 0.0405
SVGO + Android Studio 0.0944 0.0405

Conclusion

It is hard to declare a winner with XML. I like that Android Studio removes useless instructions and attributes. It doesn’t matter how android:pathData looks though since it is not getting modified by hand on a regular basis. At the same time, it helps if it is more compact, which Studio does relatively better than others.

About performance — there is no clear winner as well. Android Studio shows good results with small icons and SVGO helps with huge web images. It is understandable — SVG is a mature standard with known optimization techniques. Knowing that android:pathData is SVG path.d it is obvious that relying on a more mature optimizer makes more sense.

In other words, Android Studio is good enough, at least from my point of view. Besides, it makes the SVG import easy-as — open it in IDE and that’s it. If there is a need to optimize an image — SVGO is a good choice.


Thanks to Artem Zinnatullin and Alex Lockwood for the review!