王学岗Kotlin协程(一)————认识协程与协程上手

一:协程是什么?

协程基于线程,是轻量级的线程
coroutine = cooperation+routine
难度在哪里?
1,java中不曾出现,新概念
2,概念不清晰
3,Kotlin基础不扎实
4,多线程基础太薄弱

二:在android中协程用来解决什么问题?

1,处理耗时任务,这种任务常常会阻塞主线程
2,保证主线程安全,确保安全的从主线程调用任何suspend函数(挂起函数)
异步任务已经过时,google建议我们使用协程取代异步任务。

三:创建项目工程与相关配置

android studio基本上帮我们创建好了,我们不需要添加什么

四:异步任务

先来个界面,只有一个TextVIew和一个Button,代码就不贴了。点击按钮访问网络,结果显示到TextView上。我们先用异步任务实现。
增加retrofit的支持

 implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

增加访问网络的权限

 <uses-permission android:name="android.permission.INTERNET"/>
package com.example.testkotlin1030.api

import android.util.Log
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query

//lazy () 是一个接收一个lambda表达式,并返回一个Lazy<T> 实例的函数。
// 返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果,
// 后续调用 get() 只是返回记录的结果。 默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):
// 该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,
// 那么将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。
// 而如果你确定初始化将总是发生在单个线程,那么你可以使用 LazyThreadSafetyMode.NONE 模式,
// 它不会有任何线程安全的保证以及相关的开销。


val userServiceApi: UserServiceApi by lazy {
    val retrofit: Retrofit = Retrofit.Builder()
        .client(OkHttpClient.Builder().addInterceptor {
            it.proceed(it.request()).apply {
                Log.d("wangxuegang", "request${code()}")
            }
        }.build())
            //我自己搭建的服务器,返回name和address
        .baseUrl("http://192.168.1.4:8080/myserver")
        .addConverterFactory(MoshiConverterFactory.create())
        .build()
    retrofit.create(UserServiceApi::class.java)
}

interface UserServiceApi {
    @GET("user")
    fun loadUser(@Query("name") name:String): Call<User>
}
package com.example.testkotlin1030.api
//数据类,默认实现了toString()
//支持运算符重载,例如两个user对象相加,就是把user属性相加
data class User(var name:String,var address:String)

异步任务的写法

package com.example.testkotlin1030

import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import com.example.testkotlin1030.api.User
import com.example.testkotlin1030.api.userServiceApi

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var tvShow : TextView = findViewById<TextView>(R.id.tvShow)
        tvShow.text = "王学岗"
        //also 把接收者作为值参传给Lambda,返回接收者对象
        var buSubmit : Button = findViewById<Button>(R.id.btSubmit).also {
                    it.setOnClickListener(object : View.OnClickListener{
                    //点击请求网络
                        override fun onClick(v: View?) {
                        //对象表达式(类似java的匿名内部类 )
                               object : AsyncTask<Void,Void,User>(){
                                   override fun doInBackground(vararg params: Void?): User? {
                                        return userServiceApi.loadUser("XXX").execute().body()
                                   }

                                   override fun onPostExecute(result: User?) {
                                       super.onPostExecute(result)
                                       tvShow.text=result?.address
                                   }
                               }
                        }

                    })
        }


    }
}

问题:更新UI与访问网络(阻塞操作)在不同的方法里,阅读比较困难。人类阅读文章是从上向下陈述式的(串行思维)。
onPostExecute是一个回掉函数,容易出现回调地狱。

五:协程与异步任务对比

首先修改RetrofitServiceApi,协程里只能使用挂起函数

interface UserServiceApi {
//直接返回User对象
    @GET("user")
   suspend fun loadUser(@Query("name") name:String):User
}

引入协程的依赖

   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0-RC-native-mt'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0-RC-native-mt'
package com.example.testkotlin1030

import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import com.example.testkotlin1030.api.User
import com.example.testkotlin1030.api.userServiceApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Call

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var tvShow : TextView = findViewById<TextView>(R.id.tvShow)
        tvShow.text = "王学岗"
        //also 把接收者作为值参传给Lambda,返回接收者对象
        var buSubmit : Button = findViewById<Button>(R.id.btSubmit).also {
                    it.setOnClickListener(object : View.OnClickListener{
                        override fun onClick(v: View?) {
                            //GlobalScope顶级协程,用于创建协程,launch协程构建器
                            GlobalScope.launch (Dispatchers.Main){//指定在主线程启动协程,
                            val user:User= withContext(Dispatchers.IO){//协程任务调度器,开启一个子线程访问网络
                                userServiceApi.loadUser("XXX")
                            }
                            //因为已经制定Dispatchers.Main,这里会在主线程更新UI
                            tvShow.text = "address:${user?.address}"
                            }
                        }

                    })
        }


    }
}

协程让异步逻辑同步化,杜绝回调监狱,协程最核心的特点是,函数或者一段程序能够挂起,稍后再挂起的位置恢复。

六:协程的挂起与恢复

常规函数的操作包括invoke和return(调用和返回),协程新增加了suspend和resume(挂起与恢复)
suspend:挂起或暂停,用于暂停执行当前协程,并保存所有局部变量
resume:用于让已暂停的协程,从其暂停处继续执行

七堆栈中的函数调用流程

重构下代码

package com.example.testkotlin1030

import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import com.example.testkotlin1030.api.User
import com.example.testkotlin1030.api.userServiceApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Call

class MainActivity : AppCompatActivity() {
    private var tvShow:TextView? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvShow = findViewById<TextView>(R.id.tvShow)
        tvShow?.text = "王学岗"
        //also 把接收者作为值参传给Lambda,返回接收者对象
        var buSubmit: Button = findViewById<Button>(R.id.btSubmit).also {
            it.setOnClickListener(object : View.OnClickListener {
                override fun onClick(v: View?) {
                    //GlobalScope顶级协程,用于创建协程,launch协程构建器
                    GlobalScope.launch(Dispatchers.Main) {//指定在主线程启动协程
                        getUser()

                    }
                }

            })
        }
    }

    private suspend fun getUser() {
        val user = get()
        show(user)
    }

    //在一个函数中调用挂起函数,其也必须是挂起函数,返回值是User
    //启动后台线程访问网络
    private suspend fun get() = withContext(Dispatchers.IO) {
        userServiceApi.loadUser("XXX")
    }

    private fun show(user: User) {
        tvShow?.text = "address:${user?.address}"
    }
}

当执行getUser()函数的时候,其会出现在主线程的堆栈当中。
在这里插入图片描述

执行get()的时候,getUser会被挂起,get()函数来到堆栈当中。
在这里插入图片描述
get()函数继续执行的时候,它也会被挂起。get()被挂起之后就会有异步任务。启动异步协程,做些耗时操作。
在这里插入图片描述

异步任务执行完后,get()函数返回到堆栈中。至此get()函数执行完毕,从堆栈中移除。
在这里插入图片描述
继续执行show(user)方法,getUser函数也会恢复,回到堆栈中。至此其执行完毕。
在这里插入图片描述

注意:1,使用suspend关键字修饰的函数叫做挂起函数,挂起函数只能在协程体内或其他挂起函数内调用。
2,withContext必须在协程或者suspend函数中调用,否则会报错。它必须显示指定代码块所运行的线程,它会阻塞当前上下文线程,有返回值,会返回代码块的最后一行的值。如果最后一行是无返回值的函数,则它的返回值是最后一行函数的返回值

8挂起与阻塞的区别

阻塞
在这里插入图片描述
挂起
在这里插入图片描述
挂起就是挂起点记下来。

九代码实现挂起与阻塞对比

阻塞

package com.example.testkotlin1030

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    private var tvShow:TextView? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvShow = findViewById<TextView>(R.id.tvShow)
        tvShow?.text = "王学岗"
        //also 把接收者作为值参传给Lambda,返回接收者对象
        var buSubmit: Button = findViewById<Button>(R.id.btSubmit).also {
            it.setOnClickListener(object : View.OnClickListener {
                override fun onClick(v: View?) {
                    //GlobalScope顶级协程,用于创建协程,launch协程构建器
                    GlobalScope.launch(Dispatchers.Main) {//指定在主线程启动协程
                        //delay是挂起函数
                         delay(12000)//我们有个耗时操作delay,delay是一个挂起函数,12秒之后会执行下面的代码
                       Log.i("zhang_xin","${Thread.currentThread().name}---after delay")
                    }
                }

            })
        }
    }


}

可以看到12秒后会打印输出main—after delay(说明在主线程),界面中的按钮按下会立刻抬起。多次点击也没有任何问题,12秒后会多次打印,因为启动了多个协程,每个协程打印了
阻塞

package com.example.testkotlin1030

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    private var tvShow:TextView? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvShow = findViewById<TextView>(R.id.tvShow)
        tvShow?.text = "王学岗"
        //also 把接收者作为值参传给Lambda,返回接收者对象
        var buSubmit: Button = findViewById<Button>(R.id.btSubmit).also {
            it.setOnClickListener(object : View.OnClickListener {
                override fun onClick(v: View?) {
                    //GlobalScope顶级协程,用于创建协程,launch协程构建器
                    GlobalScope.launch(Dispatchers.Main) {//指定在主线程启动协程
                        Thread.sleep(12000)//阻塞12秒,非挂起函数
                          Log.i("zhang_xin","${Thread.currentThread().name}---after sleep")
                    }
                }

            })
        }
    }


}

12秒后打印输出 main—after sleep(同样在主线程),但是按钮不会马上弹起,12秒后才会弹起(因为阻塞了主线程)。
多次点击会出现
在这里插入图片描述
android在主线程中更新UI,16毫秒刷新一次界面,如果主线程干了很多事,主线程就不能刷新,会出现漏帧现象(Skipped 720 frames)。

10协程的基础设施层和业务框架层

kotlin协程实现分为两个层次
基础设施层和业务框架层。前者是标准库的协程API,主要对协程提供了概念和语义上最基本的支持。后者,是协程的上层框架支持。GlobalScope,launch,delay都属于业务框架层
下面我们看下基础设施层的API

package com.example.testkotlin1030

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.coroutines.*

class MainActivity : AppCompatActivity() {
    private var tvShow: TextView? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
      //协程的挂起点就是通过Continuation中的协程上下文保存起来的。
       val continuation:Continuation<Unit> =
           //suspend(挂起函数)就是协程体,耗时操作可以写在里面
           suspend {
            5
            //createCoroutine需要传入接口(我们这里传入对象表达式),协程体返回的是Int类型,所以这里的泛型就是Int
        }.createCoroutine(object : Continuation<Int>{
            //协程上下文,
            override val context: CoroutineContext = EmptyCoroutineContext


            override fun resumeWith(result: Result<Int>) {
                 //协程执行完后,执行结果返回这里,这个resumewith实际上就是回调
                Log.i("zhang_xin","$result")
            }
        })
        //上面的代码只是创建了协程,并未执启动协程。下面这句代码启动协程
        continuation.resume(Unit)
    }
}
打印输出I/zhang_xin: Success(5)

十一协程的调度器

调度器属于基础设施层
所有协程必须在调度器中运行,即使它们在主线程运行也是如此
在这里插入图片描述

十二任务泄露

当某个协程任务丢失,无法追踪,会导致内存,CPU,磁盘等资源浪费,甚至发送一个无用的网络请求,这种情况称为任务泄露。
比如:在Activity中发送网络请求,响应未完成,这时快速按下返回键。Activity虽然被销毁了,但是占用内存的网络请求还在。
为了避免协程任务泄露,Kotlin引入了结构化并发机制。

十三结构化并发

结构化并发可以做到:
取消任务(当某项任务不再需要时取消它)
追踪任务(当任务执行时,追踪它)
发出错误信号(当协程失败时候,发出错误信号表明有错误发生)
在这里插入图片描述
注:CoroutineScope就是协程作用域
GlobalScope:除非进程被杀死,否则协程一直存在。

十四MainScope使用

代码中增加依赖

  implementation "androidx.activity:activity-ktx:1.1.0"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

package com.example.testkotlin1030

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.testkotlin1030.api.User
import com.example.testkotlin1030.api.userServiceApi
import kotlinx.coroutines.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.createCoroutine

class MainActivity : AppCompatActivity() {
    //MainScope()是一个大写开头的普通函数(普通函数大写开头的是工厂模式),不是一个类
    private val mainScope = MainScope()
    private var tvShow:TextView? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvShow = findViewById<TextView>(R.id.tvShow)
        findViewById<Button>(R.id.btSubmit).also {
            it.setOnClickListener {
                //所有的协程都必须被定义在协程作用域里面
                mainScope.launch {
                     val user: User =  userServiceApi.loadUser("XXX")
                    /**
                     * 还记得不,我们以前要写一个withContext切换到Io的调度器上
                     * 。这里其实可以省略,如果retrofit知道你是个挂起函数,它会自动的
                     * 开启一个IO调度器的协程。
                     */
                     tvShow?.text = "${user.address}"
                }
            }
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        //通过mainScope启动的协程,只要mainScope取消了,里面所有的任务都会被取消
        //协程在取消的时候会抛出一个异常,可以利用这一点检验协程是否取消了。
        mainScope.cancel()
    }
}

可以用加延迟点击返回键的方法来验证异常

  /*mainScope.launch {
                    val user = userServiceApi.getUser("xx")
                    nameTextView?.text = "address:${user?.address}"
                    *//*try {
                        delay(10000)
                    }catch (e:Exception){
                        e.printStackTrace()
                    }*//*
                }*/

会抛出如下异常
在这里插入图片描述

委托方式的写法

package com.example.testkotlin1030

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.testkotlin1030.api.User
import com.example.testkotlin1030.api.userServiceApi
import kotlinx.coroutines.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.createCoroutine

/**
 * CoroutineScope是一个借口,继承接口要覆盖它的属性
 *  public val coroutineContext: CoroutineContext
 *  但我们现在不覆盖它,MainScope返回了一个CoroutineScope对象,
 *  并把该对象的上下文赋值给coroutineContext,MainActivity
 *  实际上是继承了MainScope返回的对象
 */
class MainActivity : AppCompatActivity(),CoroutineScope by MainScope() {
    

    private var tvShow:TextView? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvShow = findViewById<TextView>(R.id.tvShow)
        findViewById<Button>(R.id.btSubmit).also {
            it.setOnClickListener {
                //所有的协程都必须被定义在协程作用域里面
                launch {
                     val user: User =  userServiceApi.loadUser("XXX")
                     tvShow?.text = "${user.address}"
                }
            }
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        //委托模式直接写cancel
       cancel()
    }
}

十五 协程上手----ViewModelScope的使用

首先我们来看下,如何使用ViewModelScope。
1,gradle中增加对dataBinding的支持

 dataBinding {
        enabled = true
    }

2,布局文件

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="com.dongnaoedu.kotlincoroutine.viewmodel.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <Button
            android:id="@+id/submitButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="104dp"
            android:text="鎻愪氦"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@+id/guideline" />

        <TextView
            android:id="@+id/nameTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.userLiveData.name}"
            android:textSize="18sp"
            app:layout_constraintBottom_toTopOf="@+id/guideline"
            app:layout_constraintHorizontal_bias="0.484"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.681" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/guideline"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.5" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3,代码

package com.dongnaoedu.kotlincoroutine.viewmodel

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dongnaoedu.kotlincoroutine.api.User
import com.dongnaoedu.kotlincoroutine.repository.UserRepository
import kotlinx.coroutines.launch

class MainViewModel() : ViewModel() {

    val userLiveData = MutableLiveData<User>()

    private val userRepository = UserRepository()

    fun getUser(name: String) {
    //使用viewModelScope启动协程进行网络请求。
        viewModelScope.launch {
            userLiveData.value = userRepository.getUser(name)
        }
    }

}

创建ViewModel

package com.dongnaoedu.kotlincoroutine.repository

import com.dongnaoedu.kotlincoroutine.api.User
import com.dongnaoedu.kotlincoroutine.api.userServiceApi

/**
 *
 * @author ningchuanqi
 * @version V1.0
 */
class UserRepository {

    suspend fun getUser(name: String): User {
        return userServiceApi.getUser(name)
    }

}

UserRepository 既可以在网络请求,也可以在数据库中请求

package com.dongnaoedu.kotlincoroutine.activity

import android.annotation.SuppressLint
import android.os.AsyncTask
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.dongnaoedu.kotlincoroutine.R
import com.dongnaoedu.kotlincoroutine.api.User
import com.dongnaoedu.kotlincoroutine.api.userServiceApi
import com.dongnaoedu.kotlincoroutine.databinding.ActivityMainBinding
import com.dongnaoedu.kotlincoroutine.viewmodel.MainViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
 *
 * @author ningchuanqi
 * @version V1.0
 */
class MainActivity07 : AppCompatActivity() {

    private val mainViewModel: MainViewModel by viewModels()

    @SuppressLint("StaticFieldLeak","SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
        binding.viewModel = mainViewModel
        binding.lifecycleOwner = this//即当前的Activity
        binding.submitButton.setOnClickListener {
            mainViewModel.getUser("xxx")
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值