An exception occurs when R8 is enabled and FAST_SERVICE_LOADER_ENABLED is set to false.
Closed this issue · 15 comments
Describe the bug
AGP:8.11.1
R8:8.11.18
Kotlin:1.9.0
Kotlinx-coroutines-android:1.9.0
Caused by java.lang.IllegalStateException
Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'
Caused by java.lang.IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'
at kotlinx.coroutines.internal.MainDispatchersKt.throwMissingMainDispatcherException(MainDispatchers.kt:81)
at kotlinx.coroutines.internal.MainDispatchersKt.createMissingDispatcher(MainDispatchers.kt:78)
at kotlinx.coroutines.internal.MainDispatchersKt.createMissingDispatcher$default(MainDispatchers.kt:76)
at kotlinx.coroutines.internal.MainDispatcherLoader.loadMainDispatcher(MainDispatchers.kt:39)
at kotlinx.coroutines.internal.MainDispatcherLoader.(MainDispatchers.kt:22)
at kotlinx.coroutines.Dispatchers.getMain(Dispatchers.kt:57)
at androidx.lifecycle.LifecycleKt.getCoroutineScope(Lifecycle.kt:300)
at androidx.lifecycle.LifecycleOwnerKt.getLifecycleScope(LifecycleOwner.kt:45)
at com.dami.vipkid.engine.login.verify.VerifyCodeFragment.initData(VerifyCodeFragment.kt:166)
at com.dami.vipkid.engine.base.view.BaseFragment.onViewCreated(BaseFragment.java:84)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3128)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1899)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1817)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1760)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:547)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8500)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:640)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1026)
This issue frequently occurs on [Transsion] devices, but we cannot reproduce it currently as we don't have access to such a device.
The obfuscated content:
/* loaded from: classes2.dex */
public final class MainDispatcherLoader {
private static final boolean FAST_SERVICE_LOADER_ENABLED = false;
@NotNull
public static final MainDispatcherLoader INSTANCE;
@JvmField
@NotNull
public static final c2 dispatcher;
static {
MainDispatcherLoader mainDispatcherLoader = new MainDispatcherLoader();
INSTANCE = mainDispatcherLoader;
a0.f("kotlinx.coroutines.fast.service.loader", true);
dispatcher = mainDispatcherLoader.loadMainDispatcher();
}
private MainDispatcherLoader() {
}
private final c2 loadMainDispatcher() {
Object next;
c2 e10;
try {
List u10 = SequencesKt___SequencesKt.u(SequencesKt__SequencesKt.c(ServiceLoader.load(MainDispatcherFactory.class, MainDispatcherFactory.class.getClassLoader()).iterator()));
Iterator it2 = u10.iterator();
if (it2.hasNext()) {
next = it2.next();
if (it2.hasNext()) {
int loadPriority = ((MainDispatcherFactory) next).getLoadPriority();
do {
Object next2 = it2.next();
int loadPriority2 = ((MainDispatcherFactory) next2).getLoadPriority();
if (loadPriority < loadPriority2) {
next = next2;
loadPriority = loadPriority2;
}
} while (it2.hasNext());
}
} else {
next = null;
}
MainDispatcherFactory mainDispatcherFactory = (MainDispatcherFactory) next;
if (mainDispatcherFactory != null && (e10 = p.e(mainDispatcherFactory, u10)) != null) {
return e10;
}
return p.b(null, null, 3, null);
} catch (Throwable th) {
return p.b(th, null, 2, null);
}
}
}
Hello! Since the issue only occurs on specific devices, please use the Android issue tracker for this: in kotlinx.coroutines, we don't have any logic like "if this is a Transsion device, fail". I've also read through the obfuscated code, and it looks entirely reasonable, so the problem probably happens not during the obfuscation but at some later point. I don't see what our library can do about the problem.
Of course, if it turns out we can do something to work around this issue, let us know, and we'll reopen the issue!
Is it possible to remove the following rule from the file coroutines.pro: -assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader { boolean FAST_SERVICE_LOADER_ENABLED return false; }? Because using the Fast Service Loader hasn't caused any issues.
@JayneVong, if your project is using those coroutines.pro rules, then something is wrong with the minification: the only reason FAST_SERVICE_LOADER_ENABLED is false is that R8 can skip service loading, which as we can see, is still present in the obfuscated code for some reason. Could you please provide a project where the issue reproduces, that is, where the obfuscated code keeps the ServiceLoader.load(MainDispatcherFactory.class, MainDispatcherFactory.class.getClassLoader()) call?
This file configuration contains our final obfuscation configuration for the project. Because FAST_SERVICE_LOADER_ENABLED is forced to false, R8 optimization removes the call to FastServiceLoader, meaning that ServiceLoader is always used instead of FastServiceLoader.
Refer to the coroutines source code: source code
val factories = if (FAST_SERVICE_LOADER_ENABLED) {
FastServiceLoader.loadMainDispatcherFactory()
} else {
// We are explicitly using the
// `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
// form of the ServiceLoader call to enable R8 optimization when compiled on Android.
ServiceLoader.load(
MainDispatcherFactory::class.java,
MainDispatcherFactory::class.java.classLoader
).iterator().asSequence().toList()
}Unfortunately, I cannot reproduce this issue on my test device. This issue originated from Firebase Crashlytics, and here is the original error report: https://github.com/JayneVong/resource/blob/master/com.vipkid.ark.international.parent_issue_d117a48fc4a650153e8480ec1ea30f09_crash_session_68D43B6F019A00015E838770698A2B5B_DNE_0_v2_stacktrace.txt
@JayneVong, this is not what I mean. The minified version in #4533 (comment) is incorrect, it shouldn't look like this. Here's what it looks like in my sample project:
package q;
import java.util.Iterator;
import java.util.List;
import o.O;
public final class s {
public static final O f144a;
static {
int i2 = x.f147a;
z.b("kotlinx.coroutines.fast.service.loader", true);
f144a = a();
}
private static O a() {
List c2;
Object next;
O c3;
try {
c2 = m.c.c(m.c.a(r.a())); // <------- THIS IS IMPORTANT
Iterator it = c2.iterator();
if (it.hasNext()) {
next = it.next();
if (it.hasNext()) {
int loadPriority = ((q) next).getLoadPriority();
do {
Object next2 = it.next();
int loadPriority2 = ((q) next2).getLoadPriority();
if (loadPriority < loadPriority2) {
next = next2;
loadPriority = loadPriority2;
}
} while (it.hasNext());
}
} else {
next = null;
}
q qVar = (q) next;
if (qVar != null && (c3 = t.c(qVar, c2)) != null) {
return c3;
}
t.a(null, 3);
throw null;
} catch (Throwable th) {
t.a(th, 2);
throw null;
}
}
}package q;
import java.util.Arrays;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import kotlinx.coroutines.android.AndroidDispatcherFactory;
public abstract /* synthetic */ class r {
public static /* synthetic */ Iterator a() {
try {
return Arrays.asList(new AndroidDispatcherFactory()).iterator(); // <---- THIS IS IMPORTANT
} catch (Throwable th) {
throw new ServiceConfigurationError(th.getMessage(), th);
}
}
}As you can see, no ServiceLoader is called, because that call is supposed to be optimized by R8. For some reason, in your project, this doesn't happen. Could you please share a project whose minified version includes a call to ServiceLoader.load?
Here is a simplified project: https://github.com/JayneVong/R8ProguardDemo
The obfuscation rules are the same as my main project rules, and the AGP version is also v8.11.1.
Ok, found the root of the problem. It's these lines in your ProGuard config:
-dontoptimize-keep class kotlinx.coroutines.internal.MainDispatcherFactory { *; }-dontoptimize
If you remove them, the ServiceLoader optimization kicks in, and the rules behave as expected.
Is removing these lines viable in your case?
OK, I'll try.
Can all of the following rules be removed?
# Kotlin coroutines rules
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;}
-keep class kotlinx.coroutines.internal.MainDispatcherFactory { *; }
-keep class kotlinx.coroutines.internal.MainDispatcherLoader { *; }
-keepnames class * implements kotlinx.coroutines.internal.MainDispatcherFactory
-keepnames class * implements kotlinx.coroutines.CoroutineExceptionHandlerMy final compiled result seems to be different from yours
kotlinx.coroutines.internal.MainDispatcherLoader -> u3.d:
package u3;
import android.os.Looper;
import androidx.activity.b0;
import e3.r;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
/* loaded from: classes.dex */
public abstract class d {
/* renamed from: a, reason: collision with root package name */
public static final t3.b f5189a;
static {
String str;
Iterable iterable;
int i3 = g.f5190a;
Object obj = null;
try {
str = System.getProperty("kotlinx.coroutines.fast.service.loader");
} catch (SecurityException unused) {
str = null;
}
if (str != null) {
Boolean.parseBoolean(str);
}
try {
Iterator it = Arrays.asList(new t3.a()).iterator();
k3.c.e(it, "<this>");
Iterator it2 = new q3.a(new q3.e(it)).iterator();
if (it2.hasNext()) {
Object next = it2.next();
if (it2.hasNext()) {
ArrayList arrayList = new ArrayList();
arrayList.add(next);
while (it2.hasNext()) {
arrayList.add(it2.next());
}
iterable = arrayList;
} else {
iterable = b0.I(next);
}
} else {
iterable = r.f3154f;
}
Iterator it3 = iterable.iterator();
if (it3.hasNext()) {
obj = it3.next();
if (it3.hasNext()) {
((t3.a) obj).getClass();
do {
((t3.a) it3.next()).getClass();
} while (it3.hasNext());
}
}
if (((t3.a) obj) == null) {
throw new IllegalStateException("Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'");
}
Looper mainLooper = Looper.getMainLooper();
if (mainLooper == null) {
throw new IllegalStateException("The main looper is not available");
}
f5189a = new t3.b(t3.c.a(mainLooper), false);
} catch (Throwable th) {
throw new ServiceConfigurationError(th.getMessage(), th);
}
}
}
kotlinx.coroutines.android.AndroidDispatcherFactory -> t3.a:
package t3;
/* loaded from: classes.dex */
public final class a {
}
Can all of the following rules be removed?
Yes, you don't need any of these rules, we already ship them with the library.
My final compiled result seems to be different from yours
What matters in this case is that AndroidDispatcherFactory gets invoked without going through a ServiceLoader first, and this should fix the original issue you've reported. Module with the Main dispatcher is missing is what the ServiceLoader says when it can't find the AndroidDispatcherFactory, but here, it's explicitly placed into the list.
Got it, thanks.
I understand the original issue stems from an error loading the ServiceLoader at runtime. The current solution is to find the AndroidDispatcherFactory at compile time after enabling R8.
Yes, that's right. It still makes sense to contact the Android team, maybe they could look into it.