Mobile Development 15 min read

Resource Optimization Practices in DeWu App: Plugins, Image Compression, Deduplication, and ARSC Shrinking

DeWu reduces its APK size by removing unused plugin resources, compressing images with WebP, Guetzli, PNGQuant and Gifsicle, deduplicating and obfuscating ARSC entries, applying 7‑zip compression, delivering large assets dynamically, and continuously cleaning unused resources through compile‑time checks and runtime scanning.

DeWu Technology
DeWu Technology
DeWu Technology
Resource Optimization Practices in DeWu App: Plugins, Image Compression, Deduplication, and ARSC Shrinking

Package size optimization often starts with resource optimization. This article describes the practical techniques used in the DeWu Android app to reduce APK size by optimizing resources.

1. Plugin Optimization

The latest version of DeWu gains 12 MB by removing unused plugin resources. The plugin initialization downloads the required executable files from OSS when the runtime environment is missing.

1.2 Image Compression

During development, images are manually compressed with tools such as TinyPNG. At build time, a Gradle plugin compresses third‑party and missed images. The plugin uses cwebp for WebP conversion, guetzli for JPEG, pngquant for PNG, and gifsicle for GIF. Compression is applied to res files (WebP first) and to assets files with format‑preserving compression.

fun compressImg(imgFile: File): Long {
    if (ImageUtil.isJPG(imgFile) || ImageUtil.isGIF(imgFile) || ImageUtil.isPNG(imgFile)) {
        val lastIndexOf = imgFile.path.lastIndexOf(".")
        if (lastIndexOf < 0) {
            println("compressImg ignore ${imgFile.path}")
            return 0
        }
        val tempFilePath = "${imgFile.path.substring(0, lastIndexOf)}_temp${imgFile.path.substring(lastIndexOf)}"
        if (ImageUtil.isJPG(imgFile)) {
            Tools.cmd("guetzli", "--quality 85 ${imgFile.path} $tempFilePath")
        } else if (ImageUtil.isGIF(imgFile)) {
            Tools.cmd("gifsicle", "-O3 --lossy=25 ${imgFile.path} -o $tempFilePath")
        } else if (ImageUtil.isPNG(imgFile)) {
            Tools.cmd(
                "pngquant",
                "--skip-if-larger --speed 1 --nofs --strip --force  --quality=75  ${imgFile.path} --output $tempFilePath"
            )
        }
        val oldSize = imgFile.length()
        val tempFile = File(tempFilePath)
        val newSize = tempFile.length()
        return if (newSize in 1 until oldSize) {
            if (imgFile.exists()) imgFile.delete()
            tempFile.renameTo(File(imgFile.path))
            oldSize - newSize
        } else {
            if (tempFile.exists()) tempFile.delete()
            0L
        }
    }
    return 0
}

Assets compression follows a similar approach but uses a different Gradle task. The code below iterates over merged assets, skips whitelisted files, and applies CompressUtil.compressImg to the rest.

val mergeAssets = project.tasks.getByName("merge${variantName}Assets")
mergeAssets.doLast { task ->
    (task as MergeSourceSetFolders).outputDir.asFileTree.files.filter {
        val originalPath = it.absolutePath.replace(task.outputDir.get().toString() + "/", "")
        val filter = context.compressAssetsExtension.whiteList.contains(originalPath)
        if (filter) println("Assets compress ignore:$originalPath")
        !filter
    }.forEach { file ->
        val originalPath = file.absolutePath.replace(task.outputDir.get().toString() + "/", "")
        val reduceSize = CompressUtil.compressImg(file)
        if (reduceSize > 0) {
            assetsShrinkLength += reduceSize
            assetsList.add("$originalPath => reduce[${byteToSize(reduceSize)}]")
        }
    }
    println("assets optimized:${byteToSize(assetsShrinkLength)}")
}

1.3 Resource Deduplication

Deduplication works on the resources.arsc binary. Identical files are detected using CRC32 (with an MD5 secondary check) and removed. The ARSC table is then updated to point duplicate entries to the retained file.

// Find duplicate resources
val groupResources = ZipFile(apFile).groupsResources()
val resourcesFile = File(unZipDir, "resources.arsc")
val md5Map = HashMap
>()
val newResouce = FileInputStream(resourcesFile).use { stream ->
    val resouce = ResourceFile.fromInputStream(stream)
    groupResources.asSequence()
        .filter { it.value.size > 1 }
        .map { entry ->
            entry.value.forEach { zipEntry ->
                if (whiteList.isEmpty() || !whiteList.contains(zipEntry.name)) {
                    val file = File(unZipDir, zipEntry.name)
                    MD5Util.computeMD5(file).takeIf { it.isNotEmpty() }?.let {
                        val set = md5Map.getOrDefault(it, HashSet())
                        set.add(zipEntry)
                        md5Map[it] = set
                    }
                }
            }
            md5Map.values
        }
        .filter { it.size > 1 }
        .forEach { collection ->
            collection.forEach { it ->
                val zips = it.toTypedArray()
                val coreResources = zips[0]
                for (index in 1 until zips.size) {
                    val repeatZipFile = zips[index]
                    result?.add("${repeatZipFile.name} => ${coreResources.name}    reduce[${byteToSize(repeatZipFile.size)}]")
                    File(unZipDir, repeatZipFile.name).delete()
                    resouce.chunks.filterIsInstance
().forEach { chunk ->
                        val stringPoolChunk = chunk.stringPool
                        val idx = stringPoolChunk.indexOf(repeatZipFile.name)
                        if (idx != -1) stringPoolChunk.setString(idx, coreResources.name)
                    }
                }
            }
        }
    resouce
}

1.4 Resource Obfuscation

After deduplication, long resource paths are replaced with short ones by modifying ResourceTableChunk . This reduces the size of string pools and the overall ARSC file. A whitelist keeps reflective calls safe.

val resourcesFile = File(unZipDir, "resources.arsc")
val newResouce = FileInputStream(resourcesFile).use { inputStream ->
    val resouce = ResourceFile.fromInputStream(inputStream)
    resouce.chunks.filterIsInstance
().forEach { chunk ->
        val stringPoolChunk = chunk.stringPool
        val strings = stringPoolChunk.getStrings() ?: return@forEach
        for (i in 0 until stringPoolChunk.stringCount) {
            val v = strings[i]
            if (v.startsWith("res")) {
                if (ignore(v, context.proguardResourcesExtension.whiteList)) {
                    // keep white‑listed resource
                    continue
                }
                // replace with short path
                val newPath = createProcessPath(v, builder)
                stringPoolChunk.setString(i, newPath)
            }
        }
    }
    resouce
}

1.5 ARSC Compression

Compressing resources.arsc with 7‑zip reduces its size from ~7 MB to ~700 KB. However, for target SDK 30+ this compression is disabled because it harms memory usage and startup performance.

2. Resource Delivery

Large static resources (SO files, images) are delivered dynamically. A platform monitors resource size, schedules cleanup, and uses CI to prevent new oversized assets.

3. Unused Resource Removal

Unused resources are identified by combining compile‑time bytex resCheck results with runtime scanning (matrix‑apk‑canary). Detected resources are displayed on a management platform for incremental cleanup.

Conclusion

The article outlines DeWu’s resource‑optimization workflow: plugin shrinking, image compression, deduplication, obfuscation, ARSC compression, dynamic delivery, and unused‑resource removal. Further improvements could include smarter 9‑patch delivery, image‑similarity detection, and modular plugin distribution.

AndroidResource OptimizationAPKARSCGradleimage compressionKotlin
DeWu Technology
Written by

DeWu Technology

A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.

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.