Kotlin Native. Работаем с новой моделью памяти

—

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/578716/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *