Mobile Development 10 min read

Android APK Size Optimization: Removing Duplicate Resources and Image Compression with a Gradle Plugin

This article explains how the Android resources.arsc file is generated, identifies the problem of duplicate and uncompressed image resources in multi‑module projects, and presents a Gradle‑based solution that inserts a custom task after processReleaseResources to deduplicate resources, compress images using pngquant, guetzli and cwebp, and repack the .ap_ files, resulting in significant APK size reduction.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Android APK Size Optimization: Removing Duplicate Resources and Image Compression with a Gradle Plugin

1. Introduction

The resources.arsc file in an APK stores compiled binary representations of all non‑code resources (strings, images, arrays, colors, etc.). It is generated automatically by the Android Asset Packaging Tool (AAPT) during the build process, enabling fast runtime access to resources.

2. Pain Points

In multi‑developer or modular Android projects, duplicate image resources often accumulate, inflating the APK size. Manual compression with tools like TinyPNG cannot be reliably enforced across teams, so an automated solution is needed.

3. Finding the Intervention Point

The key build task to hook is processReleaseResources , which packages resources and creates the resources-${variantName}.ap_ archive containing AndroidManifest.xml , resources.arsc , drawables, etc. By adding a custom task after this step, we can manipulate the .ap_ file before it is packaged into the final APK.

4. Solution

The overall workflow is:

Locate the process${variantName}Resource task.

Insert a custom task after it.

Iterate over all generated .ap_ files in the task’s output directory.

For each .ap_ file:

override fun apply(p0: Project) {
    // Global configuration
    p0.extensions.create(CONFIG_NAME, Config::class.java)
    p0.extensions.create(REPEAT_RES_CONFIG_NAME, RepeatResConfig::class.java)
    p0.extensions.create(COMPRESS_IMG_CONFIG_NAME, CompressImgConfig::class.java)

    val hasAppPlugin = p0.plugins.hasPlugin(AppPlugin::class.java)
    if (hasAppPlugin) {
        p0.afterEvaluate {
            // ... omitted for brevity ...
            byType.applicationVariants.forEach {
                val variantName = it.name.capitalize()
                val processRes = p0.tasks.getByName("process${variantName}Resources")
                processRes.doLast {
                    val resourcesTask = it as LinkApplicationAndroidResourcesTask
                    val files = resourcesTask.resPackageOutputFolder.asFileTree.files
                    files.filter { file -> file.name.endsWith(".ap_") }
                        .forEach { apFile ->
                            // Decompress, deduplicate, compress, re‑zip logic
                        }
                }
            }
        }
    }
}

4.1 Delete Duplicate Resources

The plugin parses the resources.arsc binary, examines ResourceTableChunk entries, writes a mapping of duplicate files to the retained one, deletes the duplicates, and updates string pool indices accordingly.

private fun deleteRepeatRes(
    unZipPath: String,
    mappingFile: File,
    apFile: File,
    ignoreName: MutableList
?
) {
    val fileWriter = FileWriter(mappingFile)
    val groupsResources = ZipFile(apFile).groupsResources()
    val arscFile = File(unZipPath, RESOURCE_NAME)
    val newResource = FileInputStream(arscFile).use { input ->
        val fromInputStream = ResourceFile.fromInputStream(input)
        // ... iterate over duplicate groups, write mapping, delete files, update chunks ...
    }
    // Replace the original arsc with the modified one
    arscFile.delete()
    FileOutputStream(arscFile).use { it.write(newResource.toByteArray()) }
}

4.2 Compress Images

Image compression is performed in three ways:

PNG files are compressed with pngquant .

JPG files are compressed with guetzli .

Both PNG and JPG can be converted to WebP using cwebp , then compressed.

private suspend fun CoroutineScope.compressionImg(
    mappingFile: File,
    unZipDir: String,
    config: CompressImgConfig,
    webpsLsit: CopyOnWriteArrayList
) {
    val mappginWriter = FileWriter(mappingFile)
    launch {
        val file = File("$unZipDir${File.separator}res")
        file.listFiles()
            .filter { it.isDirectory && (it.name.startsWith("drawable") || it.name.startsWith("mipmap")) }
            .flatMap { it.listFiles().toList() }
            .filter { config.whiteListName?.contains(it.name)?.let { !it } ?: true }
            .filter { ImageUtil.isImage(it) }
            .forEach { img ->
                launch(Dispatchers.Default) {
                    when (config.optimizeType) {
                        OPTIMIZE_COMPRESS_PICTURE -> {
                            val reduceSize = compressImg(img)
                            // write mapping
                        }
                        OPTIMIZE_WEBP_CONVERT -> {
                            val webp0K = ImageUtil.securityFormatWebp(img, config)
                            // write mapping and collect WebP info
                        }
                        else -> println("Unsupported optimizeType, falling back to compression")
                    }
                }
            }
        }.join()
        mappginWriter.close()
    }
}

5. Benefits

Before applying the plugin, the resources.arsc size was 736.4 KB and the total APK size was 16.8 MB. After optimization, the resources.arsc shrank to 162.2 KB and the APK reduced to 12.8 MB, demonstrating a substantial reduction in package size.

before

after

resources.arsc

736.4 KB

162.2 KB

Total APK

16.8 MB

12.8 MB

Source code: https://github.com/fuusy/ResCompressPlugin

mobile developmentAndroidresource optimizationAPKimage compressionGradle Plugin
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.