diffuse: reading what actually changed inside an Android artifact
Diffuse is a tool for diffing APKs, AABs, AARs, and JARs
At a glance
- What is it?
- A Kotlin command line tool that diffs APKs, AABs, AARs and JARs at the level of dex strings, types, methods and resources, aimed at the single-PR library bump nobody wants to review by hand.
- Who is it for?
- diffuse is at its best on exactly the case the README describes: one dependency bump in one pull request, where the binary delta is small enough to reason about but too opaque to review by expanding a zip. The summary table gives you the shape of the change in seconds, and the per-section listing tells you whether a method count that went up by one is something you meant.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A summary table first, then a per-section listing
The README's opening example is the best argument for the tool, and it is a real one: updating the Dagger library inside the SDK Search app. The invocation takes two APKs and nothing else.
What comes back is layered. At the top is a compressed and uncompressed size table broken out by APK section, with old, new and diff columns for dex, arsc, manifest, res, asset and other, plus a total row. Under that is a DEX counts table with rows for count, strings, types, classes, methods and fields, where the diff column carries a sign and a plus/minus breakdown, such as `-2 (+6 -8)` on strings and `+1 (+6 -5)` on methods.
An ARSC table follows with configs and entries. Then the detail sections begin, one banner per format, and inside the DEX section the strings, types and methods each get their own listing of what was added and what was removed, with a `+` or `-` prefix on every line.
Read that structure as an answer to a specific question. A method count rising by one looks alarming; the detail section turns it into six added methods and five removed ones, which is a dependency upgrade renaming things rather than new code appearing. That is the entire value proposition: aggregate numbers tell you something changed, and the listing tells you what.
Three subcommands and one flag per input format
diffuse has multiple subcommands, and the README documents each one with the same shape.
The primary subcommand is `diff`, which takes two binaries and displays a summary and detailed listing of the changes between them. The format is selected by flag:
$ diffuse diff old.apk new.apk
$ diffuse diff --aab old.aab new.aab
$ diffuse diff --aar old.aar new.aar
$ diffuse diff --jar old.jar new.jarThe `info` subcommand answers the same summary question for a single binary, which makes it the tool you reach for first when you have an artifact and no baseline:
$ diffuse info my.apk
$ diffuse info --aab my.aab
$ diffuse info --aar my.aar
$ diffuse info --jar my.jarThe third subcommand is `members`, which lists the methods, fields, or both of a binary. The README is candid that this mimics the behavior of dex-member-list, the tool Diffuse is derived from, which is a useful clue about where the project's scope came from:
$ diffuse members my.apk
$ diffuse members --methods my.apk
$ diffuse members --aar --fields my.aarAnything beyond that is delegated to the CLI itself, with the README pointing you at running with `--help` for subcommand options and arguments. The one per-format flag pattern is consistent across all three subcommands, which makes the tool easy to script even without reading the help output in advance.
R8 mappings and lambda numbering decide what the diff means
Two details in the example output explain why the diff looks the way it does, and neither is obvious from the command line.
The first is R8. The strings listing in the example includes a line reading `~~R8{"compilation-mode":"release","min-api":24,...}`. Both the old and the new build carry one of these, and they differ, because the mapping file identifier changes on every build. A tool that ignored the mapping would either report that line as a spurious change or, worse, show minified names on both sides and hide real differences.
The second is lambdas. The release notes for 0.2.0 record a change to hide trailing numbers from lambda methods in an attempt to clean up diffs. Compiled lambdas get names with a numeric suffix that increments per compilation, so two otherwise identical builds produce method names differing only in a digit, and the diff fills up with noise that means nothing.
Neither fix is exotic, and both are the kind of thing that only matters once you have run the tool on real release builds. The 0.2.0 notes also mention handling comments in R8 mapping files that are indented with whitespace, which is the same category of problem: the tool is parsing artifacts produced by other people's tools, and those formats have more variation than the specification implies.
Zip entry sizes and why the compressed column can surprise you
Diffuse reports two sizes for every entry, a compressed size and an uncompressed size, and the diff column for each. In the example the DEX entry loses 25 bytes compressed and 112 bytes uncompressed, while a nine-patch drawable loses 14 bytes compressed and nothing uncompressed.
There is a blog post linked from the README on calculating the true impact of zip file entries, which is the author explaining why that distinction is worth caring about. The 0.2.0 release notes add a related fix: supporting reading zip entry sizes when the contents were streamed, which requires handling a trailing data descriptor, and this reportedly allows more accurately reporting the zip storage size of entries.
In practice this is the difference between asking how much a change costs you on disk and asking how much data moves over the wire. Deflate does not compress PNGs, so a resized or re-encoded image can shrink substantially uncompressed while the compressed column barely moves, and an entry whose contents did not change at all can still show a compressed delta if the entry was repacked.
If you are reviewing a build-size regression, this is the part of the output that tells you whether the number you are looking at is a real cost or a repackaging artifact.
Three releases, each one a small specific fix
The version history is short enough to read in full, which is unusual and informative.
0.1.0 shipped on 2020-08-27 as the initial release. 0.2.0 followed on 2024-02-13 and is the substantial one: `.aab` base module diffs, display of v4 signatures, lambda number hiding, an ASM update to handle newer Java bytecode versions, a binary-resources update to handle empty `.arsc` tables, and a migration of the CLI to the Gradle application plugin, which changes the artifact from an executable jar to a zip. It also fixed applying the R8 mapping file before displaying a dex diff, which is a correctness fix rather than a feature.
0.3.0 arrived on 2024-02-13's successor, 2024-02-21, eight days later, adding support for `.dex` as an input type through a `--dex` flag and ensuring an empty line appears between the summary table and the diff items.
That release shape tells you how to treat the project. It moves when the formats it parses move, and it does not accumulate features. The repository's last push was on 2026-09-25 on the `trunk` branch, and with 2,188 stars and 27 open issues it is a project people depend on quietly rather than watch closely.
Installation is one of two things. On macOS, a single brew command:
$ brew install JakeWharton/repo/diffuseElsewhere, a ZIP from the latest release. There is no other documented path, which is consistent with a Kotlin tool that ships as a self-contained executable.
What the repository layout says about extensibility
The tree is small and organized by responsibility: a `diffuse/` directory for the tool itself, a `formats/` directory that almost certainly holds one implementation per input format, an `io/` directory for the zip and binary reading, a `reports/` directory for the table rendering, and a `test-helpers/` directory alongside a Gradle build with the wrapper checked in.
Separating formats from the tool and reports from both is the structural hint that adding a format is a contained job. The `members` subcommand's debt to dex-member-list, and the flags that map one to one onto the format names in the description, both point the same way. There is also a RELEASING.md at the root, which for a project with three releases is a stronger signal of care than another release would be.
The README closes with two resources beyond the tool documentation, a talk about diffing changes in an APK and the zip entry sizing post. For a tool whose output format is the entire user experience, those are the right companion pieces: they explain why the tables look the way they do instead of leaving you to infer it from sample output.
Editorial conclusion
diffuse is at its best on exactly the case the README describes: one dependency bump in one pull request, where the binary delta is small enough to reason about but too opaque to review by expanding a zip. The summary table gives you the shape of the change in seconds, and the per-section listing tells you whether a method count that went up by one is something you meant. The honest limit is stated in the README itself, that the tool is meant for small changes such as a single PR or git SHA, so a first-time app comparison will produce more output than a person wants to read. Version 0.3.0 added bare dex input with the `--dex` flag, and the three releases since 2020 are small and specific. Install with the one-line brew command or grab a ZIP, and reach for `info` first on an artifact you have never looked at, since it prints the summary table without needing a second file to compare against.
Frequently asked questions
What is Diffuse used for?
Diffuse compares two Android build artifacts and shows what changed inside them, rather than just whether their bytes differ. It reports size deltas per APK section, counts of dex strings, types, classes, methods and fields, resource table changes, and per-file listings of what was added and removed.
Which file formats can Diffuse compare?
APKs, AABs, AARs and JARs, each selected with a flag on the subcommand, for example `diffuse diff --aab old.aab new.aab`. Since version 0.3.0 a bare `.dex` file is also accepted through the `--dex` flag.
How do I install Diffuse?
On macOS, brew has a formula: `brew install JakeWharton/repo/diffuse`. On other platforms, download a ZIP from the latest release page. The CLI migrated to the Gradle application plugin in version 0.2.0, so the downloadable artifact is a zip rather than an executable jar.
What does the Diffuse members subcommand do?
It lists the methods, fields, or both contained in a binary, with `--methods` and `--fields` narrowing the output. The README notes that this mimics the behavior of dex-member-list, the tool Diffuse is derived from.
Why does Diffuse show both compressed and uncompressed sizes?
Because the two numbers answer different questions and frequently disagree. Deflate does not compress PNGs well, so a re-encoded image can change uncompressed size while barely moving compressed size, and entries can show a compressed delta from repackaging alone even when their contents are identical. The linked post on calculating the true impact of zip file entries covers the reasoning in more depth.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jakewharton-diffuse)