Improve app performance with Kotlin coroutines(用协程提升App性能)

前情提要:谷歌官方文档,你应该读一读吧。其实协程不是像线程,应该是更像在线程池里执行任务,而且不用我们自己处理callback。

Improve app performance with Kotlin coroutines

coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously. Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages.

On Android, coroutines help to solve two primary problems:

  • Manage long-running tasks that might otherwise block the main thread and cause your app to freeze.
  • Providing main-safety, or safely calling network or disk operations from the main thread.(可以在主线程随便调用耗时方法,不用管协程在哪里执行)

This topic describes how you can use Kotlin coroutines to address these problems, enabling you to write cleaner and more concise [kənˈsaɪs] adj. 简明的,简洁的 app code.

Manage long-running tasks

On Android, every app has a main thread that handles the user interface and manages user interactions. If your app is assigning too much work to the main thread, the app can seemingly [ˈsiːmɪŋli] adv. 看来似乎;表面上看来 freeze or slow down significantly. Network requests, JSON parsing, reading or writing from a database, or even just iterating over large lists can cause your app to run slowly enough to cause visible jank—slow or frozen UI that responds slowly to touch events. These long-running operations should run outside of the main thread.

The following example shows simple coroutines implementation for a hypothetical [ˌhaɪpəˈθetɪkl] adj. 假设的 long-running task:

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

Coroutines build upon regular functions by adding two operations to handle long-running tasks. In addition to invoke (or call) and return, coroutines add suspend and resume:(挂起和恢复)

  • suspend pauses the execution of the current coroutine, saving all local variables.
  • resume continues execution of a suspended coroutine from the place where it was suspended.

You can call suspend functions only from other suspend functions or by using a coroutine builder such as launch to start a new coroutine.(挂起函数就像是一段可以丢在其他线程执行的代码,而且还附带了恢复执行的功能)

In the example above, get() still runs on the main thread, but it suspends the coroutine before it starts the network request. When the network request completes, get resumes the suspended coroutine instead of using a callback to notify the main thread.

Kotlin uses a stack frame (栈帧)to manage which function is running along with any local variables. When suspending a coroutine, the current stack frame is copied and saved for later. When resuming, the stack frame is copied back from where it was saved, and the function starts running again. (就像我丢一段代码出去,这段代码会运行在你指定的线程,运行完了之后会自动返回,接着往下执行)Even though the code might look like an ordinary sequential blocking request, the coroutine ensures that the network request avoids blocking the main thread.

Use coroutines for main-safety

Kotlin coroutines use dispatchers(调度器) to determine which threads are used for coroutine execution. To run code outside of the main thread, you can tell Kotlin coroutines to perform work on either the Default or IO dispatcher. In Kotlin, all coroutines must run in a dispatcher, even when they're running on the main thread. Coroutines can suspend themselves, and the dispatcher is responsible for resuming them.(所有协程都必须运行在调度器分配的线程中,而且调度器还负责恢复协程)

To specify where the coroutines should run, Kotlin provides three dispatchers that you can use:(三个牛逼的调度器,好好看看)

  • Dispatchers.Main - Use this dispatcher to run a coroutine on the main Android thread. This should be used only for interacting with the UI and performing quick work. Examples include calling suspend functions, running Android UI framework operations, and updating LiveData objects.
  • Dispatchers.IO - This dispatcher is optimized to perform disk or network I/O outside of the main thread. Examples include using the Room component, reading from or writing to files, and running any network operations.
  • Dispatchers.Default - This dispatcher is optimized to perform CPU-intensive work outside of the main thread. Example use cases include sorting a list and parsing JSON.

Continuing the previous example, you can use the dispatchers to re-define the get function. Inside the body of get, call withContext(Dispatchers.IO) to create a block that runs on the IO thread pool. Any code you put inside that block always executes via the IO dispatcher. Since withContext is itself a suspend function, the function get is also a suspend function.

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

With coroutines, you can dispatch threads with fine-grained adj. 细粒的 control. Because withContext() lets you control the thread pool of any line of code without introducing callbacks, you can apply it to very small functions like reading from a database or performing a network request. A good practice is to use withContext() to make sure every function is main-safe, which means that you can call the function from the main thread. This way, the caller never needs to think about which thread should be used to execute the function.(一般来说,每个耗时函数自己负责指定执行线程,这样就可以从主线程随便调用挂起函数了)

In the previous example, fetchDocs() executes on the main thread; however, it can safely call get, which performs a network request in the background. Because coroutines support suspend and resume, the coroutine on the main thread is resumed with the get result as soon as the withContext block is done.

Important: Using suspend doesn't tell Kotlin to run a function on a background thread. It's normal for suspend functions to operate on the main thread. It's also common to launch coroutines on the main thread. You should always use withContext() inside a suspend function when you need main-safety, such as when reading from or writing to disk, performing network operations, or running CPU-intensive operations.

Performance of withContext()

withContext() does not add extra overhead compared to an equivalent callback-based implementation. Furthermore, it's possible to optimize withContext() calls beyond an equivalent callback-based implementation in some situations. For example, if a function makes ten calls to a network, you can tell Kotlin to switch threads only once by using an outer withContext(). Then, even though the network library uses withContext() multiple times, it stays on the same dispatcher and avoids switching threads. In addition, Kotlin optimizes switching between Dispatchers.Default and Dispatchers.IO to avoid thread switches whenever possible.

Important: Using a dispatcher that uses a thread pool like Dispatchers.IO or Dispatchers.Default does not guarantee that the block executes on the same thread from top to bottom. In some situations, Kotlin coroutines might move execution to another thread after a suspend-and-resume. This means thread-local variables might not point to the same value for the entire withContext() block.

Designate [ˈdezɪɡneɪt]vt. 指定;指派 a CoroutineScope

When defining a coroutine, you must also designate its CoroutineScope. A CoroutineScope manages one or more related coroutines. You can also use a CoroutineScope to start a new coroutine within that scope. Unlike a dispatcher, however, a CoroutineScope doesn't run the coroutines.

One important function of CoroutineScope is stopping coroutine execution when a user leaves a content area within your app. Using CoroutineScope, you can ensure that any running operations stop correctly.

Use CoroutineScope with Android Architecture components

On Android, you can associate CoroutineScope implementations with a component lifecycle. This lets you avoid leaking memory or doing extra work for activities or fragments that are no longer relevant to the user. Using Jetpack components, they fit naturally in a ViewModel. Because a ViewModel isn't destroyed during configuration changes (such as screen rotation), you don't have to worry about your coroutines getting canceled or restarted.

Scopes know about every coroutine that they start. This means that you can cancel everything that was started in the scope at any time. (协程作用域知道他启动的所有协程,所以自然能轻松取消他们)Scopes propagate[ˈprɑːpəɡeɪt]vt. 传播 themselves, so if a coroutine starts another coroutine, both coroutines have the same scope. This means that even if other libraries start a coroutine from your scope, you can cancel them at any time. This is particularly important if you’re running coroutines in a ViewModel. If your ViewModel is being destroyed because the user has left the screen, all the asynchronous work that it is doing must be stopped. Otherwise, you’ll waste resources and potentially leak memory. If you have asynchronous work that should continue after you destroy your ViewModel, it should be done in a lower layer of your app’s architecture.(协程作用域S内的A协程启动B协程,B协程也是在协程作用域S内,S可以把A、B协程一起取消了,真牛逼)

Warning: Coroutines are cancelled cooperatively[kəu'ɔpəreitivli]adv. 合作地 by throwing a CancellationException. Exception handlers that catch Exception or Throwable are triggered during coroutine cancellation.

With the KTX library for Android Architecture components, you can also use an extension property, viewModelScope, to create coroutines that can run until the ViewModel is destroyed.

Start a coroutine

You can start coroutines in one of two ways:(启动协程一般就是launch和async)

  • launch starts a new coroutine and doesn't return the result to the caller. Any work that is considered "fire and forget" can be started using launch.
  • async starts a new coroutine and allows you to return a result with a suspend function called await.

Typically, you should launch a new coroutine from a regular function, as a regular function cannot call await. Use async only when inside another coroutine or when inside a suspend function and performing parallel decomposition.(一般来说launch在普通函数里启动协程,而async一般是在协程中或挂起函数中使用)

Building on the previous examples, here's a coroutine with the viewModelScope KTX extension property that uses launch to switch from regular functions to coroutines:

fun onDocsNeeded() {
    viewModelScope.launch {    // Dispatchers.Main
        fetchDocs()            // Dispatchers.Main (suspend function call)
    }
}

Warning: launch and async handle exceptions differently. Since async expects an eventual call to await at some point, it holds exceptions and rethrows them as part of the await call. This means if you use await to start a new coroutine from a regular function, you might silently drop an exception. These dropped exceptions won't appear in your crash metrics or be noted in logcat.

Parallel decomposition(并行分解)

All coroutines that are started by a suspend function must be stopped when that function returns, so you likely need to guarantee that those coroutines finish before returning. With structured concurrency (结构化并发)in Kotlin, you can define a coroutineScope that starts one or more coroutines. Then, using await() (for a single coroutine) or awaitAll() (for multiple coroutines), you can guarantee that these coroutines finish before returning from the function.

As an example, let's define a coroutineScope that fetches two documents asynchronously. By calling await() on each deferred reference, we guarantee that both async operations finish before returning a value:(coroutineScope是CoroutineScope.kt里的顶层方法,看后面源码备注1)

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

You can also use awaitAll() on collections, as shown in the following example:

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

Even though fetchTwoDocs() launches new coroutines with async, the function uses awaitAll() to wait for those launched coroutines to finish before returning. Note, however, that even if we had not called awaitAll(), the coroutineScope builder does not resume the coroutine that called fetchTwoDocs until after all of the new coroutines completed.

In addition, coroutineScope catches any exceptions that the coroutines throw and routes them back to the caller.

For more information on parallel decomposition, see Composing suspending functions.

Architecture components with built-in support

Some Architecture components, including ViewModel and Lifecycle, include built-in support for coroutines through their own CoroutineScope members.

For example, ViewModel includes a built-in viewModelScope. This provides a standard way to launch coroutines within the scope of the ViewModel, as shown in the following example:

class MyViewModel : ViewModel() {

    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // Modify UI
        }
    }

    /**
    * Heavy operation that cannot be done in the Main Thread
    */
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

LiveData also utilizes coroutines with a liveData block:(看后面源码备注2)

liveData {
    // runs in its own LiveData-specific scope
}

For more information on Architecture components with built-in coroutines support, see Use Kotlin coroutines with Architecture components.

More information

For more coroutines-related information, see the following links:

源码备注1

/**
 * Creates a [CoroutineScope] and calls the specified suspend block with this scope.
 * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
 * the context's [Job].
 *
 * This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails,
 * this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
 * This function returns as soon as the given block and all its children coroutines are completed.
 * A usage example of a scope looks like this:
 *
 * ```
 * suspend fun showSomeData() = coroutineScope {
 *
 *   val data = async(Dispatchers.IO) { // <- extension on current scope
 *      ... load some UI data for the Main thread ...
 *   }
 *
 *   withContext(Dispatchers.Main) {
 *     doSomeWork()
 *     val result = data.await()
 *     display(result)
 *   }
 * }
 * ```
 *
 * The scope in this example has the following semantics:
 * 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI.
 * 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception.
 * 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled.
 * 4) If the `async` block fails, `withContext` will be cancelled.
 *
 * The method may throw a [CancellationException] if the current job was cancelled externally
 * or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope
 * (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
 */
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R =
    suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }

源码备注2

/**
 * Builds a LiveData that has values yielded from the given [block] that executes on a
 * [LiveDataScope].
 *
 * The [block] starts executing when the returned [LiveData] becomes active ([LiveData.onActive]).
 * If the [LiveData] becomes inactive ([LiveData.onInactive]) while the [block] is executing, it
 * will be cancelled after [timeoutInMs] milliseconds unless the [LiveData] becomes active again
 * before that timeout (to gracefully handle cases like Activity rotation). Any value
 * [LiveDataScope.emit]ed from a cancelled [block] will be ignored.
 *
 * After a cancellation, if the [LiveData] becomes active again, the [block] will be re-executed
 * from the beginning. If you would like to continue the operations based on where it was stopped
 * last, you can use the [LiveDataScope.latestValue] function to get the last
 * [LiveDataScope.emit]ed value.

 * If the [block] completes successfully *or* is cancelled due to reasons other than [LiveData]
 * becoming inactive, it *will not* be re-executed even after [LiveData] goes through active
 * inactive cycle.
 *
 * As a best practice, it is important for the [block] to cooperate in cancellation. See kotlin
 * coroutines documentation for details
 * https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html.
 *
 * ```
 * // a simple LiveData that receives value 3, 3 seconds after being observed for the first time.
 * val data : LiveData<Int> = liveData {
 *     delay(3000)
 *     emit(3)
 * }
 *
 *
 * // a LiveData that fetches a `User` object based on a `userId` and refreshes it every 30 seconds
 * // as long as it is observed
 * val userId : LiveData<String> = ...
 * val user = userId.switchMap { id ->
 *     liveData {
 *       while(true) {
 *         // note that `while(true)` is fine because the `delay(30_000)` below will cooperate in
 *         // cancellation if LiveData is not actively observed anymore
 *         val data = api.fetch(id) // errors are ignored for brevity
 *         emit(data)
 *         delay(30_000)
 *       }
 *     }
 * }
 *
 * // A retrying data fetcher with doubling back-off
 * val user = liveData {
 *     var backOffTime = 1_000
 *     var succeeded = false
 *     while(!succeeded) {
 *         try {
 *             emit(api.fetch(id))
 *             succeeded = true
 *         } catch(ioError : IOException) {
 *             delay(backOffTime)
 *             backOffTime *= minOf(backOffTime * 2, 60_000)
 *         }
 *     }
 * }
 *
 * // a LiveData that tries to load the `User` from local cache first and then tries to fetch
 * // from the server and also yields the updated value
 * val user = liveData {
 *     // dispatch loading first
 *     emit(LOADING(id))
 *     // check local storage
 *     val cached = cache.loadUser(id)
 *     if (cached != null) {
 *         emit(cached)
 *     }
 *     if (cached == null || cached.isStale()) {
 *         val fresh = api.fetch(id) // errors are ignored for brevity
 *         cache.save(fresh)
 *         emit(fresh)
 *     }
 * }
 *
 * // a LiveData that immediately receives a LiveData<User> from the database and yields it as a
 * // source but also tries to back-fill the database from the server
 * val user = liveData {
 *     val fromDb: LiveData<User> = roomDatabase.loadUser(id)
 *     emitSource(fromDb)
 *     val updated = api.fetch(id) // errors are ignored for brevity
 *     // Since we are using Room here, updating the database will update the `fromDb` LiveData
 *     // that was obtained above. See Room's documentation for more details.
 *     // https://developer.android.com/training/data-storage/room/accessing-data#query-observable
 *     roomDatabase.insert(updated)
 * }
 * ```
 *
 * @param context The CoroutineContext to run the given block in. Defaults to
 * [EmptyCoroutineContext] combined with
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 * @param timeoutInMs The timeout in ms before cancelling the block if there are no active observers
 * ([LiveData.hasActiveObservers]. Defaults to [DEFAULT_TIMEOUT].
 * @param block The block to run when the [LiveData] has active observers.
 */
@UseExperimental(ExperimentalTypeInference::class)
fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值