kotlin協程在Android中的使用(Jetpack中的協程和Retrofit中的協程)

點擊上方「Android開發之旅,馬上關注


從 kotlin1.3之後協程api 穩定了,協程的優點網上說了一大堆,確實很方便,好用。

在 Android 中使用kotlin 協程,真爽!


  • kotlin 現在是 Android 開發第一語言,使用協程當然也是可以的。

  • kotlin協程官方也提供 Android 方面的 api 方便 Android 使用(MainScope,本文並不打算介紹這個)。

  •  Android 的 Jetpack 架構組件也開始出現配合使用 kotlin 協程的 api。

  • 現在很流行的網絡框架Retrofit也開始支持 kotlin 協程了。


以上幾條可以說明kotlin 協程你值得擁有。


1、Android Jetpack 中使用 kotlin 協程

在ViewModel 中使用ViewModelScope, 對應依賴 androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 及以上。

class MyViewModel: ViewModel() {    fun  test() {        viewModelScope.launch {            //協程代碼        }    }}


直接在Activity 或 Fragment 中使用LifecycleScope, 對應依賴 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 及以上。

class MyFragment: Fragment() {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        lifecycleScope.launch {          //協程代碼        }    }}

還可以使用liveData, 對應依賴androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01` 及以上。

val user: LiveData<User> = liveData {    //協程代碼    val data = database.loadUser()     emit(data)}

room 對協程的支持,在版本2.1及以上,對應依賴androidx.room:room-ktx:$room_version。

@Insertsuspend fun add(user: User): Long


wworkmanager對協程的支持,對應依賴implementation "androidx.work:work-runtime-ktx:$work_version"。

class MyWorker(context: Context, workerParams: WorkerParameters) :    CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = coroutineScope { //協程代碼 Result.success() }}


2、在Retofit 使用kotlin協程

retofit 在2.6.0版本(在寫此博客的時候,retofit 已經更新至2.6.2版本了)添加suspend函數的支持,使用方式如下:
    @GET("user/{userId}")    suspend fun getUserInfo(@Path("userId") userI: String): User

在函數前加上 suspend 函數直接返回你需要對象類型不需要返回Call對象。


3、kotlin協程+retrofit+ViewModel+LiveData的組合使用

本例中使用的是鴻洋大神玩Android中的api 。

ApiService.kt

interface ApiService {    @GET("article/list/{page}/json")    suspend fun getArticleList(@Path("page") page: Int = 0): Result<PageEntity<Article>>}


Result.kt

data class Result<T>(val code: Int, val errorMsg: String?, val data: T)


PageEntity.kt

data class PageEntity<T>(    val curPage:Int,    val offset:Int,    val over:Boolean,    val size:Int,    val PageCount:Int,    val total:Int,    val datas:List<T>)


Article.kt

data class Article(     @SerializedName("desc")    val desc: String,    @SerializedName("id")    val id: Int,    @SerializedName("title")    val title: String,    //…… )


ArticleViewModel.kt

class ArticleViewModel : ViewModel() {
private val _articleListData = MutableLiveData<List<Article>>() //保證外部只能觀察此數據,不同通過setValue修改 val articleListData: LiveData<List<Article>> = _articleListData
private val _errorMsg = MutableLiveData<String?>() val errorMsg: LiveData<String?> = _errorMsg
fun fetchArticleList(page: Int) { viewModelScope.launch { try { //0️⃣ ⬇️ val result = RetrofitManger.apiService.getArticleList(page) //1️⃣ ⬇️ _articleListData.value = result.data.datas } catch (e: Exception) { _errorMsg.value = e.message } } }}


上面使用viewModelScope.launch啓動協程 ,當activity 或Fragment 銷燬時,ViewModel也隨之銷燬,viewModelScope的協程也會被取消,我們不需要在ViewModel 的onCleared()方法取消(具體原因會在下面分析)。


註釋0️⃣ 處 Retofit發起網絡是在子線程執行的,請求到數據後返回線程又切回到主線程,所有註釋1️⃣已經是主線程了。如果網絡請求出現異常,使用 try catch 捕獲一下就行了。上面代碼看上去就像同步代碼一下,這就是kotlin協程的魅力。


ArticleActivity.kt

class ArticleActivity : AppCompatActivity() {    private val adapter by lazy { ArticleAdapter() }    private val viewModel: ArticleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_view_model)
recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) recyclerView.adapter = adapter //觀察文章列表數據 viewModel.articleListData.observe(this, Observer { list -> loadProgress.visibility = View.GONE adapter.submitList(list) })
viewModel.errorMsg.observe(this, Observer { if (it != null) { toast(it) } }) btn.setOnClickListener { loadProgress.visibility = View.VISIBLE viewModel.fetchArticleList(1) //請求數據 } }}


具體效果如圖所示:



4、問題分析

(1) viewModelScope的協程是如何隨着Activity 或Fragment的銷燬而取消的?


先看一下viewModelScope怎麼來的:


private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"val ViewModel.viewModelScope: CoroutineScope        get() {          //0️⃣ ⬇️            val scope: CoroutineScope? = this.getTag(JOB_KEY)             if (scope != null) {                return scope            }            //1️⃣ ⬇️            return setTagIfAbsent(JOB_KEY,                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))        }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope { override val coroutineContext: CoroutineContext = context

override fun close() { //2️⃣ ⬇️ coroutineContext.cancel() }}


viewModelScope 是ViewModel 的擴展屬性。ViewModel 中有一個Map對象。


private final Map<String, Object> mBagOfTags = new HashMap<>();


在註釋0️⃣ 處通過ViewModel的getTag 方法,從ViewModel的mBagOfTags(Map)屬性中嘗試獲取CoroutineScope對象,如果爲空,就調用ViewModel 的setTagIfAbsent 方法把CloseableCoroutineScope 對象添加到mBagOfTags中。CloseableCoroutineScope 類實現了Closeable, CoroutineScope兩個接口。


在註釋2️⃣ 中調用了取消協程方法。那調用它的close什麼時候被調用呢?CloseableCoroutineScope對象 被放到了ViewModel 的mBagOfTags 中。


在ViewModel類中找到了這麼一個個方法:


@MainThreadfinal void clear() {    mCleared = true;    // Since clear() is final, this method is still called on mock objects    // and in those cases, mBagOfTags is null. It'll always be empty though    // because setTagIfAbsent and getTag are not final so we can skip    // clearing it    if (mBagOfTags != null) {        synchronized (mBagOfTags) {            for (Object value : mBagOfTags.values()) {                // see comment for the similar call in setTagIfAbsent              //0️⃣ ⬇️                closeWithRuntimeException(value);            }        }    }    onCleared();}

private static void closeWithRuntimeException(Object obj) { if (obj instanceof Closeable) { try { ((Closeable) obj).close(); } catch (IOException e) { throw new RuntimeException(e); } } }


從上面可以看出在clear方法中循環遍歷mBagOfTags,在註釋0️⃣處執行closeWithRuntimeException() 在closeWithRuntimeException中調用close()方法,所以CloseableCoroutineScope對象中的close是在這裏調用的,那clear()方法 又是在哪裏調用的呢。那這個就要說說ViewModel 的與Activity 或Fragment 的關係了。下面說一說ViewModel和Activity的關係。創建ViewModel 對象是通過ViewModelProvider的get()方法創建的,ViewModelProvider對象創建需要兩個參數是(@NonNull ViewModelStore store, @NonNull Factory factory)。


第一個參數ViewModelStore 是通過androidx.activity.ComponentActivity的getViewModelStore() 方法或者 androidx.fragment.app.Fragment的getViewModelStore()方法獲取(ComponentActivity是AppCompatActivity是的爺爺類 )。


在 ViewModelProvider 的get方法中 會創建一個ViewModel 對象並存放在ViewModelStore的map中:


public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {   //…… 省略ViewModel常見的代碼
//把ViewModel 對象放到ViewModelStore 中 mViewModelStore.put(key, viewModel); return (T) viewModel;}


ViewModelStore.java



public class ViewModelStore {

private final HashMap<String, ViewModel> mMap = new HashMap<>();

final void put(String key, ViewModel viewModel) { ViewModel oldViewModel = mMap.put(key, viewModel); if (oldViewModel != null) { oldViewModel.onCleared(); } } final ViewModel get(String key) { return mMap.get(key); }
Set<String> keys() { return new HashSet<>(mMap.keySet()); } public final void clear() { for (ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }}


上面就是ViewModelStore類的所有代碼(此代碼版本是Viewmodel 2.2.0-rc01) 可以看到ViewModel 對象被存儲在mMap中,這個類中也有個clear方法,方法中循環遍歷mMap 執行ViewModel的clear() 哦,原來ViewModel 的clear()在這裏調用的。那ViewModelStore 的clear()又是哪裏調用的呢?ViewModelStore 的對象是從Activity 或Fragment,我們應該去他來源看一看,點擊clear()方法,發現在ComponentActivity和FragmentManagerViewModel中有調用此方法,在ComponentActivity中是這樣調用的,在ComponentActivity 的構造方法設置lifecycle 的狀態改變監聽,當處於destroy狀態是就調用ViewModelStore的clear() ,Fragment 是FragmentMager 調用Fragment.performDestroy()方法前調用。


public ComponentActivity() { // 省略部分代碼    getLifecycle().addObserver(new LifecycleEventObserver() {        @Override        public void onStateChanged(@NonNull LifecycleOwner source,                @NonNull Lifecycle.Event event) {            if (event == Lifecycle.Event.ON_DESTROY) {                if (!isChangingConfigurations()) {                    getViewModelStore().clear();                }            }        }    });}



簡單梳理一下上面的流程:


  • ViewModel 有個Map 對象,保存了CloseableCoroutineScope 引用。

  • ViewModel 創建的的時候會被保存在從getViewModelStore()獲取的對象中。

  • 當Activity 或Fragment 銷燬時,ViewModelStore的clear會被執行,此方法就會觸發ViewModel 的clear()方法,在ViewModel 的的clear()方法中觸發CloseableCoroutineScope的close方法。


(2) Retrofit 是怎麼支持協程的?


suspend 是kotlin 關鍵詞,那java 怎麼知道一個方法是suspend修飾的呢?


利用android studio 的show Kotlin Bytecode工具:


//kotlin     @GET("article/list/{page}/json")  suspend fun getArticleList(@Path("page") page:Int): Result<PageEntity<Article>>

//kotlin字節碼 反編譯後的java代碼 @GET("article/list/{page}/json") @Nullable Object getArticleList(@Path("page") int var1, @NotNull Continuation var2);


發現會在原來的參數後面加上一個Continuation var2 參數,Retrofit 是不是就是根據這個參數判斷的呢?


查看Retofit的create方法發現有一個代理,當調用接口方法時就會觸發InvocationHandler的invoke方法。




public <T> T create(final Class<T> service) { Utils.validateServiceInterface(service); if (validateEagerly) { eagerlyValidateMethods(service); } return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { private final Platform platform = Platform.get(); private final Object[] emptyArgs = new Object[0];

@Override public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { // If the method is a method from Object then defer to normal invocation. if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } if (platform.isDefaultMethod(method)) {//接口方法設置默認實現(java8+) return platform.invokeDefaultMethod(method, service, proxy, args); } return loadServiceMethod(method).invoke(args != null ? args : emptyArgs); } });}


方法最後loadServiceMethod(method) 會調用ServiceMethod的parseAnnotations()方法。


ServiceMethod.java



abstract class ServiceMethod<T> { static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) { RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);

Type returnType = method.getGenericReturnType(); if (Utils.hasUnresolvableType(returnType)) { throw methodError(method, "Method return type must not include a type variable or wildcard: %s", returnType); } if (returnType == void.class) { throw methodError(method, "Service methods cannot return void."); }

return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory); }

abstract @Nullable T invoke(Object[] args);}


在創建RequestFactory對象時,會調用RequestFactory的build()方法,在這個方法中有這麼一段代碼 循環解析方法參數。


int parameterCount = parameterAnnotationsArray.length;parameterHandlers = new ParameterHandler<?>[parameterCount];for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {  parameterHandlers[p] =      parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);}


parseParameter方法部分代碼:


private @Nullable ParameterHandler<?> parseParameter(    int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {  // 省略部分代碼……  if (result == null) {    //0️⃣ ⬇️    if (allowContinuation) {      try {        //1️⃣ ⬇️        if (Utils.getRawType(parameterType) == Continuation.class) {          isKotlinSuspendFunction = true;          return null;        }      } catch (NoClassDefFoundError ignored) {      }    }    throw parameterError(method, p, "No Retrofit annotation found.");  }

return result;}


註釋0️⃣處allowContinuation是的值是上面傳入的 p == lastParameter 也就是判斷是否是最後一個參數,在註釋1️⃣處判斷這個參數的類是否是Continuation.class 如果是就把 isKotlinSuspendFunction標記爲 true。看到這個我們也就知道了Retrofit就是根據這個最後一個參數來判斷是否支持kotlin 的 suuspend 函數。


我們再繼續看看ServiceMethod的parseAnnotations的方法吧,在構建RequestFactory對象時 判斷了是否支持kotlin susupend 函數,方法的最後調用的HttpServiceMethod的parseAnnotations 並將返回值返回。


我們再去看看HttpServiceMethod的parseAnnotations 做了些什麼?


static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(    Retrofit retrofit, Method method, RequestFactory requestFactory) {  boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;  boolean continuationWantsResponse = false;  boolean continuationBodyNullable = false;//0️⃣ ⬇️  if (isKotlinSuspendFunction) {    Type[] parameterTypes = method.getGenericParameterTypes();    Type responseType = Utils.getParameterLowerBound(0,        (ParameterizedType) parameterTypes[parameterTypes.length - 1]);    //1️⃣ ⬇️    if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {//2      // Unwrap the actual body type from Response<T>.      responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);      continuationWantsResponse = true;    } else {      // TODO figure out if type is nullable or not      // Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)      // Find the entry for method      // Determine if return type is nullable or not    }

adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType); annotations = SkipCallbackExecutorImpl.ensurePresent(annotations); } else { adapterType = method.getGenericReturnType(); } // 省略部分代碼…… if (!isKotlinSuspendFunction) { return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter); } else if (continuationWantsResponse) { //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object. //2️⃣ ⬇️ return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter); } else { //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object. //3️⃣ ⬇️ return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>(requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter, continuationBodyNullable); }}


在註釋0️⃣處先判斷是否支持kotlin的 suspend ,在註釋1️⃣處又判斷了方法返回值是不是Response類包裹的,如果是就把continuationWantsResponse 標記爲true。在註釋2️⃣和註釋3️⃣ 處根據continuationWantsResponse返回不同的對象。看到這裏。那上面寫ApiService 接口是 suspend 方法的返回值,外面應該可以包裹Response的。


@GET("article/list/{page}/json")suspend fun getArticleList1(@Path("page") page:Int): Response<Result<PageEntity<Article>>>


上面寫法寫法也是可以的,返回的的是Response對象,是否請求成功需要自己寫邏輯判斷。如果返回值外面不包裹Response那麼如果請求不成功,會拋出異常,異常需要我們自己處理。


在上面的例子中我們就是寫的是註釋3️⃣是返回值沒有Response 包裹的情況,下面來分析如果走到註釋3️⃣的情況吧。


SuspendForBody(刪除了源碼中的註釋):


static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {  private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;  private final boolean isNullable;

SuspendForBody(RequestFactory requestFactory, okhttp3.Call.Factory callFactory, Converter<ResponseBody, ResponseT> responseConverter, CallAdapter<ResponseT, Call<ResponseT>> callAdapter, boolean isNullable) { super(requestFactory, callFactory, responseConverter); this.callAdapter = callAdapter; this.isNullable = isNullable; } @Override protected Object adapt(Call<ResponseT> call, Object[] args) { call = callAdapter.adapt(call); Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
try { return isNullable ? KotlinExtensions.awaitNullable(call, continuation) : KotlinExtensions.await(call, continuation); } catch (Exception e) { return KotlinExtensions.yieldAndThrow(e, continuation); } }}


KotlinExtensions 中的方法就是使用的kotlin的suspendCancellableCoroutine 。先讓函數掛起,當回調執行拿到數據時調用 resume 再恢復到之前的協程,就可以直接得到網絡返回數據啦。


suspend fun <T : Any> Call<T>.await(): T {  return suspendCancellableCoroutine { continuation ->    continuation.invokeOnCancellation {      cancel()    }    enqueue(object : Callback<T> {      override fun onResponse(call: Call<T>, response: Response<T>) {        if (response.isSuccessful) {          val body = response.body()          if (body == null) {            //省略部分代碼            continuation.resumeWithException(e)          } else {            continuation.resume(body)          }        } else {          continuation.resumeWithException(HttpException(response))        }      }      override fun onFailure(call: Call<T>, t: Throwable) {        continuation.resumeWithException(t)      }    })  }}


上面SuspendForBody(HttpServiceMethod是他爸爸,ServiceMethodd是他爺爺) 的adapt 是怎麼調用的呢?


我們從頭再看看當接口方法調用是觸發的那個方法吧,在InvocationHandler 的invoke的方法 最後一句是:


return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);


我們上面分析了那麼多我們只是分析了loadServiceMethod(method)方法執行後的一系列實現,這個方法返回值還調用了invoke方法,那loadServiceMethod(method)方法返回值是什麼,就是這個SuspendForBody對象(suspend 函數且函數返回值外面沒有Response包裹時),在HttpServiceMethod中 實現了 invoke方法,並條用了adapt方法:


@Override final @Nullable ReturnT invoke(Object[] args) {  Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);  return adapt(call, args);}


以上就是Retofit協程請求方式的大致過程。


掃描下方二維碼關注公衆號,回覆「協程資料」獲取源碼。


版權聲明:本文爲CSDN博主「Programmer_knight」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。

鏈接:https://blog.csdn.net/knight1996/article/details/102492883






點個贊,證明你還愛我



本文分享自微信公衆號 - Android開發之旅(AndroidDevTour)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章