Kotlin Coroutine在 Android 中的使用和封装

4,205 阅读5分钟

kotlin出来很多年,并被google被赋予Android首选开发语言的地位,还是有必要研究一下的,基础语法不再复述了,只简单聊聊kotlin中的核心内容之一:协程 Coroutine。

相关概念和API可以直接进入官网查询, 不过不推荐kotlin中文网,文章翻译的略显生硬,看了更容易迷惑。

本文不讲基础(基础我也不怎么清楚。。),适合喜欢拿来主义的同学

协程作为轻量级的线程,是建立在线程之上的调度,比线程节省资源但不意味着不消耗资源,在Android这种系统资源相对珍贵的环境中,当异步繁琐的调用或者切换线程,如何及时的回收也是必要的工作。

一、添加依赖配置

coroutine 是在kotlin 1.3中发布的1.0正式版,所以建议优先使用这个版本。

项目根目录中gradle配置:

buildscript {
    ext.kotlin_version = '1.3.0'
    repositories {
        google()
        mavenCentral()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

主module中gradle配置

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
}

// 实验性开启协程的配置,目前版本中已经不需要了
kotlin{             
    experimental{
        coroutines 'enable'
    }
}

假如你的其他module也使用kt代码,也需要添加上述依赖,不然有可能会出现找不到class或符号相关编译错误


二、替换RxJava和升级Retrofit

一般项目中会引用retrofit + RxJava的组合进行网络请求,或者你单独封装了一层RxJava来进行异步操作、切换线程来处理你的业务

RxJava虽然操作符众多(得有100个以上了吧),上手不易,但着实好用,一条链走下来搞定你的全部业务,如果有网络请求配合上retrofit这种支持Rx的库,不要太给力。

不过函数式开发有个通病,这条链子上会创建大量对象,回收内存抖动你不得不考虑,虽然使用了线程池,可开销不容小觑。

RxJava 虽然可以链式调用,但终究还是基于回调形式,而协程完全做到了同步方式写异步代码。

看下以前Retrofit的写法(简易)

object RetrofitHelper: Interceptor {

    init {
        initOkHttpClient();
    }

    private var mOkHttpClient: OkHttpClient? = null

    private val BASE_GANK_URL = "http://gank.io/api/"

    fun getRetroFitBuilder(url :String) : Retrofit {
        return Retrofit.Builder()
                .baseUrl(url)
                .client(mOkHttpClient)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    }

    fun getGankMeiziApi(): GankMeiziAPI {
        return getRetroFitBuilder(BASE_GANK_URL).create(GankMeiziAPI::class.java)
    }

interface GankMeiziAPI {
    @GET("data/福利/{number}/{page}")
    abstract fun getGankMeizi(@Path("number") number: Int, @Path("page") page: Int): Observable<GankMeiziResult>
}

class GankMeiziPresenter : BaseMvpPresenter<IGankMeiziView>() {

    fun getGankList(context: RxAppCompatActivity, pageNum: Int , page :Int){
        RetrofitHelper.getGankMeiziApi()
                .getGankMeizi(pageNum, page)
                .subscribeOn(Schedulers.io())
                .compose(context.bindUntilEvent(ActivityEvent.DESTROY))
                .filter({ gankMeiziResult -> !gankMeiziResult.error })
                .map({ gankMeiziResult -> gankMeiziResult.results })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ gankMeiziInfos ->
                    mvpView?.showSuccess(gankMeiziInfos)
                }, { throwable ->
                    mvpView?.showError()
                })
    }
}

RxJava 的调用链上使用RxLife来绑定生命周期(或者使用Uber的autodispose),很常见的代码。



对于RxJava你还有可能会封装一个小工具(主要是为了生命周期和回收问题)

public class RxImpl {

    public static <T> void exeOnLife(final AppCompatActivity mActivity , Observable<RespBody<XueError, T>> observable, final Accept acceptFun){
        if (mActivity == null || ActivityHelper.activityIsDead(mActivity) || observable == null) return;
        observable.as(AutoDispose.<RespBody<XueError,T>>autoDisposable(AndroidLifecycleScopeProvider.from(mActivity, Lifecycle.Event.ON_DESTROY)))
                .subscribe(new Consumer<RespBody<XueError, T>>() {
                    @Override
                    public void accept(RespBody<XueError, T> respBody) throws Exception {
                        if (acceptFun == null) return;
                        acceptFun.accept(respBody);
                    }
                });
    }

    public interface Accept <E extends XueError,T extends Object> {
        void accept(RespBody<E, T> respBody);
    }
}

现在呢,想用协程怎么办,假设你现在已经知道了如何启动一个协程

GlobalScope.launch { }
        
GlobalScope.async {}        

也知道了launch 和 async的区别(返回值不同),也知道了需要一个返回值和多个返回值的选择(async 和 reduce),那在Android中如何使用呢



三、封装

协程Job具有层级关系,父协程控制子协程的运行,取消父协程的运行,也就相当于关闭了所有你开在父协程里的全部任务,所以在基类中你需要声明一个全局的协程上下文,保证你开启的协程都处于这个context环境中,在onDestroy下取消全局context,就达到了你的目的

abstract class AbstractFragment : RxFragment() , CoroutineScope by MainScope() {

    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

你可以像封装RxJava一样,封装一个工具来使用协程,同时遵循一些原则:

1 调度器

你开启的协程需要有调度器,就像RxJava自由切换线程一样,但主线程调度器最好指定Main来避免不必要的麻烦。

举个例子,你使用RxJava+autodipose来绑定 生命周期,但你在子线程用RxJava又开启了一个新线程,调度回UI线程刷新view的时候会发生崩溃

2 父协程和子协程有强烈的层级关系

cancel一个Job 会抛出 CancellationException ,但这个异常不需要开发人员管理,但如果协程内部发生了非 CancellationException ,则会使用这个异常来取消父Coroutine。为了保证稳定的Coroutine层级关系,这种行为不能被修改。 然后当所有的子Coroutine都终止后,父Coroutine会收到原来抛出的异常信息。


下边是个协程的简易封装案例

    fun <T> executeRequest(context : CoroutineContext, request: suspend () -> T?, onSuccess: (T) -> Unit = {}, onFail: (Throwable) -> Unit = {}): Job {
    return CoroutineScope(Dispatchers.Main).launch(context) {
        try {
            val res: T? = withContext(Dispatchers.IO) { request() }
            res?.let {
                onSuccess(it)
            }
        } catch (e: Exception) {
            e.printStackTrace()
            onFail(e)
        }
    }
}

context 视业务情况而定是否传入

然后在你的act或Fragment中直接调用

class MyFragment : BaseFragment {
    
    onAcitvityCreate(){
        executeRequest<Body>(
            context,
                request = {
                    // 异步任务
                },

                onSuccess = {
                   
                },

                onFail = {
                    
                }
        )
    }
}




对于Retrofit修改更为简单,2.6版本已经支持了协程,网上有很多介绍

interface GankMeiziAPI {
    @GET("data/福利/{number}/{page}")
    suspend fun getGankMeizi(@Path("number") number: Int, @Path("page") page: Int): GankMeiziResult
}

将Observable返回值修改为你需要的bean, 加上suspend标记即可, 下边是示例代码:

return executeRequest<GankMeiziResult>(
               context,
                request = {
                    RetrofitHelper.getGankMeiziApi().getGankMeizi(pageNum, page)
                },

                onSuccess = {
                    mvpView?.showSuccess(it.results)
                },

                onFail = {
                    mvpView?.showError()
                }
        )

四、混淆

网上的帖子大多以demo的思路来写,商用肯定要混淆了,不添加proguard你混淆后就死翘翘了,所以直接贴上来

coroutine的配置

-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}
-keepclassmembernames class kotlinx.** {
    volatile <fields>;
}

假如release打包后你的Retrofit 请求结果发生了npe,检查下你的bean是否添加了不混淆配置



其他

研究协程没多久,有错误或建议欢迎指出,共同交流

另外推荐云在千峰的博客 ,是我目前看过的最友好的coroutine系列文章