package kotlinx.coroutines.internal import java.io.* import java.net.* import java.util.* import java.util.jar.* import java.util.zip.* import kotlin.collections.ArrayList /** * Don't use JvmField here to enable R8 optimizations via "assumenosideeffects" */ internal val ANDROID_DETECTED = runCatching { Class.forName("android.os.Build") }.isSuccess /** * A simplified version of [ServiceLoader]. * FastServiceLoader locates and instantiates all service providers named in configuration * files placed in the resource directory META-INF/services. * * The main difference between this class and classic service loader is in skipping * verification JARs. A verification requires reading the whole JAR (and it causes problems and ANRs on Android devices) * and prevents only trivial checksum issues. See #878. * * If any error occurs during loading, it fallbacks to [ServiceLoader], mostly to prevent R8 issues. */ internal object FastServiceLoader { private const val PREFIX: String = "META-INF/services/" /** * This method attempts to load [MainDispatcherFactory] in Android-friendly way. * * If we are not on Android, this method fallbacks to a regular service loading, * else we attempt to do `Class.forName` lookup for * `AndroidDispatcherFactory` and `TestMainDispatcherFactory`. * If lookups are successful, we return resultinAg instances because we know that * `MainDispatcherFactory` API is internal and this is the only possible classes of `MainDispatcherFactory` Service on Android. * * Such intricate dance is required to avoid calls to `ServiceLoader.load` for multiple reasons: * 1) It eliminates disk lookup on potentially slow devices on the Main thread. * 2) Various Android toolchain versions by various vendors don't tend to handle ServiceLoader calls properly. * Sometimes META-INF is removed from the resulting APK, sometimes class names are mangled, etc. * While it is not the problem of `kotlinx.coroutines`, it significantly worsens user experience, thus we are workarounding it. * Examples of such issues are #932, #1072, #1557, #1567 * * We also use SL for [CoroutineExceptionHandler], but we do not experience the same problems and CEH is a public API * that may already be injected vis SL, so we are not using the same technique for it. */ internal fun loadMainDispatcherFactory(): List { val clz = MainDispatcherFactory::class.java if (!ANDROID_DETECTED) { return load(clz, clz.classLoader) } return try { val result = ArrayList(2) createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) } createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) } result } catch (e: Throwable) { // Fallback to the regular SL in case of any unexpected exception load(clz, clz.classLoader) } } /* * This method is inline to have a direct Class.forName("string literal") in the byte code to avoid weird interactions with ProGuard/R8. */ @Suppress("NOTHING_TO_INLINE") private inline fun createInstanceOf( baseClass: Class, serviceClass: String ): MainDispatcherFactory? { return try { val clz = Class.forName(serviceClass, true, baseClass.classLoader) baseClass.cast(clz.getDeclaredConstructor().newInstance()) } catch (e: ClassNotFoundException) { // Do not fail if TestMainDispatcherFactory is not found null } } private fun load(service: Class, loader: ClassLoader): List { return try { loadProviders(service, loader) } catch (e: Throwable) { // Fallback to default service loader ServiceLoader.load(service, loader).toList() } } // Visible for tests internal fun loadProviders(service: Class, loader: ClassLoader): List { val fullServiceName = PREFIX + service.name // Filter out situations when both JAR and regular files are in the classpath (e.g. IDEA) val urls = loader.getResources(fullServiceName) val providers = urls.toList().flatMap { parse(it) }.toSet() require(providers.isNotEmpty()) { "No providers were loaded with FastServiceLoader" } return providers.map { getProviderInstance(it, loader, service) } } private fun getProviderInstance(name: String, loader: ClassLoader, service: Class): S { val clazz = Class.forName(name, false, loader) require(service.isAssignableFrom(clazz)) { "Expected service of class $service, but found $clazz" } return service.cast(clazz.getDeclaredConstructor().newInstance()) } private fun parse(url: URL): List { val path = url.toString() // Fast-path for JARs if (path.startsWith("jar")) { val pathToJar = path.substringAfter("jar:file:").substringBefore('!') val entry = path.substringAfter("!/") // mind the verify = false flag! (JarFile(pathToJar, false)).use { file -> BufferedReader(InputStreamReader(file.getInputStream(ZipEntry(entry)), "UTF-8")).use { r -> return parseFile(r) } } } // Regular path for everything else return BufferedReader(InputStreamReader(url.openStream())).use { reader -> parseFile(reader) } } // JarFile does no implement Closesable on Java 1.6 private inline fun JarFile.use(block: (JarFile) -> R): R { var cause: Throwable? = null try { return block(this) } catch (e: Throwable) { cause = e throw e } finally { try { close() } catch (closeException: Throwable) { if (cause === null) throw closeException cause.addSuppressed(closeException) throw cause } } } private fun parseFile(r: BufferedReader): List { val names = mutableSetOf() while (true) { val line = r.readLine() ?: break val serviceName = line.substringBefore("#").trim() require(serviceName.all { it == '.' || Character.isJavaIdentifierPart(it) }) { "Illegal service provider class name: $serviceName" } if (serviceName.isNotEmpty()) { names.add(serviceName) } } return names.toList() } }