
Всем доброго дня! С вами Анна Жаркова, ведущий мобильный разработчик компании Usetech. Продолжаем рассматривать способы многопоточный работы в Kotlin Native.
В предыдущих статьях мы уже рассмотрели существующие способы работы с многопоточностью с корутинами и без, и что делать с имеющимися болями. Теперь поговорим о новой модели управления памятью, которая появилась совсем недавно.
31 августа компания JetBrains представили превью новой модели управления памятью в Kotlin Native. Основной упор команда разработчиков сделала на безопасность шаринга между потоками, устранение утечек памяти и освобождение нас от использования специальных аннотаций. Также доработка коснулась Coroutines, и теперь можно без опаски переключаться между контекстами корутин без заморозки. Обновления подхватили и Ktor:
Итак, что же нового появится в версии Kotlin 1.6.0-M1-139:
1. Заявлено, что мы можем убрать все freeze() блоки (в том числе и во всех фоновых Worker), и переключаться между контекстами и потоками без каких-либо проблем.
2.Использование AtomicReference или FreezableAtomicReference не приводит к утечкам памяти.
3.При работе с глобальными константами не нужно теперь использовать SharedImmutable.
4.При работе с Worker.execute producer больше не требует возвращать изолированный подграф объектов.
Однако есть и нюансы:
1. Необходимо оставлять заморозку при работе с AtomicReference. В качестве альтернативы мы можем использовать FreezableAtomicReference или AtomicRef из atomicfu . Однако, нас предупреждают, что atomicfu еще не достигла версии 1.х.
2.При вызове suspend функции Kotlin в Swift ее completion handler блок может не прийти в main thread. Т.е добавляем DispatchQueue.main.async{...}, если нам нужно.
3.deInit Swift/ObjC объктов может вызываться в другом потоке.
4.Глобальные свойства инициализируются лениво, т.е при первом обращении. Ранее глобальные свойства инициализировались при запуске. Если вам нужно поддерживать это поведение, то добавляем теперь аннотацию @'EagerInitialization. Рекомендовано ознакомиться с документацией перед использованием.
Нюансы есть и в работе с корутинами, в версии поддерживающей новую модель управления памятью:
1.Мы можем работать в Worker с Channel и Flow без заморозки. И в отличии от native-mt версии заморозка, например, канала заморозить все его содержимое, что может не ожидаться.
2.Dispatchers.Default теперь поддерживается global queue.
3.newSingleThreadContext и newFixedThreadPoolContext теперь можно использовать для создания диспетчера корутин с поддержкой пула одного или нескольких разработчиков.
4.Dispatchers.Main связан с main queue для Darwin и отдельным Worker для других платформ Native. Поэтому рекомендовано не использовать его для работы с Unit тестами, так как ничего не будет вызвано в очереди главного потока.
Нюансов много, есть определенные проблемы с перформансом и известные баги, о которых команда разработки написала предупредительно в документации. Но это пока превью (даже не альфа).
Что ж, давайте попробуем настроить наше решение из предыдущих статей под новую версию модели управления памяти.
Для установки версии 1.6.0-M1-139 добавим некоторые настройки:
// build.gradle.kts buildscript { repositories { gradlePluginPortal() google() mavenCentral() maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/temporary") } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${findProperty("version.kotlin")}") classpath("org.jetbrains.kotlin:kotlin-serialization:${findProperty("version.kotlin")}") classpath("com.android.tools.build:gradle:${findProperty("version.androidGradlePlugin")}") } } // settings.gradle.kts pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") } maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-coroutines/maven") } } } }
//gradle.properties kotlin.native.binary.memoryModel=experimental #kotlin.native.binary.freezing=disabled #Common versions version.kotlin=1.6.0-M1-139 version.androidGradlePlugin=7.0.0 version.kotlinx.serialization=1.2.2 version.kotlinx.coroutines=1.5.1-new-mm-dev2
И разумеется, добавим зависимость для корутин:
//version.kotlinx.coroutines=1.5.1-new-mm-dev2 val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${findProperty("version.kotlinx.coroutines")}") } }
Важно! Если у вас не установлен Xcode версии 12.5 или выше, обязательно скачайте и поставьте. Это минимальная совместимая версия с 1.6.0-M1-139. Если у вас установлено уже несколько версий Xcode, в том числе и более низкие, то поменяйте на подходящую с помощью xcode-select, закройте проект Kotlin Multiplatform и запустите Invalidate cache and Restart. Иначе получите ошибку о несовместимости версии.
Начнем с уборки freeze() блоков из бескорутиновой версии:
//Worker internal fun background(block: () -> Unit) { val future = worker.execute(TransferMode.SAFE, { block}) { it() } collectFutures.add(future) } //Main wrapper internal fun main(block:()->Unit) { dispatch_async(dispatch_get_main_queue()) { block() } }
Также уберем заморозку с параметров, которые мы используем для UrlSession (у нас нативный сетевой клиент):
fun request(request: Request, completion: (Response) -> Unit) { this.completion = completion val responseReader = ResponseReader().apply { this.responseListener = this@HttpEngine } val urlSession = NSURLSession.sessionWithConfiguration( NSURLSessionConfiguration.defaultSessionConfiguration, responseReader, delegateQueue = NSOperationQueue.currentQueue() ) val urlRequest = NSMutableURLRequest(NSURL.URLWithString(request.url)!!).apply { setAllHTTPHeaderFields(request.headers) setHTTPMethod(request.method.value) setCachePolicy(NSURLRequestReloadIgnoringCacheData) } fun doRequest() { val task = urlSession.dataTaskWithRequest(urlRequest) task?.resume() } background{ doRequest() } }
Для полного избавления от заморозок меняем AtomicReference на FreezableAtomicReference:
/* internal fun <T> T.atomic(): AtomicReference<T>{ return AtomicReference(this.share()) }*/ internal fun <T> T.atomic(): FreezableAtomicReference<T>{ return FreezableAtomicReference(this) }
И подправляем код, где мы атомарные ссылки используем:
private fun updateChunks(data: NSData) { var newValue = ByteArray(0) newValue += chunks.value newValue += data.toByteArray() chunks.value = newValue//.share() }
Код так и дышит чистотой и просто летает, несмотря на то, что GC (в котором могут быть боли) у нас не поменялся.
Теперь подтюнингуем пример с корутинами:
val uiDispatcher: CoroutineContext = Dispatchers.Main val ioDispatcher: CoroutineContext = Dispatchers.Default
Для начала мы воспользуемся диспетчерами по умолчанию. Чтобы проверить магию GlobalQueue, выведем данные о контексте в блоке под управлением ioDispatcher:
//output StandaloneCoroutine{Active}@26dbcd0, DarwinGlobalQueueDispatcher@28ea470
Убираем заморозки при работе с Flow и/или Channel:
class FlowResponseReader : NSObject(), NSURLSessionDataDelegateProtocol { private var chunksFlow = MutableStateFlow(ByteArray(0)) private var rawResponse = CompletableDeferred<Response>() suspend fun awaitResponse(): Response { var chunks = ByteArray(0) chunksFlow.onEach { chunks += it }.launchIn(scope) val response = rawResponse.await() response.content = chunks.string() return response } /***/ private fun updateChunks(data: NSData) { val bytes = data.toByteArray() chunksFlow.tryEmit(bytes) } }
Все работает, отлично и быстро. Не забываем вынести ответ в очередь main thread:
actual override suspend fun request(request: Request):Response { val response = engine.request(request) return withContext(uiDispatcher){response} }
Важно! Для предотвращения утечек на стороне iOS, особенно в случае большого количества различных объектов Swift/ObjC, и вспоможения GC оборачиваем блоки вызова и работы с ответом в autoreleasepool.
Теперь попробуем следующее. Запустим на MainScope, но с помощью newSingleThreadContext укажем другой фоновый диспетчер:
val task = urlSession.dataTaskWithRequest(urlRequest) mainScope.launch(newSingleThreadContext("MyOwnThread")) { print("${this.coroutineContext}") task?.resume() } //output [StandaloneCoroutine{Active}@384d2a0, WorkerDispatcher@384d630]
Все отрабатывает без запинок. С наших разработческих плеч совсем скоро свалится гора забот.
Но остается жирное "НО". Не все библиотеки, которые мы используем в приложениях KMM, готовы к новой модели памяти, новому подходу к заморозкам и передаче между контекстами. Мы можем получить исключение InvalidMutabilityException или FreezingException.
Поэтому для них в приложениях с версией 1.6.0-M1-139 придется отключить встроенную заморозку:
//gradle.properties kotlin.native.binary.freezing=disabled //либо build.gradle.kts kotlin.targets.withType(KotlinNativeTarget::class.java) { binaries.all { binaryOptions["freezing"] = "disabled" } }
Более подробно о новой версии модели управления памятью смотрите здесь: https://github.com/JetBrains/kotlin/blob/master/kotlin-native/NEW_MM.md
И сэмпл на коленке:
https://github.com/anioutkazharkova/kotlin_native_network_client/tree/feature/1.6-kn/sample
