
R8 Full Mode and the LogSessionId Crash That Keep Rules Couldn’t Fix
Muhammad Kamran
I turned on R8 for an Android video app in mid-August. The reason was material-icons-extended ships 11,386 icon classes and the app references 32 of them. Unminified, the DEX is 61 MB. Minified it is 11 MB. That is not an optimisation you skip.
That is not a small improvement. So turning R8 on was an easy decision.
But after some days I notice that my app is crashing on some devices and I was not able to understand why my app is crashing on some devices then I further investigate I notice a pattern that my app is crashing on below android 12.
The crash
On older Android devices, users could open the video compressor normally, but the moment an export started, the app crashed:
java.lang.NoClassDefFoundError: Failed resolution of:
Landroid/media/metrics/LogSessionId;
at androidx.media3.transformer.DefaultAssetLoaderFactory
.createAssetLoader(...)
Caused by: java.lang.ClassNotFoundException:
Didn't find class "android.media.metrics.LogSessionId"
on path: DexPathList[...]
Three things about this made it slow to find.
It only happened in minified builds, so I cannot reproduce on debug build. It only happened on Android 11 and below, and my own device was not falling in these version. And the app has minSdk 24, which means a large slice of real users were on exactly the versions I could not test on.
Nothing in my own code is anywhere near this. LogSessionId is a platform class, DefaultAssetLoaderFactory is inside Media3, and I had not touched either.
The fix I thought would work
I found the upstream issue, androidx/media#2535, and interpreted it as a shrinking problem.
So I added four keep rules:
-keep class android.media.metrics.LogSessionId { *; }
-keep class android.media.metrics.** { *; }
-keep class androidx.media3.transformer.ExoPlayerAssetLoader$Factory { *; }
-keep class androidx.media3.transformer.TransformerInternal$SequenceAssetLoaderListener { *; }
At that point, I genuinely believed the issue was solved. It wasn't.
Two of those rules were never relevant
The first two rules target:
android.media.metrics.*
Those are Android platform classes.
They are provided by the Android OS and live on the device's bootclasspath. They are not classes sitting inside my APK waiting for R8 to shrink them.
So this:
-keep class android.media.metrics.LogSessionId { *; }
looks perfectly reasonable at first glance, but R8 has nothing in the application package to keep there.
The two Transformer classes are real and are in the APK, so keeping them does something, It stops R8 renaming or removing them. But the crash is not a renamed or deleted class. NoClassDefFoundError on a platform type has nothing to do with it.
What was actually happening
Media3 reads a logSessionId and passes it to a constructor. On a device below API 31 that value is null, and Media3 is fine with that plain ART will happily carry a null reference of a type that does not exist on the device, as long as nothing forces the class to resolve.
R8 full mode forces it to resolve. Optimising that path, it rewrites the getfield and the constructor call into API-outlined helpers, and those helpers are guarded by:
check-cast android.media.metrics.LogSessionId
A check-cast is a resolution point. ART has to load the class to check the cast, the class does not exist below API 31, and you get NoClassDefFoundError before the null ever mattered.
So the crash was never about a class being removed. It was about an instruction being added. That is the part no keep rule can reach: -keep tells R8 what not to rename or delete. It says nothing about what R8 is allowed to rewrite.
I wanted proof, not another theory
At this point, I could have added another keep rule and hoped.
Instead, I disassembled the release DEX.
The check was simple:
dexdump -d classes.dex | grep -E 'check-cast.*LogSessionId'
With R8 full mode: 3 instructions.
With compatibility mode: 0.
That was basically the whole diagnosis. And honestly, it took less time than the hours I had spent trying to reason about keep rules.
That changed how I approach release-only R8 bugs. When you suspect the optimizer changed something, inspect the code that actually shipped. Don't just stare at the source and guess what R8 probably did.
The actual fix
The real fix was one line:
android.enableR8.fullMode=false
That switches R8 back to compatibility mode.
You still get shrinking. You just give up more aggressive optimizations full mode performs.
For this particular app, the trade-off was acceptable. The release DEX measures roughly:
61 MB unminified
9 MB R8 full mode
11 MB R8 compatibility mode
So disabling full mode cost me about 2 MB, and shrinking is still doing almost all of its work.
In exchange, video compression actually worked on Android 11 and below. That's an extremely easy trade to make.
One important detail here is that R8 full mode has been the default since AGP 8.0.
I never explicitly thought:
I'm enabling aggressive R8 full mode today.
I thought:
I'm enabling minifyEnabled.
Full mode came with it. That's one reason this can catch people off guard.
The versions involved
For reference, this was the stack I was using:
Media3: 1.10.1
AGP: 9.2.1
minSdk: 24
What I do differently now
The biggest change is simple.
When a release build crashes but the debug build works, I don't immediately start writing keep rules. First, I disable R8 full mode:
android.enableR8.fullMode=false
If the crash disappears, I have strong evidence that I'm dealing with an optimization issue. That immediately removes an entire category of possibilities, and I can investigate the specific transformation instead of randomly keeping classes. And if it doesn't help, I've still eliminated one major variable.
That's a much better debugging loop than:
crash → add keep rule → build succeeds → assume fixed
I also think about keep rules differently now.
A keep rule isn't free. Every rule you add can prevent shrinking and optimization, and if you are guessing, that rule may sit in the project for years. The worst part is that future-you won't remember why it exists.
That was already happening in my project. Those two platform-class rules looked important. They weren't. Someone reading the configuration a year from now could easily assume they were essential to the Media3 crash. They were just noise.
The keep rule I don't want to write
This is the nuclear option:
-keep class com.yourapp.** { *; }
Yes, it can make certain R8 problems disappear. But it does so by effectively telling R8:
Stop shrinking my application.
You enabled R8 because you wanted a smaller, optimized application. Turning off shrinking for the entire package defeats the purpose.
The part I got wrong
The embarrassing part of this story isn't that I hit an R8 bug. That's normal.
The mistake was more subtle. I saw:
NoClassDefFoundError
and immediately thought:
R8 probably removed something.
So I reached for -keep.
But R8 isn't only a shrinker. It's an optimizer.
A missing class error does not automatically mean a class was deleted or renamed. Sometimes the optimizer changes the code so that a class gets resolved when it previously never needed to be. That's what happened here.
Enjoyed this article?
Check out more of my content or get in touch if you'd like to work together on your next project.


