許多應用程式會使用 Hilt 為不同的建構變數注入不同的行為。在應用程式對應用程式進行微型基準測試時,這項功能特別實用,因為這可讓您切換可能會扭曲結果的元件。舉例來說,下列程式碼片段顯示可擷取及排序名稱清單的存放區:
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(); } }
如果在基準測試時加入網路呼叫,請實作假網路呼叫,以便取得更準確的結果。
在基準測試時加入實際網路呼叫,會讓基準測試結果難以解讀。網路呼叫可能會受到許多外部因素的影響,執行時間則可能因執行基準測試的疊代而異。網路呼叫的時間可能比排序時間長。
使用 Hilt 實作假網路呼叫
如上例所示,對 dataSource.getPeople()
的呼叫包含網路呼叫。不過,Hilt 會插入 NetworkDataSource
例項,您可以將其替換為下列假設實作項目,以便進行基準測試:
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; } }
這項假網路呼叫的設計目的,是在您呼叫 getPeople()
方法時,盡可能快速執行。為了讓 Hilt 能夠插入此內容,我們使用了以下提供者:
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); } }
系統會使用可能可變長度的 I/O 呼叫從資產載入資料。不過,這項作業會在初始化期間完成,因此在基準測試期間呼叫 getPeople()
時不會造成任何異常。
部分應用程式已在偵錯版本中使用假資料,以移除任何後端依附元件。不過,您必須盡可能在接近發布版本的版本上進行基準測試。本文件的其餘部分會使用多模組、多變化版本的結構,如完整專案設定所述。
共有三個模組:
benchmarkable
:包含要基準測試的程式碼。benchmark
:包含基準測試程式碼。app
:包含其餘的應用程式程式碼。
上述每個模組都有名為 benchmark
的建構變化版本,以及一般 debug
和 release
變化版本。
設定基準測試模組
假網路呼叫的程式碼位於 benchmarkable
模組的 debug
來源集,而完整網路實作則位於同一個模組的 release
來源集。含有假實作項目傳回資料的素材資源檔案位於 debug
來源集,可避免 release
版本中的 APK 膨脹。benchmark
變化版本必須以 release
為基礎,並使用 debug
來源集。benchmarkable
模組 (包含假實作) 的 benchmark
變化版本建構設定如下:
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'] } } }
在 benchmark
模組中新增自訂測試執行程式,為支援 Hilt 的測試建立 Application
,如下所示:
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); } }
這麼做會讓執行測試的 Application
物件擴充 HiltTestApplication
類別。對建構設定進行下列變更:
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 }
上述範例會執行以下操作:
- 將必要的 Gradle 外掛程式套用至建構作業。
- 指定使用自訂測試執行工具來執行測試。
- 指定
benchmark
變體為此模組的測試類型。 - 新增
benchmark
變化版本。 - 新增必要的依附元件。
您需要變更 testBuildType
,確保 Gradle 會建立執行基準測試的 connectedBenchmarkAndroidTest
工作。
建立 Microbenchmark
基準測試的實作方式如下:
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; } }
上述範例會為基準測試和 Hilt 建立規則。benchmarkRule
會執行基準測試的時間。hiltRule
會在基準測試類別上執行依附元件插入作業。您必須在 @Before
函式中叫用 Hilt 規則的 inject()
方法,以便在執行任何個別測試之前執行插入作業。
基準測試本身會在註冊 LiveData
觀察器時暫停計時。接著,它會使用門栓等待 LiveData
更新完成後再結束。由於排序作業是在呼叫 peopleRepository.update()
與 LiveData
收到更新之間的時間執行,因此排序時間長度會納入基準時間中。
執行微基準測試
使用 ./gradlew :benchmark:connectedBenchmarkAndroidTest
執行基準測試,以便在多次疊代中執行基準測試,並將時間資料輸出到 Logcat:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
上例顯示在 0.6 毫秒至 1.4 毫秒之間的基準測試結果,以便在包含 1,000 個項目的清單上執行排序演算法。不過,如果基準測試中包含網路呼叫,則各個迴迭之間的差異會大於排序本身執行所需的時間,因此需要將排序與網路呼叫分開。
您隨時可以重構程式碼,讓系統更容易執行排序作業,但如果您已使用 Hilt,可以改為使用 Hilt 注入假資料來進行基準測試。