بسیاری از برنامه ها از Hilt برای تزریق رفتارهای مختلف به انواع مختلف ساخت استفاده می کنند. این می تواند به ویژه در هنگام Microbenchmarking برنامه شما مفید باشد زیرا به شما امکان می دهد جزیی را تغییر دهید که می تواند نتایج را تغییر دهد. به عنوان مثال، قطعه کد زیر یک مخزن را نشان می دهد که لیستی از نام ها را واکشی و مرتب می کند:
کاتلین
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); } }
داده ها با استفاده از تماس ورودی/خروجی با طول متغیر بالقوه از دارایی ها بارگیری می شوند. با این حال، این کار در حین تنظیم اولیه انجام میشود و هنگام فراخوانی getPeople()
در حین بنچمارک، بینظمی ایجاد نمیکند.
برخی از برنامهها در حال حاضر از تقلبی در ساختهای اشکالزدایی استفاده میکنند تا وابستگیهای 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 لازم را روی بیلد اعمال می کند.
- مشخص می کند که برای اجرای تست ها از تست runner سفارشی استفاده می شود.
- مشخص می کند که نوع
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...
مثال قبلی نتیجه معیار را بین 0.6 میلیثانیه تا 1.4 میلیثانیه نشان میدهد تا الگوریتم مرتبسازی را در فهرستی از 1000 مورد اجرا کند. با این حال، اگر تماس شبکه را در معیار قرار دهید، واریانس بین تکرارها بیشتر از زمانی است که خود مرتبسازی برای اجرا میگذرد، بنابراین نیاز به جداسازی مرتبسازی از تماس شبکه است.
شما همیشه میتوانید کد را اصلاح کنید تا مرتبسازی را بهصورت مجزا اجرا کنید، اما اگر قبلاً از Hilt استفاده میکنید، میتوانید به جای آن از آن برای تزریق تقلبی برای محک زدن استفاده کنید.