美文网首页
Android kotlin 协程笔记(一)

Android kotlin 协程笔记(一)

作者: 坑逼的严 | 来源:发表于2021-06-17 11:18 被阅读0次

    有次面试,面试官问我协程是什么?用过吗?我的回答是:协程就是基于线程,一个轻量级的线程。不要过于迷信和夸张。我也不知道回答的怎么样,哈哈哈。

    传统异步请求和加入协程的请求

    前期准备UI,分为两块,看看两种的区别


    image.png

    布局代码

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        tools:context=".kotlinCoroutine.CoroutineOneActivity">
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@android:color/holo_blue_light">
            <View
                android:id="@+id/line"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#a0a0a0"
                android:layout_centerInParent="true"/>
            <TextView
                android:id="@+id/mTvAsync"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="返回值"
                android:gravity="center"
                android:layout_centerHorizontal="true"
                android:layout_above="@id/line"
                android:padding="5dp"/>
            <Button
                android:id="@+id/mBtnAsync"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:text="异步获取数据"/>
        </RelativeLayout>
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@android:color/holo_red_light">
            <View
                android:id="@+id/line2"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#a0a0a0"
                android:layout_centerInParent="true"/>
            <TextView
                android:id="@+id/mTvCoroutine"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="返回值"
                android:gravity="center"
                android:layout_centerHorizontal="true"
                android:layout_above="@id/line2"
                android:padding="5dp"/>
            <Button
                android:id="@+id/mBtnCoroutine"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:text="协程获取数据"/>
        </RelativeLayout>
    </LinearLayout>
    

    引入网络请求代码

        implementation 'com.squareup.retrofit2:retrofit:2.6.0'
        implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
        implementation 'com.squareup.retrofit2:converter-scalars:2.0.0'
    

    清单文件添加网络权限和高版本请求网络配置

    <uses-permission android:name="android.permission.INTERNET" />
    
    <application
            ...
            android:networkSecurityConfig="@xml/network_config"
            android:theme="@style/AppTheme">
    

    network_config.xml

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <base-config cleartextTrafficPermitted="true" />
    </network-security-config>
    

    创建Retrofit工具

    class RetrofitClient{
        private val retrofit by lazy {
            Log.d("yanjin","retrofit")
            retrofit2.Retrofit.Builder()
                .client(OkHttpClient.Builder().build())
                .baseUrl("http://apis.juhe.cn/fapig/euro2020/")
                .addConverterFactory(ScalarsConverterFactory.create())//不返回json对象而是返回string
                .build()
        }
    
        val api by lazy {
            retrofit.create(API::class.java)
        }
        companion object{
            val getInstance: RetrofitClient by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
                RetrofitClient()
            }
        }
    }
    
    interface API {
        @FormUrlEncoded
        @POST("schedule")
        fun loadString(@Field("key") key:String):Call<String>
    
        @FormUrlEncoded
        @POST("schedule")
        suspend fun getString(@Field("key") key:String): String
    
    }
    

    最后重点来了activity中调用

    class CoroutineOneActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_coroutine_one)
            mBtnAsync?.setOnClickListener {
                onAsync()
            }
            mBtnCoroutine?.setOnClickListener {
                onCoroutine()
            }
        }
    
        private fun onCoroutine() {
            GlobalScope.launch(Dispatchers.Main) {
                val result = withContext(Dispatchers.IO){
                    RetrofitClient.getInstance.api.getString("eb7d9ea45c80b366bfeefddcd491c39d")
                }
                mTvCoroutine?.text = result
            }
        }
    
        /**
         * 异步方式调用
         */
        @SuppressLint("StaticFieldLeak")
        private fun onAsync() {
    
            object:AsyncTask<Void,Void,String>(){
                override fun doInBackground(vararg params: Void?): String {
                    return RetrofitClient.getInstance.api.loadString("eb7d9ea45c80b366bfeefddcd491c39d").execute().body()!!
                }
    
                override fun onPostExecute(result: String?) {
                    super.onPostExecute(result)
                    mTvAsync?.text = result
                }
            }.execute()
        }
    }
    

    这么说呢?用了协程的话,代码读起来更像读书了,因为它就是自上而下的,没有AsyncTask的那种回调,避免了地狱式回调,有意思哈哈哈。

    挂起和恢复

    上文中的suspend就是挂起操作,那么恢复就是resume。


    image.png

    我们把上面的协程调用稍微改写一下便于理解

    mBtnCoroutine?.setOnClickListener {
                GlobalScope.launch(Dispatchers.Main) {
                    onCoroutine()
                }
                
            }
    ......
    private suspend fun onCoroutine() {
            val result = get()
            show(result)
        }
    
        private suspend fun get() = withContext(Dispatchers.IO){
            RetrofitClient.getInstance.api.getString("eb7d9ea45c80b366bfeefddcd491c39d")
        }
    
        private fun show(result: String){
            mTvCoroutine?.text = result
        }
    

    当我们点击按钮时,onCoroutine方法处于主线程中,他会先执行自己的get()方法,当然这个方法是一个挂起函数,也就是他需要异步操作,这个时候get后面的东西不再执行,但是也不阻塞主线程。因为一旦阻塞久了就是崩溃ANR了。当get()执行完毕,就会resume到之前的挂起原点也就是记录点

    val result = get()
    

    并且返回来值,后面我们就能设置值了。当然,为什么我们能直接设置值呢?为什么不用切换成主线程呢?因为onCoroutine方法就是指定在了主线程,所以,协程的好处就是避免了写很多的切换线程代码了。
    我理解的协程挂起和恢复就是把异步的代码用同步的方式写了出来,onCoroutine方法里面有get()这个耗时操作,那么就等他执行完再执行下面的,所以我们体验下来就是同步的感觉。

    代码上体验阻塞和挂起

    image.png

    还是差不多的界面,分别点击下面的按钮改变上面的文字。
    UI代码

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        tools:context=".kotlinCoroutine.CoroutineOneActivity">
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@android:color/holo_blue_light">
            <View
                android:id="@+id/line"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#a0a0a0"
                android:layout_centerInParent="true"/>
            <TextView
                android:id="@+id/mTvBlock"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="未设置"
                android:gravity="center"
                android:layout_centerHorizontal="true"
                android:layout_above="@id/line"
                android:padding="5dp"/>
            <Button
                android:id="@+id/mBtnBlock"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:text="阻塞式设置数据"/>
        </RelativeLayout>
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@android:color/holo_red_light">
            <View
                android:id="@+id/line2"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#a0a0a0"
                android:layout_centerInParent="true"/>
            <TextView
                android:id="@+id/mTvCoroutine"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="未设置"
                android:gravity="center"
                android:layout_centerHorizontal="true"
                android:layout_above="@id/line2"
                android:padding="5dp"/>
            <Button
                android:id="@+id/mBtnCoroutine"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:text="挂起式设置数据"/>
        </RelativeLayout>
    </LinearLayout>
    

    我们知道,线程的sleep就是阻塞式的,那么我们就用它模拟

    mBtnBlock?.setOnClickListener {
                //阻塞式设置数据
                Thread.sleep(10000)//阻塞10s
                mTvBlock?.text = "阻塞完毕"
                Log.d("yanjin","阻塞完毕")
            }
    

    log如下:

    2021-06-16 14:19:24.629 1384-1384/com.example.myapplication D/yanjin: 阻塞完毕
    2021-06-16 14:19:24.632 1384-1384/com.example.myapplication I/Choreographer: Skipped 601 frames!  The application may be doing too much work on its main thread.
    

    当我们只按一次,10s后设置了数据,而且打印了log,但是看Android提示的话语,Skipped 601 frames,跳过了601帧,The application may be doing too much work on its main thread.在主线程做了太多工作,如果我们多点几下就能奔溃,ANR了。
    而挂起呢?

    mBtnCoroutine?.setOnClickListener {
                //挂起式设置数据
                GlobalScope.launch (Dispatchers.Main){
                    delay(10000)//挂起10s
                    mTvCoroutine?.text = "挂起完毕"
                    Log.d("yanjin","挂起完毕")
                }
    
            }
    

    GlobalScope手动指定为主线程delay是协程中重要的延时执行的方法,他是挂起式的。

    2021-06-16 14:29:05.856 4627-4627/com.example.myapplication D/yanjin: 挂起完毕
    2021-06-16 14:29:05.999 4627-4627/com.example.myapplication D/yanjin: 挂起完毕
    2021-06-16 14:29:06.180 4627-4627/com.example.myapplication I/chatty: uid=10699(com.example.myapplication) identical 1 line
    2021-06-16 14:29:06.352 4627-4627/com.example.myapplication D/yanjin: 挂起完毕
    2021-06-16 14:29:06.543 4627-4627/com.example.myapplication D/yanjin: 挂起完毕
    2021-06-16 14:29:06.730 4627-4627/com.example.myapplication D/yanjin: 挂起完毕
    

    看log可知,我们点击多次,Android也没有提示丢失帧也没有ANR。所以他是真的安全的。

    协程的不同作用域

    image.png
    GlobalScope

    是进程级别的,除非进程杀死,才能停下来,所以用它做事情记得判断activity或fragment是否为空或是否显示。

    MainScope

    有一个cancel方法,他会抛出异常进而阻止协程继续下去。所以我们可以绑定activity或fragment的销毁生命周期

    class CoroutineTirActivity : AppCompatActivity(),CoroutineScope by MainScope() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_coroutine_tir)
            mBtnMainScope?.setOnClickListener {
                testMainScope();
            }
        }
    
        /**
         * 学习学习MainScope
         */
        private fun testMainScope() {
            launch {
                val result =
                    RetrofitClient.getInstance.api.getString("eb7d9ea45c80b366bfeefddcd491c39d")
                Log.d("yanjin", "result = $result")
            }
        }
    
        override fun onDestroy() {
            super.onDestroy()
            cancel()
        }
    }
    

    这里我们用了另外一种方式,activity代理了MainScope

    CoroutineScope by MainScope()
    

    所以我们直接在activity中就能使用他的方法了,不用对象了。还有一点,

    RetrofitClient.getInstance.api.getString("eb7d9ea45c80b366bfeefddcd491c39d")
    

    我在调用网络接口得时候,没有在家withContext,这是retrofit2对协程进行的改造。更方便了。

    viewModelScope

    我们点击一个按钮请求接口并设置数据,当然viewModelScope要和Binding、viewmodle等配合使用,所以就能用的更爽了
    布局文件

    <?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"
        xmlns:tools="http://schemas.android.com/tools">
    
        <data>
            <variable
                name="viewModle"
                type="com.example.myapplication.kotlinCoroutine.MyViewModle" />
        </data>
    
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".kotlinCoroutine.CoroutineForActivity">
    
            <Button
                android:id="@+id/mBtn"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:text="请求" />
    
            <TextView
                android:id="@+id/mTextView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_above="@id/mTextView"
                android:text="@{viewModle.liveData}"/>
        </RelativeLayout>
    </layout>
    

    activity中不负责网络请求,遵循mvvm

    class CoroutineForActivity : AppCompatActivity() {
        private val viewModle: MyViewModle by viewModels()
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val mBinding = DataBindingUtil.setContentView<ActivityCoroutineForBinding>(
                CoroutineForActivity@ this,
                R.layout.activity_coroutine_for
            )
            mBinding.viewModle = viewModle
            mBinding.lifecycleOwner = CoroutineForActivity@ this
            mBtn?.setOnClickListener {
                viewModle.getResult()
            }
        }
    }
    

    viewmodle的话只是一个管理请求和管理数据

    class MyViewModle() : ViewModel() {
        var liveData = MutableLiveData<String>()
        private val myRepository = MyRepository()
        public fun getResult(){
            viewModelScope.launch {
                liveData.value = myRepository.getResult()
            }
        }
    }
    

    谷歌官网推介用Repository请求网络

    class MyRepository {
        public suspend fun getResult() :String {
            return RetrofitClient.getInstance.api.getString("eb7d9ea45c80b366bfeefddcd491c39d")
        }
    }
    

    因为需要用到java1.8,所以在modle的gradle中配置一下

    compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
        kotlinOptions{
            jvmTarget = "1.8"
        }
    

    然后加入kotlin版的jetpack组件依赖

        implementation 'androidx.activity:activity-ktx:1.1.0'
        implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01'
        implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01'
        implementation 'androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.0-beta01'
        implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01'
    

    相关文章

      网友评论

          本文标题:Android kotlin 协程笔记(一)

          本文链接:https://www.haomeiwen.com/subject/ztvxyltx.html