Viele Apps verwenden Hilt, um verschiedenen Build-Varianten unterschiedliches Verhalten zu verleihen. Das kann besonders nützlich sein, wenn Sie Ihre App mithilfe von Mikrobenchmarks testen, da Sie so eine Komponente austauschen können, die die Ergebnisse verfälschen kann. Das folgende Code-Snippet zeigt beispielsweise ein Repository, das eine Liste von Namen abruft und sortiert:
Kotlin
class PeopleRepository @Inject constructor( @Kotlin private val dataSource: NetworkDataSource, @Dispatcher(DispatcherEnum.IO) private val dispatcher: CoroutineDispatcher ) { private val _peopleLiveData = MutableLiveData<List<Person>>() val peopleLiveData: LiveData<List<Person>> get() = _peopleLiveData suspend fun update() { withContext(dispatcher) { _peopleLiveData.postValue( dataSource.getPeople() .sortedWith(compareBy({ it.lastName }, { it.firstName })) ) } } }}
Java
public class PeopleRepository { private final MutableLiveData<List<Person>> peopleLiveData = new MutableLiveData<>(); private final NetworkDataSource dataSource; public LiveData<List<Person>> getPeopleLiveData() { return peopleLiveData; } @Inject public PeopleRepository(NetworkDataSource dataSource) { this.dataSource = dataSource; } private final Comparator<Person> comparator = Comparator.comparing(Person::getLastName) .thenComparing(Person::getFirstName); public void update() { Runnable task = new Runnable() { @Override public void run() { peopleLiveData.postValue( dataSource.getPeople() .stream() .sorted(comparator) .collect(Collectors.toList()) ); } }; new Thread(task).start(); } }
Wenn Sie beim Benchmarking einen Netzwerkaufruf verwenden, implementieren Sie einen fiktiven Netzwerkaufruf, um ein genaueres Ergebnis zu erhalten.
Wenn Sie bei der Benchmarking-Analyse einen echten Netzwerkaufruf verwenden, ist es schwieriger, die Benchmarking-Ergebnisse zu interpretieren. Netzwerkaufrufe können von vielen externen Faktoren beeinflusst werden und ihre Dauer kann zwischen den einzelnen Iterationen des Benchmarks variieren. Die Dauer von Netzwerkaufrufen kann länger dauern als das Sortieren.
Einen gefälschten Netzwerkaufruf mit Hilt implementieren
Der Aufruf von dataSource.getPeople()
enthält, wie im vorherigen Beispiel gezeigt, einen Netzwerkaufruf. Die NetworkDataSource
-Instanz wird jedoch von Hilt eingefügt und Sie können sie für das Benchmarking durch die folgende gefälschte Implementierung ersetzen:
Kotlin
class FakeNetworkDataSource @Inject constructor( private val people: List<Person> ) : NetworkDataSource { override fun getPeople(): List<Person> = people }
Java
public class FakeNetworkDataSource implements NetworkDataSource{ private List<Person> people; @Inject public FakeNetworkDataSource(List<Person> people) { this.people = people; } @Override public List<Person> getPeople() { return people; } }
Dieser gefälschte Netzwerkaufruf soll so schnell wie möglich ausgeführt werden, wenn Sie die Methode getPeople()
aufrufen. Damit Hilt dies einfügen kann, wird der folgende Anbieter verwendet:
Kotlin
@Module @InstallIn(SingletonComponent::class) object FakekNetworkModule { @Provides @Kotlin fun provideNetworkDataSource(@ApplicationContext context: Context): NetworkDataSource { val data = context.assets.open("fakedata.json").use { inputStream -> val bytes = ByteArray(inputStream.available()) inputStream.read(bytes) val gson = Gson() val type: Type = object : TypeToken<List<Person>>() {}.type gson.fromJson<List<Person>>(String(bytes), type) } return FakeNetworkDataSource(data) } }
Java
@Module @InstallIn(SingletonComponent.class) public class FakeNetworkModule { @Provides @Java NetworkDataSource provideNetworkDataSource( @ApplicationContext Context context ) { List<Person> data = new ArrayList<>(); try (InputStream inputStream = context.getAssets().open("fakedata.json")) { int size = inputStream.available(); byte[] bytes = new byte[size]; if (inputStream.read(bytes) == size) { Gson gson = new Gson(); Type type = new TypeToken<ArrayList<Person>>() { }.getType(); data = gson.fromJson(new String(bytes), type); } } catch (IOException e) { // Do something } return new FakeNetworkDataSource(data); } }
Die Daten werden über einen E/A-Aufruf mit potenziell variabler Länge aus Assets geladen.
Dies geschieht jedoch während der Initialisierung und führt zu keinen Unregelmäßigkeiten, wenn getPeople()
während des Benchmarkings aufgerufen wird.
Einige Apps verwenden bereits Fakes in Debug-Builds, um Back-End-Abhängigkeiten zu entfernen. Sie müssen jedoch einen Benchmark mit einem Build durchführen, der dem Release-Build möglichst nahe kommt. Im Rest dieses Dokuments wird eine mehrmodulige, mehrvariantige Struktur verwendet, wie unter Vollständige Projekteinrichtung beschrieben.
Es gibt drei Module:
benchmarkable
: Enthält den Code für den Benchmark.benchmark
: enthält den Benchmark-Code.app
: Enthält den verbleibenden App-Code.
Jedes der vorherigen Module hat neben den üblichen Varianten debug
und release
eine Buildvariante namens benchmark
.
Benchmarkmodul konfigurieren
Der Code für den gefälschten Netzwerkaufruf befindet sich im debug
-Quellsatz des benchmarkable
-Moduls und die vollständige Netzwerkimplementierung im release
-Quellsatz desselben Moduls. Die Asset-Datei mit den von der gefälschten Implementierung zurückgegebenen Daten befindet sich im debug
-Quellsatz, um ein zu großes APK im release
-Build zu vermeiden. Die benchmark
-Variante muss auf release
basieren und das debug
-Quellset verwenden. Die Build-Konfiguration für die benchmark
-Variante des benchmarkable
-Moduls mit der gefälschten Implementierung sieht so aus:
Kotlin
android { ... buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } create("benchmark") { initWith(getByName("release")) } } ... sourceSets { getByName("benchmark") { java.setSrcDirs(listOf("src/debug/java")) assets.setSrcDirs(listOf("src/debug/assets")) } } }
Groovy
android { ... buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' ) } benchmark { initWith release } } ... sourceSets { benchmark { java.setSrcDirs ['src/debug/java'] assets.setSrcDirs(listOf ['src/debug/assets'] } } }
Fügen Sie im benchmark
-Modul einen benutzerdefinierten Test-Runner hinzu, der ein Application
für die Ausführung der Tests erstellt, das Hilt unterstützt. Gehen Sie dazu so vor:
Kotlin
class HiltBenchmarkRunner : AndroidBenchmarkRunner() { override fun newApplication( cl: ClassLoader?, className: String?, context: Context? ): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } }
Java
public class JavaHiltBenchmarkRunner extends AndroidBenchmarkRunner { @Override public Application newApplication( ClassLoader cl, String className, Context context ) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication(cl, HiltTestApplication.class.getName(), context); } }
Dadurch wird das Application
-Objekt, in dem die Tests ausgeführt werden, in die Klasse HiltTestApplication
aufgenommen. Nehmen Sie die folgenden Änderungen an der Buildkonfiguration vor:
Kotlin
plugins { alias(libs.plugins.android.library) alias(libs.plugins.benchmark) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kapt) alias(libs.plugins.hilt) } android { namespace = "com.example.hiltmicrobenchmark.benchmark" compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner = "com.example.hiltbenchmark.HiltBenchmarkRunner" } testBuildType = "benchmark" buildTypes { debug { // Since isDebuggable can't be modified by Gradle for library modules, // it must be done in a manifest. See src/androidTest/AndroidManifest.xml. isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" ) } create("benchmark") { initWith(getByName("debug")) } } } dependencies { androidTestImplementation(libs.bundles.hilt) androidTestImplementation(project(":benchmarkable")) implementation(libs.androidx.runner) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.junit) implementation(libs.androidx.benchmark) implementation(libs.google.dagger.hiltTesting) kaptAndroidTest(libs.google.dagger.hiltCompiler) androidTestAnnotationProcessor(libs.google.dagger.hiltCompiler) }
Cool
plugins { alias libs.plugins.android.library alias libs.plugins.benchmark alias libs.plugins.jetbrains.kotlin.android alias libs.plugins.kapt alias libs.plugins.hilt } android { namespace = 'com.example.hiltmicrobenchmark.benchmark' compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner 'com.example.hiltbenchmark.HiltBenchmarkRunner' } testBuildType "benchmark" buildTypes { debug { // Since isDebuggable can't be modified by Gradle for library modules, // it must be done in a manifest. See src/androidTest/AndroidManifest.xml. minifyEnabled true proguardFiles( getDefaultProguardFile('proguard-android-optimize.txt'), 'benchmark-proguard-rules.pro' ) } benchmark { initWith debug" } } } dependencies { androidTestImplementation libs.bundles.hilt androidTestImplementation project(':benchmarkable') implementation libs.androidx.runner androidTestImplementation libs.androidx.junit androidTestImplementation libs.junit implementation libs.androidx.benchmark implementation libs.google.dagger.hiltTesting kaptAndroidTest libs.google.dagger.hiltCompiler androidTestAnnotationProcessor libs.google.dagger.hiltCompiler }
Das vorherige Beispiel führt Folgendes aus:
- Wendet die erforderlichen Gradle-Plug-ins auf den Build an.
- Gibt an, dass die Tests mit dem benutzerdefinierten Test-Runner ausgeführt werden.
- Gibt an, dass die
benchmark
-Variante der Testtyp für dieses Modul ist. - Fügt die Variante
benchmark
hinzu. - Fügen Sie die erforderlichen Abhängigkeiten hinzu.
Sie müssen testBuildType
ändern, damit Gradle die Aufgabe connectedBenchmarkAndroidTest
erstellt, die die Benchmarking-Tests durchführt.
MicroBenchmark erstellen
Die Benchmark wird so implementiert:
Kotlin
@RunWith(AndroidJUnit4::class) @HiltAndroidTest class PeopleRepositoryBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() @get:Rule val hiltRule = HiltAndroidRule(this) private val latch = CountdownLatch(1) @Inject lateinit var peopleRepository: PeopleRepository @Before fun setup() { hiltRule.inject() } @Test fun benchmarkSort() { benchmarkRule.measureRepeated { runBlocking { benchmarkRule.getStart().pauseTiming() withContext(Dispatchers.Main.immediate) { peopleRepository.peopleLiveData.observeForever(observer) } benchmarkRule.getStart().resumeTiming() peopleRepository.update() latch.await() assert(peopleRepository.peopleLiveData.value?.isNotEmpty() ?: false) } } } private val observer: Observer<List<Person>> = object : Observer<List<Person>> { override fun onChanged(people: List<Person>?) { peopleRepository.peopleLiveData.removeObserver(this) latch.countDown() } } }
Java
@RunWith(AndroidJUnit4.class) @HiltAndroidTest public class PeopleRepositoryBenchmark { @Rule public BenchmarkRule benchmarkRule = new BenchmarkRule(); @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this); private CountdownLatch latch = new CountdownLatch(1); @Inject JavaPeopleRepository peopleRepository; @Before public void setup() { hiltRule.inject(); } @Test public void benchmarkSort() { BenchmarkRuleKt.measureRepeated(benchmarkRule, (Function1<BenchmarkRule.Scope, Unit>) scope -> { benchmarkRule.getState().pauseTiming(); new Handler(Looper.getMainLooper()).post(() -> { awaitValue(peopleRepository.getPeopleLiveData()); }); benchmarkRule.getState().resumeTiming(); peopleRepository.update(); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } assert (!peopleRepository.getPeopleLiveData().getValue().isEmpty()); return Unit.INSTANCE; }); } private <T> void awaitValue(LiveData<T> liveData) { Observer<T> observer = new Observer<T>() { @Override public void onChanged(T t) { liveData.removeObserver(this); latch.countDown(); } }; liveData.observeForever(observer); return; } }
Im vorherigen Beispiel werden Regeln sowohl für den Benchmark als auch für Hilt erstellt.
benchmarkRule
führt die Zeitmessung der Benchmark durch. hiltRule
führt die Abhängigkeitsinjektion in der Benchmark-Testklasse durch. Sie müssen die Methode inject()
der Hilt-Regel in einer @Before
-Funktion aufrufen, um die Injektion durchzuführen, bevor Sie einzelne Tests ausführen.
Der Benchmark selbst hält die Zeitmessung an, während der LiveData
-Beobachter registriert wird. Dann wird mit einem Latch gewartet, bis die LiveData
aktualisiert wurde, bevor der Vorgang abgeschlossen wird. Da die Sortierung zwischen dem Aufruf von peopleRepository.update()
und dem Empfang einer Aktualisierung von LiveData
ausgeführt wird, ist die Dauer der Sortierung im Benchmark-Timing enthalten.
Microbenchmark ausführen
Führen Sie den Benchmark mit ./gradlew :benchmark:connectedBenchmarkAndroidTest
aus, um den Benchmark über viele Iterationen hinweg auszuführen und die Zeitdaten in Logcat zu drucken:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
Im vorherigen Beispiel wird das Benchmark-Ergebnis zwischen 0,6 ms und 1,4 ms für die Ausführung des Sortieralgorithmus auf einer Liste mit 1.000 Elementen angezeigt. Wenn Sie den Netzwerkaufruf jedoch in den Benchmark einbeziehen, ist die Abweichung zwischen den Iterationen größer als die Zeit, die für die Sortierung selbst benötigt wird. Daher muss die Sortierung vom Netzwerkaufruf getrennt werden.
Sie können den Code jederzeit umstrukturieren, um die Sortierung einfacher in Isolation auszuführen. Wenn Sie Hilt bereits verwenden, können Sie stattdessen damit Fakes für das Benchmarking einschleusen.