Viele Apps verwenden Hilt, um unterschiedliche Verhaltensweisen in verschiedene Build-Varianten einzufügen. Das kann besonders nützlich sein, wenn Sie Microbenchmarking für Ihre App durchführen, da Sie so eine Komponente austauschen können, die die Ergebnisse verfälschen könnte. Im folgenden Codebeispiel wird ein Repository gezeigt, 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 einfügen, implementieren Sie einen gefälschten Netzwerkaufruf, um ein genaueres Ergebnis zu erhalten.
Wenn beim Benchmarking ein echter Netzwerkaufruf enthalten ist, sind die Benchmark-Ergebnisse schwieriger zu interpretieren. Netzwerkaufrufe können durch viele externe Faktoren beeinflusst werden und ihre Dauer kann zwischen den einzelnen Benchmark-Ausführungen variieren. Netzwerkaufrufe können länger dauern als das Sortieren.
Fake-Netzwerkaufruf mit Hilt implementieren
Der Aufruf von dataSource.getPeople()
im vorherigen Beispiel enthält einen Netzwerkaufruf. Die NetworkDataSource
-Instanz wird jedoch von Hilt eingefügt und kann für Benchmarking durch die folgende gefälschte Implementierung ersetzt werden:
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 ist so konzipiert, dass er so schnell wie möglich ausgeführt wird, wenn Sie die Methode getPeople()
aufrufen. Damit Hilt diese Abhängigkeit 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 aus Assets mit einem E/A-Aufruf mit variabler Länge geladen.
Dies geschieht jedoch während der Initialisierung und führt zu keinen Unregelmäßigkeiten, wenn getPeople()
während des Benchmarking aufgerufen wird.
Einige Apps verwenden bereits Fakes in Debug-Builds, um alle Backend-Abhängigkeiten zu entfernen. Sie sollten jedoch Benchmarks für einen Build durchführen, der dem Release-Build so ähnlich wie möglich ist. Im Rest dieses Dokuments wird eine Struktur mit mehreren Modulen und Varianten verwendet, wie unter Vollständige Projekteinrichtung beschrieben.
Es gibt drei Module:
benchmarkable
: Enthält den Code, der als Benchmark verwendet werden soll.benchmark
: Enthält den Benchmark-Code.app
: Enthält den restlichen App-Code.
Jedes der vorherigen Module hat eine Build-Variante namens benchmark
sowie die üblichen Varianten debug
und release
.
Benchmarkmodul konfigurieren
Der Code für den gefälschten Netzwerkaufruf befindet sich im Quellset debug
des Moduls benchmarkable
und die vollständige Netzwerkimplementierung im Quellset release
desselben Moduls. Die Asset-Datei mit den von der gefälschten Implementierung zurückgegebenen Daten befindet sich im Quellsatz debug
, um eine APK-Aufblähung im release
-Build zu vermeiden. Die benchmark
-Variante muss auf release
basieren und das Quellset debug
verwenden. Die Build-Konfiguration für die benchmark
-Variante des benchmarkable
-Moduls mit der Fake-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 Tests erstellt, das Hilt unterstützt:
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, auf die Klasse HiltTestApplication
erweitert. Nehmen Sie die folgenden Änderungen an der Build-Konfiguration 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) }
Groovy
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 }
Im vorherigen Beispiel wird Folgendes ausgeführt:
- Wendet die erforderlichen Gradle-Plug-ins auf den Build an.
- Gibt an, dass der benutzerdefinierte Test-Runner zum Ausführen der Tests verwendet wird.
- Gibt an, dass die
benchmark
-Variante der Testtyp für dieses Modul ist. - Fügt die Variante
benchmark
hinzu. - Fügt die erforderlichen Abhängigkeiten hinzu.
Sie müssen testBuildType
ändern, damit Gradle die Aufgabe connectedBenchmarkAndroidTest
erstellt, mit der das Benchmarking durchgeführt wird.
Microbenchmark erstellen
Der 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 für Benchmark und Hilt erstellt.
benchmarkRule
führt die Zeitmessung des Benchmarks durch. hiltRule
führt die Abhängigkeitsinjektion für die Benchmark-Testklasse aus. Sie müssen die Methode inject()
der Hilt-Regel in einer @Before
-Funktion aufrufen, um die Injektion vor dem Ausführen einzelner Tests durchzuführen.
Der Benchmark selbst pausiert das Timing, während der LiveData
-Observer registriert ist. Anschließend wird ein Latch verwendet, um zu warten, bis LiveData
aktualisiert wurde, bevor der Vorgang abgeschlossen wird. Da die Sortierung in der Zeit zwischen dem Aufruf von peopleRepository.update()
und dem Zeitpunkt erfolgt, zu dem LiveData
ein Update erhält, wird die Dauer der Sortierung in die Benchmark-Zeitmessung einbezogen.
Microbenchmark ausführen
Führen Sie den Benchmark mit ./gradlew :benchmark:connectedBenchmarkAndroidTest
aus, um ihn in vielen Iterationen auszuführen und die Zeitmessungsdaten in Logcat auszugeben:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
Im vorherigen Beispiel liegt das Benchmark-Ergebnis zwischen 0,6 ms und 1,4 ms für die Ausführung des Sortieralgorithmus für eine Liste mit 1.000 Elementen. Wenn Sie den Netzwerkaufruf jedoch in den Benchmark einbeziehen, ist die Varianz zwischen den Iterationen größer als die Zeit, die für das Sortieren selbst benötigt wird. Daher ist es erforderlich, das Sortieren vom Netzwerkaufruf zu isolieren.
Sie können Code jederzeit umgestalten, um das Sortieren isoliert auszuführen. Wenn Sie jedoch bereits Hilt verwenden, können Sie damit stattdessen Fakes für Benchmarking einfügen.