Skip to content

Commit 2e4f54b

Browse files
committed
User Class.forName instead of ServiceLoader to instantiate Dispatchers.Main on Android
Fixes #1557 Fixes #878
1 parent 1652bb9 commit 2e4f54b

File tree

5 files changed

+71
-8
lines changed

5 files changed

+71
-8
lines changed

kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ private val handlers: List<CoroutineExceptionHandler> = ServiceLoader.load(
2222
CoroutineExceptionHandler::class.java.classLoader
2323
).iterator().asSequence().toList()
2424

25-
2625
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
2726
// use additional extension handlers
2827
for (handler in handlers) {

kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt

+59-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import java.net.*
55
import java.util.*
66
import java.util.jar.*
77
import java.util.zip.*
8+
import kotlin.collections.ArrayList
9+
10+
/**
11+
* Don't use JvmField here to enable R8 optimizations via "assumenosideeffects"
12+
*/
13+
internal val ANDROID_DETECTED = runCatching { Class.forName("android.os.Build") }.isSuccess
814

915
/**
1016
* A simplified version of [ServiceLoader].
@@ -20,7 +26,59 @@ import java.util.zip.*
2026
internal object FastServiceLoader {
2127
private const val PREFIX: String = "META-INF/services/"
2228

23-
internal fun <S> load(service: Class<S>, loader: ClassLoader): List<S> {
29+
/**
30+
* This method attempts to load [MainDispatcherFactory] in Android-friendly way.
31+
*
32+
* If we are not on Android, this method fallbacks to a regular service loading,
33+
* else we attempt to do `Class.forName` lookup for
34+
* `AndroidDispatcherFactory` and `TestMainDispatcherFactory`.
35+
* If lookups are successful, we return resultinAg instances because we know that
36+
* `MainDispatcherFactory` API is internal and this is the only possible classes of `MainDispatcherFactory` Service on Android.
37+
*
38+
* Such intricate dance is required to avoid calls to `ServiceLoader.load` for multiple reasons:
39+
* 1) It eliminates disk lookup on potentially slow devices on the Main thread.
40+
* 2) Various Android toolchain versions by various vendors don't tend to handle ServiceLoader calls properly.
41+
* Sometimes META-INF is removed from the resulting APK, sometimes class names are mangled, etc.
42+
* While it is not the problem of `kotlinx.coroutines`, it significantly worsens user experience, thus we are workarounding it.
43+
* Examples of such issues are #932, #1072, #1557, #1567
44+
*
45+
* We also use SL for [CoroutineExceptionHandler], but we do not experience the same problems and CEH is a public API
46+
* that may already be injected vis SL, so we are not using the same technique for it.
47+
*/
48+
internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
49+
val clz = MainDispatcherFactory::class.java
50+
if (!ANDROID_DETECTED) {
51+
return load(clz, clz.classLoader)
52+
}
53+
54+
return try {
55+
val result = ArrayList<MainDispatcherFactory>(2)
56+
createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
57+
createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
58+
result
59+
} catch (e: Throwable) {
60+
// Fallback to the regular SL in case of any unexpected exception
61+
load(clz, clz.classLoader)
62+
}
63+
}
64+
65+
/*
66+
* This method is inline to have a direct Class.forName("string literal") in the byte code to avoid weird interactions with ProGuard/R8.
67+
*/
68+
@Suppress("NOTHING_TO_INLINE")
69+
private inline fun createInstanceOf(
70+
baseClass: Class<MainDispatcherFactory>,
71+
serviceClass: String
72+
): MainDispatcherFactory? {
73+
return try {
74+
val clz = Class.forName(serviceClass, true, baseClass.classLoader)
75+
baseClass.cast(clz.getDeclaredConstructor().newInstance())
76+
} catch (e: ClassNotFoundException) { // Do not fail if TestMainDispatcherFactory is not found
77+
null
78+
}
79+
}
80+
81+
private fun <S> load(service: Class<S>, loader: ClassLoader): List<S> {
2482
return try {
2583
loadProviders(service, loader)
2684
} catch (e: Throwable) {

kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt

+4-6
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@ internal object MainDispatcherLoader {
2020
private fun loadMainDispatcher(): MainCoroutineDispatcher {
2121
return try {
2222
val factories = if (FAST_SERVICE_LOADER_ENABLED) {
23-
MainDispatcherFactory::class.java.let { clz ->
24-
FastServiceLoader.load(clz, clz.classLoader)
25-
}
23+
FastServiceLoader.loadMainDispatcherFactory()
2624
} else {
27-
//We are explicitly using the
28-
//`ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
29-
//form of the ServiceLoader call to enable R8 optimization when compiled on Android.
25+
// We are explicitly using the
26+
// `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
27+
// form of the ServiceLoader call to enable R8 optimization when compiled on Android.
3028
ServiceLoader.load(
3129
MainDispatcherFactory::class.java,
3230
MainDispatcherFactory::class.java.classLoader

ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro

+4
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@
33
# this results in direct instantiation when loading Dispatchers.Main
44
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
55
boolean FAST_SERVICE_LOADER_ENABLED return false;
6+
}
7+
8+
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
9+
boolean ANDROID_DETECTED return true;
610
}

ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro

+4
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33
# - META-INF/proguard/coroutines.pro
44

55
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
6+
7+
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
8+
boolean ANDROID_DETECTED return true;
9+
}

0 commit comments

Comments
 (0)