تستخدم العديد من التطبيقات 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 مللي ثانية لتشغيل خوارزمية الفرز في قائمة مكونة من 1000 عنصر. ومع ذلك، إذا قمت بتضمين استدعاء الشبكة في المعيار، فإن التباين بين التكرارات يكون أكبر من الوقت الذي يستغرقه الفرز نفسه للتشغيل، ومن ثم الحاجة إلى عزل الفرز من استدعاء الشبكة.
يمكنك دائمًا إعادة هيكلة الرمز لتسهيل تشغيل عملية الترتيب بشكل منفصل، ولكن إذا كنت تستخدم Hilt، يمكنك استخدامه لإدخال بيانات مزيفة لقياس الأداء بدلاً من ذلك.