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系列文章