kotlin协程中viewModelScope的使用方法和原理解析

9,118 阅读3分钟

废话不说直奔主题

如果不使用viewModelScope,我们的代码是这样的

class MyViewModel : ViewModel() {

    private val viewModelJob = SupervisorJob()
    
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
    
    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel() // Cancel all coroutines
    }
    
    fun launchDataLoad() {
        uiScope.launch {
            sortList()
            // Modify UI
        }
    }
    
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

我们需要在onCleared方法中主动调用viewModelJob.cancel()方法取消该协程和它的所有子协程,如果我们忘记调用viewModelJob.cancel(),就会发生内存泄漏,Android官方给我们提供了一个lib库,帮助我们解决该问题,这样自己省去了写viewModelJob.cancel()

使用viewModelScope的方式如下

...
import androidx.lifecycle.viewModelScope
...

fun demo() {
    viewModelScope.launch(Dispatchers.Main) {
        launch(Dispatchers.IO) {
            val result = App.retrofit.create(WanApi::class.java).getHome()
        }
    }
}

viewModelScope的使用方法很简单,只需要在gradle中添加kotlin的协程lib后额外引入如下lib(写文章时的版本是2.1.0)

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'

然后在页面销毁的时候,viewModel就会在自动调用clear后调用CoroutineContext的cancel方法,这里其实和自己写没有区别,只不过不需要自己管理了

那么原理是什么呢?

首先,看viewModelScope的源码,位置如下

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
        }

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

    override fun close() {
        coroutineContext.cancel()
    }
}

看到没,和我们自己写是一样的,也是CoroutineScope(Dispatchers.Main + viewModelJob),然后我们看setTagIfAbsent方法,这个方法从代码上看就知道在ViewModel中,代码如下

<T> T setTagIfAbsent(String key, T newValue) {
    T previous;
    synchronized (mBagOfTags) {
        previous = (T) mBagOfTags.get(key);
        if (previous == null) {
            mBagOfTags.put(key, newValue);
        }
    }
    T result = previous == null ? newValue : previous;
    if (mCleared) {
        closeWithRuntimeException(result);
    }
    return result;
}

简单的说,就是把CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))存到了mBagOfTags这个HashMap里,然后在viewModel调用clear()方法的时候,这个CoroutineScope就被取消了,代码如下

@MainThread
final void clear() {
    mCleared = true;
    if (mBagOfTags != null) {
        synchronized (mBagOfTags) {
            for (Object value : mBagOfTags.values()) {
                closeWithRuntimeException(value);
            }
        }
    }
    onCleared();//我们之前把CoroutineScope的取消放到onCleared方法里
}

至于clear()是什么时候调用的,流程也很简单,用Android Studio查看一下调用关系就会发现在ViewModelStore类里调用了

final void put(String key, ViewModel viewModel) {
    ViewModel oldViewModel = mMap.put(key, viewModel);
    if (oldViewModel != null) {
        oldViewModel.onCleared();
    }
}
...
public final void clear() {
    for (ViewModel vm : mMap.values()) {
        vm.clear();
    }
    mMap.clear();
}

而这个clear()在哪里调用的呢,继续查看调用关系,会在ComponentActivity的构造函数里找到如下代码(Fragment的我没有找,不过取消方式应该类似,都是页面销毁的时候调用)

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {

        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();//1
                        }
                    }
                }
            });
            ...
        }
            
        public ViewModelStore getViewModelStore() {//2
            if (getApplication() == null) {
                throw new IllegalStateException("Your activity is not yet attached to the "
                        + "Application instance. You can't request ViewModel before onCreate call.");
            }
            if (mViewModelStore == null) {
                NonConfigurationInstances nc =
                        (NonConfigurationInstances) getLastNonConfigurationInstance();
                if (nc != null) {
                    // Restore the ViewModelStore from NonConfigurationInstances
                    mViewModelStore = nc.viewModelStore;
                }
                if (mViewModelStore == null) {
                    mViewModelStore = new ViewModelStore();
                }
            }
            return mViewModelStore;
    }
}

可以看到是在上面的注释1调用的,而注释2的getViewModelStore()方法里的mViewModelStore属性是ComponentActivity的实例属性,这个属性只在getViewModelStore()方法暴露了,所以肯定有类调用了此方法在mViewModelStore实例里存入了viewModel实例,然后查看调用栈,调用的地方也是有好几处,但是有一个调用方是ViewModelProvidersof方法,这个方法就是我们平时获取viewModel实例的地方,代码如下

public static ViewModelProvider of(@NonNull FragmentActivity activity, @Nullable Factory factory) {
    Application application = checkApplication(activity);
    if (factory == null) {
        factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }
    return new ViewModelProvider(activity.getViewModelStore(), factory);
}
...
public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {
    Application application = checkApplication(checkActivity(fragment));
    if (factory == null) {
        factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }
    return new ViewModelProvider(fragment.getViewModelStore(), factory);
}

这也就是ViewModel不应该自己new的原因,而是应该用该ViewModelProviders.of获取,这样就能让ActivityFragment自动管理ViewModel,在页面销毁的时候自动调用ViewModelclear()方法

至此,本文结束,现在我们知道了为什么ViewModelProviders.of获取的viewModel在页面销毁的时候会自动调用clear()方法