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.
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.
DeWu Technology
A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.