אפליקציות רבות משתמשות ב-Hilt כדי להחדיר התנהגויות שונות לווריאציות שונות של build. התכונה הזו יכולה להיות שימושית במיוחד לביצוע מיקרובנצ'מרקים לאפליקציה, כי היא מאפשרת להחליף רכיב שיכול להטות את התוצאות. לדוגמה, בקטע הקוד מוצג מאגר שמאחזר וממיין רשימת שמות:
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()
, כמו שמוצג בדוגמה שלמעלה,
מכיל שיחת רשת. עם זאת, למכונה NetworkDataSource
תוחדר.
של Hilt, ואפשר להחליף אותו בהטמעה המזויפת הבאה
בנצ'מרק:
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()
. כדי ש-Hhilt יוכל להחדיר את הפונקציה הזו,
נעשה שימוש בספק:
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()
במהלך ההשוואה לשוק.
אפליקציות מסוימות כבר משתמשות בזיופים בגרסאות build של ניפוי באגים כדי להסיר יחסי תלות של הקצה העורפי. עם זאת, צריך להגדיר נקודת השוואה ל-build שקרוב מאוד ל-build של הגרסה ככל האפשר. שאר המסמך משתמש במבנה רב-מודולים ורב-משתנים. כפי שמתואר במאמר הגדרת פרויקט מלאה.
יש שלושה מודולים:
benchmarkable
: מכיל את הקוד להשוואה.benchmark
: מכיל את קוד ההשוואה לשוק.app
: מכיל את קוד האפליקציה שנותר.
בכל אחד מהמודולים הקודמים יש וריאנט build בשם benchmark
, לצד
הווריאציות הרגילות debug
ו-release
.
הגדרת מודול ההשוואה לשוק
הקוד לשיחת הרשת המזויפת נמצא בקבוצת המקור debug
של
המודול benchmarkable
, והטמעת הרשת המלאה נמצאת בrelease
קבוצת המקור של אותו המודול. קובץ הנכס שמכיל את הנתונים שהוחזרו על ידי
ההטמעה המזויפת נמצאת במקור debug
שהוגדר כדי למנוע עומס יתר של APK
את ה-build של release
. הווריאנט benchmark
צריך להתבסס על release
ועל
משתמשים בקבוצת המקור debug
. תצורת ה-build של הווריאנט benchmark
של המודול benchmarkable
שמכיל את ההטמעה המזויפת:
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")) } } }
מגניב
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
מוסיפים תוכנת הרצת בדיקה בהתאמה אישית שיוצרת Application
שהבדיקות ירוצו שתומכות ב-Hilt, באופן הבא:
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
. צריך לבצע את השינויים הבאים ב-build
תצורה:
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) }
מגניב
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 }
הדוגמה שלמעלה מבצעת את הדברים הבאים:
- מחילה על ה-build את יישומי הפלאגין המתאימים לשדרוג.
- מציינת שהרצת הבדיקות בהתאמה אישית משמשת להרצת הבדיקות.
- מציינת שווריאנט
benchmark
הוא סוג הבדיקה של המודול הזה. - הוספת הווריאנט
benchmark
. - מחברת את יחסי התלות הנדרשים.
צריך לשנות את testBuildType
כדי להבטיח ש-Gradle ייצור את
משימה אחת (connectedBenchmarkAndroidTest
), שמבצעת את ההשוואה לשוק.
יצירת המיקרו-בנצ'מרק
ההשוואה לשוק מיושמת באופן הבא:
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
מבצע את
החדרת תלות במחלקה של מבחן ההשוואה לשוק. צריך להפעיל את
ה-method inject()
של כלל Hilt בפונקציה @Before
כדי לבצע את הפונקציה
הזרקה לפני הרצת בדיקות ספציפיות.
נקודת ההשוואה עצמה תשהה את התזמון בזמן שהצופה 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 פריטים. אבל אם כוללים את אזי בנצ'מרק, אז השונות בין האיטרציות היא גדולה יותר מאשר הזמן שלוקח למיון עצמו לפעול, לכן צריך לבודד המיון של שיחת הרשת.
תמיד תוכלו לשנות את ארגון הקוד (Refactoring) כדי שיהיה קל יותר לבצע מיון אבל אם אתם כבר משתמשים ב-Hilt, תוכלו להשתמש בו כדי להזריק זיופים השוואה לשוק.