Skip to content

Commit eea2dda

Browse files
committed
User Class.forName instead of ServiceLoader to instantiate Dispatchers.Main on Android
Fixes #1557 Fixes #878
1 parent 3dbe82b commit eea2dda

File tree

9 files changed

+88
-8
lines changed

9 files changed

+88
-8
lines changed

kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro

+4
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@
88
-keepclassmembernames class kotlinx.** {
99
volatile <fields>;
1010
}
11+
12+
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
13+
boolean ANDROID_DETECTED return false;
14+
}

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

+60-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ 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+
* TODO investigate possible java.vendor values for fallbacks
12+
* Don't use JvmField here to enable R8 optimizations via "assumenosideeffects"
13+
*/
14+
internal val ANDROID_DETECTED = systemProp("java.vendor")?.toLowerCase()?.contains("android") ?: false
815

916
/**
1017
* A simplified version of [ServiceLoader].
@@ -20,7 +27,59 @@ import java.util.zip.*
2027
internal object FastServiceLoader {
2128
private const val PREFIX: String = "META-INF/services/"
2229

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

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ repositories {
66
google()
77
}
88

9+
test {
10+
systemProperty 'java.vendor', 'Android'
11+
}
12+
913
dependencies {
1014
testImplementation 'com.google.android:android:4.1.1.4'
1115
testImplementation 'com.android.support:support-annotations:26.1.0'

ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/proguard/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 false;
9+
}

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 false;
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 false;
9+
}

ui/kotlinx-coroutines-android/resources/META-INF/proguard/coroutines.pro

+4
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@
55
# - META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro
66

77
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
8+
9+
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
10+
boolean ANDROID_DETECTED return false;
11+
}

0 commit comments

Comments
 (0)