Im vorherigen Artikel habe ich über eine Möglichkeit gesprochen, Multithreading in einer Kotlin Multiplatform-Anwendung zu implementieren. Heute werden wir eine alternative Situation in Betracht ziehen, wenn wir eine Anwendung mit dem am häufigsten verwendeten gemeinsamen Code implementieren und alle Arbeiten mit Threads in eine gemeinsame Logik übertragen.
Im vorherigen Beispiel wurde uns von der Ktor-Bibliothek geholfen, die alle Hauptaufgaben der Bereitstellung von Asynchronität im Netzwerkclient übernahm. Dies ersparte uns in diesem speziellen Fall die Verwendung von DispatchQueue unter iOS, in anderen Fällen mussten wir jedoch einen Run Queue-Job verwenden, um die Geschäftslogik aufzurufen und die Antwort zu verarbeiten. Auf der Android-Seite haben wir MainScope verwendet, um eine angehaltene Funktion aufzurufen.
Wenn wir also eine einheitliche Arbeit mit Multithreading in einem gemeinsamen Projekt implementieren möchten, müssen wir den Umfang und den Kontext der Coroutine, in der sie ausgeführt wird, korrekt konfigurieren.
Fangen wir einfach an. Erstellen wir unseren Architekturmediator, der die Servicemethoden in ihrem Umfang aufruft, die aus dem Coroutine-Kontext stammen:
class PresenterCoroutineScope(context: CoroutineContext) : CoroutineScope {
private var onViewDetachJob = Job()
override val coroutineContext: CoroutineContext = context + onViewDetachJob
fun viewDetached() {
onViewDetachJob.cancel()
}
}
//
abstract class BasePresenter(private val coroutineContext: CoroutineContext) {
protected var view: T? = null
protected lateinit var scope: PresenterCoroutineScope
fun attachView(view: T) {
scope = PresenterCoroutineScope(coroutineContext)
this.view = view
onViewAttached(view)
}
}
Wir rufen den Service in der Methode des Mediators auf und übergeben ihn an unsere Benutzeroberfläche:
class MoviesPresenter:BasePresenter(defaultDispatcher){
var view: IMoviesListView? = null
fun loadData() {
//
scope.launch {
service.getMoviesList{
val result = it
if (result.errorResponse == null) {
data = arrayListOf()
data.addAll(result.content?.articles ?: arrayListOf())
withContext(uiDispatcher){
view?.setupItems(data)
}
}
}
}
//IMoviesListView - /, UIViewController Activity.
interface IMoviesListView {
fun setupItems(items: List<MovieItem>)
}
class MoviesVC: UIViewController, IMoviesListView {
private lazy var presenter: IMoviesPresenter? = {
let presenter = MoviesPresenter()
presenter.attachView(view: self)
return presenter
}()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
presenter?.attachView(view: self)
self.loadMovies()
}
func loadMovies() {
self.presenter?.loadMovies()
}
func setupItems(items: List<MovieItem>){}
//....
class MainActivity : AppCompatActivity(), IMoviesListView {
val presenter: IMoviesPresenter = MoviesPresenter()
override fun onResume() {
super.onResume()
presenter.attachView(this)
presenter.loadMovies()
}
fun setupItems(items: List<MovieItem>){}
//...
Um einen Bereich korrekt aus einem Coroutine-Kontext zu erstellen, müssen Sie einen Coroutine-Dispatcher einrichten.
Diese Logik ist plattformabhängig, daher verwenden wir die Anpassung mit Expect / Actual.
expect val defaultDispatcher: CoroutineContext
expect val uiDispatcher: CoroutineContext
uiDispatcher ist für die Arbeit am UI-Thread verantwortlich. defaultDispatcher wird verwendet, um außerhalb des UI-Threads zu arbeiten.
Der einfachste Weg, es zu erstellen, ist in androidMain, da die Kotlin JVM vorgefertigte Implementierungen für Coroutine-Dispatcher hat. Um auf die entsprechenden Streams zuzugreifen, verwenden wir CoroutineDispatchers Main (UI-Stream) und Default (Standard für Coroutine):
actual val uiDispatcher: CoroutineContext
get() = Dispatchers.Main
actual val defaultDispatcher: CoroutineContext
get() = Dispatchers.Default
Der MainDispatcher wird für die Plattform unter der Haube des CoroutineDispatcher mithilfe der MainDispatcherLoader-Dispatcher-Factory ausgewählt:
internal object MainDispatcherLoader {
private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)
@JvmField
val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()
private fun loadMainDispatcher(): MainCoroutineDispatcher {
return try {
val factories = if (FAST_SERVICE_LOADER_ENABLED) {
FastServiceLoader.loadMainDispatcherFactory()
} else {
// We are explicitly using the
// `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
// form of the ServiceLoader call to enable R8 optimization when compiled on Android.
ServiceLoader.load(
MainDispatcherFactory::class.java,
MainDispatcherFactory::class.java.classLoader
).iterator().asSequence().toList()
}
@Suppress("ConstantConditionIf")
factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
?: createMissingDispatcher()
} catch (e: Throwable) {
// Service loader can throw an exception as well
createMissingDispatcher(e)
}
}
}
Das Gleiche gilt für Standard:
internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
val IO: CoroutineDispatcher = LimitingDispatcher(
this,
systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)),
"Dispatchers.IO",
TASK_PROBABLY_BLOCKING
)
override fun close() {
throw UnsupportedOperationException("$DEFAULT_DISPATCHER_NAME cannot be closed")
}
override fun toString(): String = DEFAULT_DISPATCHER_NAME
@InternalCoroutinesApi
@Suppress("UNUSED")
public fun toDebugString(): String = super.toString()
}
Allerdings verfügen nicht alle Plattformen über Coroutine-Dispatcher-Implementierungen. Zum Beispiel für iOS, das mit Kotlin / Native funktioniert, nicht mit Kotlin / JVM.
Wenn wir versuchen, den Code wie in Android zu verwenden, wird eine Fehlermeldung angezeigt: Mal sehen, was wir
tun.
Ausgabe 470 von GitHub Kotlin Coroutines enthält Informationen, dass spezielle Dispatcher für iOS noch nicht implementiert wurden:
Ausgabe 462 , von der 470 abhängt, die sich noch im Status "Offen" befindet: Die
empfohlene Lösung besteht darin, eigene Dispatcher für iOS zu erstellen:
actual val defaultDispatcher: CoroutineContext
get() = IODispatcher
actual val uiDispatcher: CoroutineContext
get() = MainDispatcher
private object MainDispatcher: CoroutineDispatcher(){
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_main_queue()) {
try {
block.run()
}catch (err: Throwable) {
throw err
}
}
}
}
private object IODispatcher: CoroutineDispatcher(){
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(),
0.toULong())) {
try {
block.run()
}catch (err: Throwable) {
throw err
}
}
}
Beim Start wird der gleiche Fehler angezeigt.
Erstens können wir dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong (), 0.toULong ()) nicht verwenden, da es im
Gegensatz zu Kotlin / nicht an einen Thread in Kotlin / Native gebunden ist: Zweitens Kotlin / Native JVM kann keine Coroutinen zwischen Threads fummeln. Und auch alle veränderlichen Objekte.
Daher verwenden wir in beiden Fällen MainDispatcher:
actual val ioDispatcher: CoroutineContext
get() = MainDispatcher
actual val uiDispatcher: CoroutineContext
get() = MainDispatcher
@ThreadLocal
private object MainDispatcher: CoroutineDispatcher(){
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_main_queue()) {
try {
block.run().freeze()
}catch (err: Throwable) {
throw err
}
}
}
Damit wir veränderbare Codeblöcke und Objekte zwischen Threads übertragen können, müssen wir sie einfrieren , bevor wir sie mit dem Befehl freeze () übertragen:
Wenn wir jedoch versuchen, ein bereits eingefrorenes Objekt einzufrieren, z. B. Singletons, die standardmäßig als eingefroren gelten, erhalten wir FreezingException.
Um dies zu verhindern, markieren wir die Singletons mit der Annotation @ThreadLocal und den globalen Variablen @SharedImmutable:
/**
* Marks a top level property with a backing field or an object as thread local.
* The object remains mutable and it is possible to change its state,
* but every thread will have a distinct copy of this object,
* so changes in one thread are not reflected in another.
*
* The annotation has effect only in Kotlin/Native platform.
*
* PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES.
*/
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
public actual annotation class ThreadLocal
/**
* Marks a top level property with a backing field as immutable.
* It is possible to share the value of such property between multiple threads, but it becomes deeply frozen,
* so no changes can be made to its state or the state of objects it refers to.
*
* The annotation has effect only in Kotlin/Native platform.
*
* PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES.
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
public actual annotation class SharedImmutable
Die Verwendung von MainDispatcher ist in beiden Fällen in Ordnung, wenn Sie mit Ktor arbeiten. Wenn unsere umfangreichen Anforderungen in den Hintergrund treten sollen, können wir sie mit dem Haupt-Dispatcher Dispatchers.Main / MainDispatcher als Kontext an
iOS an GlobalScope senden: iOS
actual fun ktorScope(block: suspend () -> Unit) {
GlobalScope.launch(MainDispatcher) { block() }
}
Android:
actual fun ktorScope(block: suspend () -> Unit) {
GlobalScope.launch(Dispatchers.Main) { block() }
}
Der Anruf und die Kontextänderung werden dann in unserem Dienst sein:
suspend fun loadMovies(callback:(MoviesList?)->Unit) {
ktorScope {
val url =
"http://api.themoviedb.org/3/discover/movie?api_key=KEY&page=1&sort_by=popularity.desc"
val result = networkService.loadData<MoviesList>(url)
delay(1000)
withContext(uiDispatcher) {
callback(result)
}
}
}
Und selbst wenn Sie dort nicht nur die Ktor-Funktionalität aufrufen, funktioniert alles.
Sie können unter iOS auch einen Blockaufruf mit einer Übertragung in den Hintergrund DispatchQueue wie folgt implementieren:
// , ,
actual fun callFreeze(callback: (Response)->Unit) {
val block = {
// ,
callback(Response("from ios").freeze())
}
block.freeze()
dispatch_async {
queue = dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND.toLong,
0.toULong())
block = block
}
}
Natürlich müssen Sie auch auf der Android-Seite tatsächlich Spaß an callFreeze (...) hinzufügen, aber nur, wenn Sie Ihre Antwort auf den Rückruf weiterleiten.
Als Ergebnis nach all Änderungen zu machen, erhalten wir eine Anwendung, die auf beiden Plattformen gleich funktioniert:
Beispiel Quellen github.com/anioutkazharkova/movies_kmp
Es gibt ein ähnliches Beispiel, aber nicht unter Kotlin 1.4
github.com/anioutkazharkova/kmp_news_sample
tproger.ru/articles/creating-an -app-for-kotlin-multiplatform
github.com/JetBrains/kotlin-native
github.com/JetBrains/kotlin-native/blob/master/IMMUTABILITY.md
github.com/Kotlin/kotlinx.coroutines/issues/462
helw.net / 2020/04/16 / Multithreading-in-Kotlin-Multiplattform-Apps