بسیاری از برنامهها از Hilt برای تزریق رفتارهای مختلف به تنوعهای ساخت مختلف استفاده میکنند. این کار بهویژه هنگام «ریز محکزنی» برنامهتان میتواند مفید باشد زیرا به شما امکان میدهد مؤلفهای را که میتواند نتایج را منحرف کند تغییر دهید. برای مثال، تکهکد زیر مخزنی را نشان میدهد که فهرستی از نامها را واکشی و مرتب میکند:
کاتلین
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 })) ) } } }}
جاوا
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(); } }
اگر هنگام محکزنی، تماس شبکهای را لحاظ میکنید، تماس شبکهای جعلی را پیادهسازی کنید تا نتیجه دقیقتری دریافت کنید.
گنجاندن تماس شبکه واقعی هنگام محکزنی، تفسیر نتایج محک را دشوارتر میکند. فراخوانیهای شبکه میتوانند تحت تأثیر عوامل خارجی زیادی قرار گیرند و مدتزمان آنها میتواند بین تکرارهای اجرای محک متفاوت باشد. مدت تماسهای شبکه میتواند بیشتر از مرتبسازی طول بکشد.
Implement a fake network call using Hilt
فراخوانی dataSource.getPeople()، همانطور که در مثال قبلی نشان داده شده است،
شامل فراخوانی شبکه است. بااینحال، نمونه NetworkDataSource توسط Hilt تزریق میشود و میتوانید آن را با پیادهسازی جعلی زیر برای محکزنی جایگزین کنید:
کاتلین
class FakeNetworkDataSource @Inject constructor( private val people: List<Person> ) : NetworkDataSource { override fun getPeople(): List<Person> = people }
جاوا
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 بتواند این را تزریق کند، از ارائهدهنده زیر
استفاده میشود:
کاتلین
@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) } }
جاوا
@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 حاوی پیادهسازی جعلی بهصورت زیر است:
کاتلین
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 بهصورت زیر پشتیبانی کند:
کاتلین
class HiltBenchmarkRunner : AndroidBenchmarkRunner() { override fun newApplication( cl: ClassLoader?, className: String?, context: Context? ): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } }
جاوا
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 را گسترش دهد. تغییرات زیر را در پیکربندی ساخت
ایجاد کنید:
کاتلین
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 را ایجاد میکند که محکزنی را انجام میدهد.
ایجاد ریز محک
محکزنی بهصورت زیر اجرا میشود:
کاتلین
@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() } } }
جاوا
@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...
مثال قبلی نتیجه محک را بین ۰٫۶ میلیثانیه و ۱٫۴ میلیثانیه برای اجرای الگوریتم مرتبسازی در فهرستی از ۱۰۰۰ مورد نشان میدهد. اما اگر فراخوانی شبکه را در محک بگنجانید، واریانس بین تکرارها بیشتر از زمانی است که خود مرتبسازی برای اجرا نیاز دارد، بنابراین لازم است مرتبسازی را از فراخوانی شبکه جدا کنید.
همیشه میتوانید کد را بازسازی کنید تا اجرای مرتبسازی بهصورت مجزا آسانتر شود، اما اگر ازقبل از Hilt استفاده میکنید، میتوانید از آن برای تزریق دادههای جعلی برای محکزنی استفاده کنید.