Всем доброго дня! С вами Анна Жаркова, ведущий мобильный разработчик компании 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