前回の記事では、Kotlinマルチプラットフォームアプリケーションにマルチスレッドを実装する方法の1つについて説明しました。今日は、最も共有されている共通コードを使用してアプリケーションを実装し、スレッドを使用するすべての作業を共通ロジックに転送する場合の代替状況を検討します。
前の例では、ネットワーククライアントで非同期を提供するすべての主要な作業を引き継いだKtorライブラリの助けを借りました。これにより、その特定のケースではiOSでDispatchQueueを使用する必要がなくなりましたが、他のケースでは、実行キュージョブを使用してビジネスロジックを呼び出し、応答を処理する必要がありました。Android側では、MainScopeを使用して一時停止された関数を呼び出しました。
したがって、共通のプロジェクトでマルチスレッドを使用して統一された作業を実装する場合は、それが実行されるコルーチンのスコープとコンテキストを正しく構成する必要があります。
簡単に始めましょう。日常のコンテキストから取得した、スコープ内のサービスメソッドを呼び出すアーキテクチャメディエーターを作成しましょう。
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)
}
}
メディエーターメソッドでサービスを呼び出し、それをUIに渡します。
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>){}
//...
定期的なコンテキストからスコープを正しく作成するには、定期的なディスパッチャを設定する必要があります。
このロジックはプラットフォームに依存するため、expect / actualでカスタマイズを使用します。
expect val defaultDispatcher: CoroutineContext
expect val uiDispatcher: CoroutineContext
uiDispatcherは、UIスレッドでの作業を担当します。defaultDispatcherは、UIスレッドの外部で機能するために使用されます。
Kotlin JVMには、通常のディスパッチャ用の既製の実装があるため、これを作成する最も簡単な方法はandroidMainです。対応するストリームにアクセスするには、CoroutineDispatchers Main(UIストリーム)とDefault(Coroutineの標準)を使用します。
actual val uiDispatcher: CoroutineContext
get() = Dispatchers.Main
actual val defaultDispatcher: CoroutineContext
get() = Dispatchers.Default
MainDispatcherは、MainDispatcherLoaderディスパッチャファクトリを使用して、CoroutineDispatcherの内部のプラットフォーム用に選択されます。
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)
}
}
}
デフォルトと同じです:
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()
}
ただし、すべてのプラットフォームに定期的なディスパッチャが実装されているわけではありません。たとえば、Kotlin / JVMではなく、Kotlin / Nativeで動作するiOSの場合。
Androidのようにコードを使用しようとすると、エラーが発生します
。何をしているのか見て みましょう。
問題470 GitHubのKotlinコルーチンからは特別ディスパッチャはまだiOSのために実装されていないことの情報が含まれています
発行462 470が依存する、オープン状態で同じまだ:
推奨ソリューションは、iOSのための独自のディスパッチャを作成することです:
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
}
}
}
起動時に同じエラーが発生します。
まず、dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong()、0.toULong()))を使用できません。これは、Kotlin / Nativeのどのスレッドにもバインドされていないためです。
次に、Kotlin / Nativeは、Kotlin /とは異なります。 JVMは、スレッド間でコルルーチンをファンブルすることはできません。また、可変オブジェクト。
したがって、どちらの場合も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
}
}
}
コードとオブジェクトの可変ブロックをスレッド間で転送できるようにするには、freeze()コマンドを使用して転送する前にそれらをフリーズする必要があります 。
ただし、デフォルトでフリーズと見なされるシングルトンなど、すでにフリーズされているオブジェクトをフリーズしようとすると、次のようになります。 FreezingException。
これを防ぐために、シングルトンに@ThreadLocalアノテーションを付け、グローバル変数@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
Ktorを使用する場合は、どちらの場合もMainDispatcherを使用しても問題ありません。大量のリクエストをバックグラウンドで送信する場合は、メインディスパッチャDispatchers.Main / MainDispatcherをコンテキストとしてGlobalScopeに送信できます:
iOS
actual fun ktorScope(block: suspend () -> Unit) {
GlobalScope.launch(MainDispatcher) { block() }
}
アンドロイド:
actual fun ktorScope(block: suspend () -> Unit) {
GlobalScope.launch(Dispatchers.Main) { block() }
}
呼び出しとコンテキストの変更は、次のサービスで行われます。
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)
}
}
}
そして、そこでKtor機能を呼び出すだけでなく、すべてが機能します。
次のように、バックグラウンドDispatchQueueに転送するブロック呼び出しをiOSに実装することもできます。
// , ,
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
}
}
もちろん、Android側にも実際の楽しいcallFreeze(...)を追加する必要がありますが、応答をコールバックに渡すだけです。
その結果、すべての編集を行った後、両方のプラットフォームで同じように機能するアプリケーションが得られます。
ソースの例github.com/anioutkazharkova/movies_kmp
同様の例がありますが、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-multiplatform-apps