Skip to content

Commit d6b0b0f

Browse files
wojtek-kalicinskiJakeWharton
authored andcommitted
Adds R8 optimization rule for FastServiceLoader
This anticipates a new mechanism in a future Android Gradle Plugin (3.6.0+) that enables version targeting of ProGuard/R8 rules. It does not change any behavior for current users of the library. Once the new plugin is used, the R8 optimization will be read automatically. It also contains tests. Co-authored-by: Wojtek Kaliciński <wkal@google.com> Co-authored-by: Jake Wharton <jakew@google.com>
1 parent 46b5ea5 commit d6b0b0f

File tree

10 files changed

+192
-1
lines changed

10 files changed

+192
-1
lines changed

ui/kotlinx-coroutines-android/build.gradle

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44

55
repositories {
66
google()
7+
// TODO Remove once R8 is updated to a 1.6.x version.
8+
maven {
9+
url "http://storage.googleapis.com/r8-releases/raw/master"
10+
metadataSources {
11+
artifact()
12+
}
13+
}
14+
}
15+
16+
configurations {
17+
r8
718
}
819

920
dependencies {
@@ -12,6 +23,80 @@ dependencies {
1223

1324
testImplementation 'com.google.android:android:4.1.1.4'
1425
testImplementation 'org.robolectric:robolectric:4.0-alpha-3'
26+
testImplementation 'org.smali:baksmali:2.2.7'
27+
28+
// TODO Replace with a 1.6.x version once released to maven.google.com.
29+
r8 'com.android.tools:r8:a7ce65837bec81c62261bf0adac73d9c09d32af2'
30+
}
31+
32+
class RunR8Task extends JavaExec {
33+
34+
@OutputDirectory
35+
File outputDex
36+
37+
@InputFile
38+
File inputConfig
39+
40+
@InputFile
41+
final File inputConfigCommon = new File('r8-test-common.pro')
42+
43+
@InputFiles
44+
final File jarFile = project.jar.archivePath
45+
46+
@Override
47+
Task configure(Closure closure) {
48+
super.configure(closure)
49+
classpath = project.configurations.r8
50+
main = 'com.android.tools.r8.R8'
51+
52+
def arguments = [
53+
'--release',
54+
'--no-desugaring',
55+
'--output', outputDex.absolutePath,
56+
'--pg-conf', inputConfig.absolutePath
57+
]
58+
arguments.addAll(project.configurations.runtimeClasspath.files.collect { it.absolutePath })
59+
arguments.addAll(jarFile.absolutePath)
60+
61+
args = arguments
62+
return this
63+
}
64+
65+
@Override
66+
void exec() {
67+
if (outputDex.exists()) {
68+
outputDex.deleteDir()
69+
}
70+
outputDex.mkdirs()
71+
72+
super.exec()
73+
}
74+
}
75+
76+
def optimizedDex = new File(buildDir, "dex-optim/")
77+
def unOptimizedDex = new File(buildDir, "dex-unoptim/")
78+
79+
task runR8(type: RunR8Task, dependsOn: 'jar'){
80+
outputDex = optimizedDex
81+
inputConfig = file('r8-test-rules.pro')
82+
}
83+
84+
task runR8NoOptim(type: RunR8Task, dependsOn: 'jar'){
85+
outputDex = unOptimizedDex
86+
inputConfig = file('r8-test-rules-no-optim.pro')
87+
}
88+
89+
test {
90+
// Ensure the R8-processed dex is built and supply its path as a property to the test.
91+
dependsOn(runR8)
92+
dependsOn(runR8NoOptim)
93+
def dex1 = new File(optimizedDex, "classes.dex")
94+
def dex2 = new File(unOptimizedDex, "classes.dex")
95+
96+
inputs.files(dex1, dex2)
97+
98+
systemProperty 'dexPath', dex1.absolutePath
99+
systemProperty 'noOptimDexPath', dex2.absolutePath
15100
}
16101

17102
tasks.withType(dokka.getClass()) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Entry point for retaining MainDispatcherLoader which uses a ServiceLoader.
2+
-keep class kotlinx.coroutines.Dispatchers {
3+
** getMain();
4+
}
5+
6+
# Entry point for retaining CoroutineExceptionHandlerImpl.handlers which uses a ServiceLoader.
7+
-keep class kotlinx.coroutines.CoroutineExceptionHandlerKt {
8+
void handleCoroutineException(...);
9+
}
10+
11+
# We are cheating a bit by not having android.jar on R8's library classpath. Ignore those warnings.
12+
-ignorewarnings
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-include r8-test-common.pro
2+
3+
# Include the shrinker config used by legacy versions of AGP and ProGuard
4+
-include resources/META-INF/com.android.tools/proguard/coroutines.pro
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-include r8-test-common.pro
2+
3+
# Ensure the custom, fast service loader implementation is removed. In the case of fast service
4+
# loader encountering an exception it falls back to regular ServiceLoader in a way that cannot be
5+
# optimized out by R8.
6+
-include resources/META-INF/com.android.tools/r8-min-1.6.0/coroutines.pro
7+
-checkdiscard class kotlinx.coroutines.internal.FastServiceLoader
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# When editing this file, update the following files as well:
2+
# - META-INF/com.android.tools/r8-max-1.5.999/coroutines.pro
3+
# - META-INF/proguard/coroutines.pro
4+
5+
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# When editing this file, update the following files as well:
2+
# - META-INF/com.android.tools/proguard/coroutines.pro
3+
# - META-INF/proguard/coroutines.pro
4+
5+
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Allow R8 to optimize away the FastServiceLoader.
2+
# Together with ServiceLoader optimization in R8
3+
# this results in direct instantiation when loading Dispatchers.Main
4+
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
5+
boolean FAST_SERVICE_LOADER_ENABLED return false;
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Files in this directory will be ignored starting with Android Gradle Plugin 3.6.0+
2+
3+
# When editing this file, update the following files as well for AGP 3.6.0+:
4+
# - META-INF/com.android.tools/proguard/coroutines.pro
5+
# - META-INF/com.android.tools/r8-max-1.5.999/coroutines.pro
6+
7+
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}

ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay {
4949
public abstract override val immediate: HandlerDispatcher
5050
}
5151

52-
@Keep
5352
internal class AndroidDispatcherFactory : MainDispatcherFactory {
5453

5554
override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.android
6+
7+
import org.jf.dexlib2.*
8+
import org.junit.Test
9+
import java.io.*
10+
import java.util.stream.*
11+
import kotlin.test.*
12+
13+
class R8ServiceLoaderOptimizationTest {
14+
private val r8Dex = File(System.getProperty("dexPath")!!).asDexFile()
15+
private val r8DexNoOptim = File(System.getProperty("noOptimDexPath")!!).asDexFile()
16+
17+
@Test
18+
fun noServiceLoaderCalls() {
19+
val serviceLoaderInvocations = r8Dex.types.any {
20+
it.type == "Ljava/util/ServiceLoader;"
21+
}
22+
assertEquals(
23+
false,
24+
serviceLoaderInvocations,
25+
"References to the ServiceLoader class were found in the resulting DEX."
26+
)
27+
}
28+
29+
@Test
30+
fun androidDispatcherIsKept() {
31+
val hasAndroidDispatcher = r8DexNoOptim.classes.any {
32+
it.type == "Lkotlinx/coroutines/android/AndroidDispatcherFactory;"
33+
}
34+
35+
assertEquals(true, hasAndroidDispatcher)
36+
}
37+
38+
@Test
39+
fun noOptimRulesMatch() {
40+
val paths = listOf(
41+
"META-INF/com.android.tools/proguard/coroutines.pro",
42+
"META-INF/proguard/coroutines.pro",
43+
"META-INF/com.android.tools/r8-max-1.5.999/coroutines.pro"
44+
)
45+
paths.associateWith { path ->
46+
val ruleSet = javaClass.classLoader.getResourceAsStream(path)!!.bufferedReader().lines().filter { line ->
47+
line.isNotBlank() && !line.startsWith("#")
48+
}.collect(Collectors.toSet())
49+
ruleSet
50+
}.asSequence().reduce { acc, entry ->
51+
assertEquals(
52+
acc.value,
53+
entry.value,
54+
"Rule sets between ${acc.key} and ${entry.key} don't match."
55+
)
56+
entry
57+
}
58+
}
59+
}
60+
61+
private fun File.asDexFile() = DexFileFactory.loadDexFile(this, null)

0 commit comments

Comments
 (0)