بسیاری از برنامهها از 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(); } }
اگر هنگام بنچمارکگیری، فراخوانی شبکه را لحاظ میکنید، برای دریافت نتیجه دقیقتر، یک فراخوانی شبکه جعلی پیادهسازی کنید.
گنجاندن یک فراخوانی شبکه واقعی هنگام بنچمارک، تفسیر نتایج بنچمارک را دشوارتر میکند. فراخوانیهای شبکه میتوانند تحت تأثیر عوامل خارجی زیادی قرار گیرند و مدت زمان آنها میتواند بین تکرارهای اجرای بنچمارک متفاوت باشد. مدت زمان فراخوانیهای شبکه میتواند بیشتر از مرتبسازی طول بکشد.
پیادهسازی یک تماس شبکهای جعلی با استفاده از 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); } }
دادهها با استفاده از یک فراخوانی ورودی/خروجی با طول متغیر، از assetها بارگذاری میشوند. با این حال، این کار در طول مقداردهی اولیه انجام میشود و هنگام فراخوانی getPeople() در طول بنچمارک، هیچ بینظمی ایجاد نمیکند.
برخی از برنامهها از قبل از نسخههای جعلی در نسخههای اشکالزدایی (debug builds) برای حذف هرگونه وابستگی به backend استفاده میکنند. با این حال، شما باید تا حد امکان روی نسخهای که به نسخه منتشر شده نزدیک است، بنچمارک بگیرید. بقیه این سند از یک ساختار چند ماژولی و چند متغیره استفاده میکند، همانطور که در بخش «راهاندازی کامل پروژه» توضیح داده شده است.
سه ماژول وجود دارد:
-
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 را به build اعمال میکند.
- مشخص میکند که از اجراکنندهی تست سفارشی برای اجرای تستها استفاده شود.
- مشخص میکند که نوع
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 متوقف میکند. سپس از یک قفل (latch) برای منتظر ماندن تا بهروزرسانی LiveData قبل از اتمام استفاده میکند. از آنجایی که مرتبسازی در فاصله زمانی بین فراخوانی تابع peopleRepository.update() و دریافت بهروزرسانی LiveData انجام میشود، مدت زمان مرتبسازی در زمانبندی بنچمارک لحاظ میشود.
میکرو بنچمارک را اجرا کنید
برای انجام بنچمارک در تکرارهای زیاد و چاپ دادههای زمانبندی در Logcat ، بنچمارک را با استفاده از ./gradlew :benchmark:connectedBenchmarkAndroidTest اجرا کنید:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
مثال قبلی نتیجه معیار را بین ۰.۶ میلیثانیه و ۱.۴ میلیثانیه برای اجرای الگوریتم مرتبسازی روی لیستی از ۱۰۰۰ مورد نشان میدهد. با این حال، اگر فراخوانی شبکه را در معیار لحاظ کنید، واریانس بین تکرارها بیشتر از زمانی است که خود مرتبسازی برای اجرا صرف میکند، از این رو نیاز به جداسازی مرتبسازی از فراخوانی شبکه وجود دارد.
شما همیشه میتوانید کد را اصلاح کنید تا اجرای مرتبسازی به صورت جداگانه آسانتر شود، اما اگر در حال حاضر از Hilt استفاده میکنید، میتوانید به جای آن از آن برای تزریق دادههای جعلی برای بنچمارک استفاده کنید.