تستخدم العديد من التطبيقات 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()، كما هو موضّح في المثال السابق، على طلب على الشبكة. ومع ذلك، يتم إدخال مثيل 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(). لكي يتمكّن 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); } }
يتم تحميل البيانات من مواد العرض باستخدام طلب إدخال/إخراج قد يكون له طول متغيّر.
ومع ذلك، يتم إجراء ذلك أثناء الإعداد ولن يؤدي إلى أي مخالفات عند استدعاء getPeople() أثناء قياس الأداء.
تستخدم بعض التطبيقات بالفعل بيانات وهمية في إصدارات تصحيح الأخطاء لإزالة أي تبعيات على الخلفية. ومع ذلك، عليك إجراء قياس الأداء على بنية أقرب ما يمكن إلى بنية الإصدار. يستخدم باقي هذا المستند بنية متعددة الوحدات ومتعددة التصاميم المتنوعة كما هو موضّح في إعداد المشروع الكامل.
هناك ثلاث وحدات:
benchmarkable: تحتوي على الرمز البرمجي لقياس الأداء.benchmark: تحتوي على رمز قياس الأداء.app: تحتوي على رمز التطبيق المتبقي.
تحتوي كل وحدة من الوحدات السابقة على تصميم متنوع باسم benchmark بالإضافة إلى التصميمَين المتنوعَين المعتادَين debug وrelease.
ضبط وحدة قياس الأداء
يوجد رمز الطلب الوهمي على الشبكة في مجموعة رموز المصدر debug التابعة لوحدة benchmarkable، ويوجد تنفيذ الشبكة الكامل في مجموعة رموز المصدر release التابعة للوحدة نفسها. يوجد ملف مواد العرض الذي يحتوي على البيانات التي يعرضها التنفيذ الوهمي في مجموعة رموز المصدر debug لتجنُّب أي تضخّم في ملف APK في إصدار release. يجب أن يستند التصميم المتنوع benchmark إلى release وأن يستخدم مجموعة رموز المصدر debug. في ما يلي إعدادات التصميم للتصميم المتنوع 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. أجرِ التغييرات التالية على إعداد الإصدار:
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 }
يفعل المثال السابق ما يلي:
- يطبّق مكوّنات Gradle الإضافية الضرورية على الإصدار.
- يحدّد أنّه يتم استخدام برنامج تشغيل الاختبارات المخصّص لتشغيل الاختبارات.
- يحدّد أنّ التصميم المتنوع
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 عملية إدخال التبعية في فئة اختبار قياس الأداء. عليك استدعاء طريقة
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 عنصر. ومع ذلك، إذا تضمّن قياس الأداء الطلب على الشبكة، سيكون التباين بين التكرارات أكبر من الوقت الذي يستغرقه الفرز نفسه، لذا يجب عزل الفرز عن الطلب على الشبكة.
يمكنك دائمًا إعادة تصميم الرمز البرمجي لتسهيل تشغيل الفرز بشكل منفصل، ولكن إذا كنت تستخدم Hilt، يمكنك استخدامه لإدخال بيانات وهمية لقياس الأداء بدلاً من ذلك.