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

Комментарии (0)