ViewModel 在 Activity 中的实现原理
系统源码参考自 Android API 30 Platform,以及 androidx.lifecycle:lifecycle-viewmodel:2.2.0
在文章开始之前,有如下几点疑惑,先记录下来:
ViewModel#onCleared()
的调用时机 ?- ViewModel 与
onSaveInstanceState()
有联系吗?或者有什么区别吗? - ViewModel 是否涉及序列化与反序列化?
- 一个 Activity 中是否支持多个 ViewModel ?
ViewModelProvider#get()
的使用时机。
1. 与 ViewModel
相关的类
1.1 ViewModelStore
和 ViewModelStoreOwner
ViewModel
实现的关键就是 ViewModelStore
,其内部有一个 HashMap<String, ViewModel>
用于存储多个 ViewModel
实例,即本质上就是一个用来维护多个 ViewModel
实例的 map
容器。
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);}...
}
另外,还涉及到 ViewModelStoreOwner
接口。
public interface ViewModelStoreOwner {@NonNullViewModelStore getViewModelStore();
}
1.2 androidx.activity.ComponentActivity
androidx.activity.ComponentActivity
(源码基于 androidx.activity:activity:1.1.0
),就是我们常用的 androidx.appcompat.app.AppCompatActivity
的父类。
androidx.activity.ComponentActivity
继承自 androidx.core.app.ComponentActivity
,且实现了 ViewModelStoreOwner
接口,同时内部维护了对应的成员变量 mViewModelStore
(ViewModelStore
类型的)。
// androidx.activity.ComponentActivity@NonNull
@Override
public ViewModelStore getViewModelStore() {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 为 ComponentActivity 中的内部静态类NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();if (nc != null) {// Restore the ViewModelStore from NonConfigurationInstancesmViewModelStore = nc.viewModelStore;}// 如果前面都没有获取到 ViewModelStore 实例,则这里直接实例化一个新的if (mViewModelStore == null) {mViewModelStore = new ViewModelStore();}}return mViewModelStore;
}static final class NonConfigurationInstances {Object custom;ViewModelStore viewModelStore;
}
而 getLastNonConfigurationInstance()
是 android.app.Activity
的成员方法。
// android.app.Activity/*** Retrieve the non-configuration instance data that was previously* returned by {@link #onRetainNonConfigurationInstance()}. This will* be available from the initial {@link #onCreate} and* {@link #onStart} calls to the new instance, allowing you to extract* any useful dynamic state from the previous instance.** <p>Note that the data you retrieve here should <em>only</em> be used* as an optimization for handling configuration changes. You should always* be able to handle getting a null pointer back, and an activity must* still be able to restore itself to its previous state (through the* normal {@link #onSaveInstanceState(Bundle)} mechanism) even if this* function returns null.** <p><strong>Note:</strong> For most cases you should use the {@link Fragment} API* {@link Fragment#setRetainInstance(boolean)} instead; this is also* available on older platforms through the Android support libraries.** @return the object previously returned by {@link #onRetainNonConfigurationInstance()}*/
@Nullable
public Object getLastNonConfigurationInstance() {// mLastNonConfigurationInstances 则是在 Activity 中定义的内部静态类 ComponentActivity.NonConfigurationInstances 类型return mLastNonConfigurationInstances != null? mLastNonConfigurationInstances.activity : null;
}static final class NonConfigurationInstances {Object activity;HashMap<String, Object> children;FragmentManagerNonConfig fragments;ArrayMap<String, LoaderManager> loaders;VoiceInteractor voiceInteractor;
}
在 android.app.Activity
内部维护着成员变量 mLastNonConfigurationInstances
,其是在 Activity#attach()
中被赋值的,其中 lastNonConfigurationInstances
则是从前一个被销毁的 Activity 传递过来的(具体就在第 2 部分展开讲)。
而 getLastNonConfigurationInstance()
的作用就是为了从先前的 Activity 实例获取维护的动态的状态,即通过 mLastNonConfigurationInstances
实现,且在初始的 onCreate()
、onStart()
中可用(会在Activity#performResume()
中被置为 null)。另外,与 getLastNonConfigurationInstance()
对应的是 onRetainNonConfigurationInstance()
(后面会说到)。
// android.app.Activityfinal void attach(..., NonConfigurationInstances lastNonConfigurationInstances, ...) {...mLastNonConfigurationInstances = lastNonConfigurationInstances;...
}
从上述一连串逻辑可以知道,androidx.activity.ComponentActivity#mViewModelStore
本质上对应着 android.app.Activity#mLastNonConfigurationInstances.activity.viewModelStore
。
2. 在 Activity 的实现原理
接下来要讲的是 ViewModel 在 Activity 的原理,其实也对应着 android.app.Activity#mLastNonConfigurationInstances.activity
在 Activity 销毁重建之间的传递逻辑。
当系统配置发生变更(如切换语言等)、横竖屏切换等导致 Activity 销毁重建时,此时会有如下调用逻辑:
2.1 ActivityThread#handleRelaunchActivityInner()
上述流程是在应用进程中进行的,主要分为两个部分,原有 Activity 的销毁,以及新 Activity 的重建,相关逻辑集中在 ActivityThread#handleRelaunchActivityInner()
中,
// ActivityThread/*** @param r 需要被销毁的 activity 对应的 ActivityClientRecord 实例*/
private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,PendingTransactionActions pendingActions, boolean startsNotResumed,Configuration overrideConfig, String reason) {...// 第四个参数即为最终在 performDestroyActivity() 中用到的 getNonConfigInstance,且为 true// 变量 r 最终传递到了 performDestroyActivity() 中,并且 r.lastNonConfigurationInstances 被赋值了handleDestroyActivity(r.token, false, configChanges, true, reason);...// destory 上一个 activity 之后,然后 launch 目标 activity,// 从而将之前 Activity 的 retainNonConfigurationInstances() 传递到目标 ActivityhandleLaunchActivity(r, pendingActions, customIntent);
}
2.2 ActivityThread#handleDestroyActivity()
@Override
public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges,boolean getNonConfigInstance, String reason) {// 进一步调用 ActivityThread#performDestroyActivity(),且 getNonConfigInstance 从 handleRelaunchActivityInner() 传入,且恒为 trueActivityClientRecord r = performDestroyActivity(token, finishing, configChanges, getNonConfigInstance, reason);...
}ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,int configChanges, boolean getNonConfigInstance, String reason) {// 根据 token 获取到在应用进程维护的对应的 ActivityClientRecord 实例 r,且与需要被销毁的 activity 存在关联,即 r.activityActivityClientRecord r = mActivities.get(token);Class<? extends Activity> activityClass = null;if (r != null) {activityClass = r.activity.getClass();r.activity.mConfigChangeFlags |= configChanges;if (finishing) {r.activity.mFinished = true;}// Calls Activity#onPause(), "destroy" is a param for "reason"performPauseActivityIfNeeded(r, "destroy");if (!r.stopped) {// Calls Activity#onStop() and Activity#onSaveInstanceState(Bundle), and updates the client record's state.callActivityOnStop(r, false /* saveState */, "destroy");}// 在上游调用方(handleRelaunchActivityInner())中被设置为 trueif (getNonConfigInstance) {try {// 调用需要被销毁的 activity.retainNonConfigurationInstances(),从而获取到需要被保留的 ViewModelStore,// 且先临时赋值给 r.lastNonConfigurationInstancesr.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();} catch (Exception e) {...}}try {r.activity.mCalled = false;// Calls Activity#onDestory()mInstrumentation.callActivityOnDestroy(r.activity);...} catch (SuperNotCalledException e) {...}r.setState(ON_DESTROY);}...return r;
}
需要注意的是,performDestroyActivity()
并不是单纯的处理 Activity#onDestory()
的,而是为了 destory activity 处理一系列逻辑。
其中,需要关注的是 r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances()
,r.activity
即需要被销毁的 Activity
实例。
// android.app.ActivityNonConfigurationInstances retainNonConfigurationInstances() {// onRetainNonConfigurationInstance() 会在 androidx.activity.ComponentActivity 被重写Object activity = onRetainNonConfigurationInstance();...NonConfigurationInstances nci = new NonConfigurationInstances();nci.activity = activity;...return nci;
}
// androidx.activity.ComponentActivity/*** Called by the system, as part of destroying an* activity due to a configuration change, when it is known that a new* instance will immediately be created for the new configuration. You* can return any object you like here, including the activity instance* itself, which can later be retrieved by calling* {@link #getLastNonConfigurationInstance()} in the new activity* instance.*/
@Override
public final Object onRetainNonConfigurationInstance() {// mViewModelStore 会在 getViewModelStore() 的时候被赋值或者初始化ViewModelStore viewModelStore = mViewModelStore;// 如果 viewModelStore 为 null,则表示 getViewModelStore() 从没有被调用过,即上层使用者在当前需要被销毁的 Activity 中// 没有实例化过 ViewModelProvider,即进一步表明没有创建过 ViewModel。// 但是反过来,没有创建过 ViewModel,getViewModelStore() 也有可能会被调用(具体见第 4 节问题(5))if (viewModelStore == null) {// No one called getViewModelStore(), so see if there was an existing// ViewModelStore from our last NonConfigurationInstanceNonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();// 尝试经由 getLastNonConfigurationInstance() 看是否之前有 viewModelStoreif (nc != null) {viewModelStore = nc.viewModelStore;}}if (viewModelStore == null && custom == null) {return null;}// 走到此处时,viewModelStore 如果还不为 null,则表示之前历代被销毁的 Activity(也包含当前将要被销毁的 Activity) 有维护着一个 viewModelStore 实例,// 因此在当前 Activity 被销毁的时候,还需要将其传承下去(即传递到下一个将被重建的 Activity)NonConfigurationInstances nci = new NonConfigurationInstances();...nci.viewModelStore = viewModelStore;return nci;
}
在 androidx.activity.ComponentActivity#onRetainNonConfigurationInstance()
中,主要是为了得到当前需要被销毁的 Activity 维护的 mViewModelStore
,如果为 null,则进一步通过父类方法 android.app.Activity#getLastNonConfigurationInstance()
去获取 android.app.Activity#mLastNonConfigurationInstances.activity
。
而 android.app.Activity#mLastNonConfigurationInstances
在第 1 部分有讲,是从上一次被销毁的 Activity 传递过来的,而 mLastNonConfigurationInstances.activity
即对应着上一次被销毁的 Activity 维护的 mViewModelStore
。
因此,androidx.activity.ComponentActivity#onRetainNonConfigurationInstance()
关于 ViewModel 实现的意义,简单的说就是要拿到之前历代被销毁的 Activity(也包含当前将要被销毁的 Activity)维护的且一直传递着的 viewModelStore 实例,使其接着继续传递到下一个被重建的 Activity 中。
然后回到 performDestroyActivity()
的
r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances()
该处逻辑的作用就是得到 android.app.Activity.NonConfigurationInstances
实例对象(而该对象会间接(NonConfigurationInstances#activity.viewModelStore
)持有着之前历代被销毁的 Activity(也包含当前将要被销毁的 Activity)维护的一直传递着的 viewModelStore 实例),并将其赋值到给 r.lastNonConfigurationInstances
。
小节
ActivityThread#handleDestroyActivity()
的作用就是为了处理要被销毁的 Activity 的,以及在销毁之前,获取到之前历代被销毁的 Activity(也包含当前将要被销毁的 Activity)维护的一直传递着的 viewModelStore 实例。
2.2 ActivityThread#handleLaunchActivity()
@Override
public Activity handleLaunchActivity(ActivityClientRecord r, PendingTransactionActions pendingActions, Intent customIntent) {...// 进一步调用 performLaunchActivity(),并把 r 传递进去final Activity a = performLaunchActivity(r, customIntent);...return a;
}
可以看到,handleLaunchActivity()
会进一步调用 performLaunchActivity()
,关键的就是参数 r
,其是最开始是从 handleRelaunchActivityInner()
中传递过来的,且在 handleLaunchActivity()
之前,r
被传入 performDestroyActivity()
中,实现了 r.lastNonConfigurationInstances
持有要被传递到重建 Activity 中的 viewModelStore 实例。
/** Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {...Activity activity = null;try {// 创建新的 Activity 实例java.lang.ClassLoader cl = appContext.getClassLoader();activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);...} catch (Exception e) {...}try {...if (activity != null) {...activity.attach(appContext, this, getInstrumentation(), r.token,r.ident, app, r.intent, r.activityInfo, title, r.parent,r.embeddedID, r.lastNonConfigurationInstances, config,r.referrer, r.voiceInteractor, window, r.configCallback,r.assistToken);if (customIntent != null) {activity.mIntent = customIntent;}// 清除 old activity 对应的 r.lastNonConfigurationInstances 的引用r.lastNonConfigurationInstances = null;...} catch (Exception e) {...}return activity;
}
接着是 performLaunchActivity()
,内部处理关于重建 Activity 的逻辑,如创建新的 Activity 实例等,其中,关键的是,会调用新的 Activity 实例的 attach()
,并传入 r.lastNonConfigurationInstances
,从而实现将之前的 viewModelStore 实例传入到新的 Activity 之中(与之前说的 Activity#attach()
相呼应)。
小节
ActivityThread#handleLaunchActivity()
的作用则是处理新 Activity 的重建流程,并把之前维护的 viewModelStore 实例传递到新 Activity 中。
至此, ViewModel 在 Activity 中的实现的关键就是这些了。这一系列的流程,都是在应用进程中处理的,这其实就能说明 ViewModel 的实现的本质,其实就是对象在内存中保留与传递。
3. ViewModel#onCleared()
调用时机
3.1 Old ViewModel 被覆盖时
在使用 ViewModelProvider#get()
获取 ViewModel 实例的时候,最终都会走到如下方法:
// ViewModelProvider.java@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {ViewModel viewModel = mViewModelStore.get(key);// tag 1if (modelClass.isInstance(viewModel)) {if (mFactory instanceof OnRequeryFactory) {((OnRequeryFactory) mFactory).onRequery(viewModel);}return (T) viewModel;} else {//noinspection StatementWithEmptyBodyif (viewModel != null) {// TODO: log a warning.}}if (mFactory instanceof KeyedFactory) {viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);} else {viewModel = (mFactory).create(modelClass);}mViewModelStore.put(key, viewModel);return (T) viewModel;
}
其中,方法参数 key,其取值会有两种,第一种是由用户自定义的,即直接调用 ViewModelProvider#get(key, modelClass)
,此时 key
完全由用户自己控制。还有一种是调用 ViewModelProvider#get(modelClass)
,此时 key
为默认的,是与 modelClass
的 class name 相关的关键字。
private static final String DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"默认的 key = DEFAULT_KEY + ":" + modelClass.getCanonicalName()
因此对于用户自定义 key 的情况,此时就需要通过 tag 1 处的逻辑,来判断重复调用 ViewModelProvider#get(key, modelClass2)
且 key 与之前的值重复时,通过重复的 key 获取到的 ViewModel1 实例类型是否与 modelClass2 对应,如果不对应的话,则表示要基于 modelClass2 来生成一个新的 ViewModel2 实例保存起来,且把老的 ViewModel1 在 mViewModelStore 中覆盖掉(通过调用 mViewModelStore.put()
),然后回调老的 ViewModel1.onCleared()
。
// ViewModelStore.javafinal void put(String key, ViewModel viewModel) {ViewModel oldViewModel = mMap.put(key, viewModel);if (oldViewModel != null) {oldViewModel.onCleared();}
}
需要注意,Class#isInstance()
的作用与 instanceof(Java) 是等价的,只不过 instanceof 是关键字,isInstance()
是 Class 的成员方法。因此对于通过相同的 key 来获取 ViewModel 实例时,如果前(子类)后(父类)的 modelClass 存在继承关系时,则会后面的父类 ViewModel 不会被实例化。
open class FatherViewModel: ViewModel()class SubViewModel: FatherViewModel()// 同时调用下面两行的逻辑,都使用相同的 key
ViewModelProvider(owner).get("test_key", SubViewModel::class.java) // line 1
ViewModelProvider(owner).get("test_key", FatherViewModel::class.java) // line 2
在 line1 会先生成 SubViewModel 实例对象 model1 并存到 mViewModelStore
中,然后在 line 2 的时候,因为都是用的 test_key
,因此从 mViewModelStore 中获取到 model1 之后,再判断 FatherViewModel::class.java.isInstance(model1)
,此时条件成立,则会直接返回 model1。
3.1 androidx.activity.ComponentActivity
正常 destroy 时
在 androidx.activity.ComponentActivity
的无参构造方法会通过 lifecycle 回调监听 ON_DESTROY
事件,在判断到 Activity 是正常被销毁时,而不是因为配置发生改变去销毁 Activity 而 ON_DESTROY
时,会正常调用 ViewModelStore#clear()
。
// androidx.activity.ComponentActivity.javapublic ComponentActivity() {Lifecycle lifecycle = getLifecycle();...getLifecycle().addObserver(new LifecycleEventObserver() {@Overridepublic void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {if (event == Lifecycle.Event.ON_DESTROY) {if (!isChangingConfigurations()) {getViewModelStore().clear();}}}});...
}
// ViewModelStore.java
public final void clear() {for (ViewModel vm : mMap.values()) {// ViewModel#clear() 内部会调用 ViewModel#onCleared()vm.clear();}mMap.clear();
}
4. 问题的解答
在文章最前面留了几个小问题,这里统一的处理下。
(1)ViewModel#onCleared()
的调用时机 ?
这个参考上文的第 3 节即可。
(2)ViewModel 与 onSaveInstanceState()
有联系吗?或者有什么区别吗?
可以参考 getLastNonConfigurationInstance() 的注释:
Note that the data you retrieve here should only be used as an optimization for handling
configuration changes. You should always be able to handle getting a null pointer back,
and an activity must still be able to restore itself to its previous state (through the
normal onSaveInstanceState(Bundle) mechanism) even if this function returns null.两者的相同之处,都是为了保留数据。但是使用场景不一样:- ViewModel 是为了在系统配置改变导致 Activity 销毁重建时维护保留数据用的,数据是维护在应用进程内存中的,即动态的;- onSaveInstanceState(outState: Bundle) 则是为了应对 Activity 被切换到后台时可能会被系统回收的情况,涉及到应用进程的销毁重建,会涉及跨进程通信。因此对于 ViewModel 中维护的数据,也需要在 onSaveInstanceState() 时把必要的数据维护到 outState 中,避免在
切换到后台时被系统回收而导致应用进程重建时数据的丢失。
(3)ViewModel 是否涉及序列化与反序列化?
显然,是不涉及的,数据不涉及跨进程通信,都是直接在当前应用进程的内存中传递。
(4)一个 Activity 中是否支持多个 ViewModel ?
准确的说,是支持多个 key 不一致的 ViewModel 对象,因为 ViewModelStore 的本质是一个维护 Key-ViewModel 的
map 容器。
(5)ViewModelProvider#get()
的使用时机。
显而易见,最早的调用时机是要在 Activity#attach() 之后,这个在
androidx.activity.ComponentActivity#getViewModelStore() 中也有提到:Your activity is not yet attached to the Application instance.
You can't request ViewModel before onCreate call.那是不是一定要在 onCreate()、onStart() 中调用呢?在 onResume() 中是否可以呢?毕竟在 1.2 小节提到了
android.app.Activity.getLastNonConfigurationInstance() 与 mLastNonConfigurationInstances,
以及 mLastNonConfigurationInstances 的有效周期是在初始的 onCreate()、onStart(),之后会在
Activity#performResume() 中被置为 null。先说答案,ViewModelProvider#get() 在 onResume() 也能够正常使用。这是因为 androidx.activity.ComponentActivity#getViewModelStore() 在 onCreate() 之前就会被
系统调用到(具体调用时机有待探究,但是可以通过重写 getViewModelStore() 来证明 ),因此对于借助
mLastNonConfigurationInstances 的 ViewModelSrtore,并不会受到其会在 Activity#performResume()
中被置为 null 的影响。
ViewModel 在 Activity 中的实现原理相关推荐
- activity中fragment 返回键不退出_优雅地处理加载中(loading),重试(retry)和无数据(empty)等...
LoadSir是一个高效易用,低碳环保,扩展性良好的加载反馈页管理框架,在加载网络或其他数据时候,根据需求切换状态页面,可添加自定义状态页面,如加载中,加载失败,无数据,网络超时,占位图,登录失效等常 ...
- 多activity中退出整个程序
问题 多activity中退出整个程序,例如从A->B->C->D,这时我需要从D直接退出程序. 网上资料 finish()和system(0)都只能退出单个activity.杀进程 ...
- Android 中的dm-verity原理分析
[-] Android 中的Verified Boot之dm-verity 相关原理 为什么要使用dm-verity Dm-verity的工作流程 Dm-verity的实现 接口 Deveice Ma ...
- Android筑基——Activity的启动过程之同进程在一个Activity中启动另一个Activity(基于api21)
目录 1. 前言 2. 正文 2.1 Activity类的startActivity()方法 2.2 Instrumentation类的execStartActivity()方法 2.3 Activi ...
- 点击事件如何传递到Activity中
1.首先,当我们触摸屏幕时,通过Android消息机制,从Looper从MessageQueue中取出该事件,发送给WindowInputEventReceiver. 2.WindowInputEve ...
- android自定义view获取控件,android 自定义控件View在Activity中使用findByViewId得到结果为null...
转载:http://blog.csdn.net/xiabing082/article/details/48781489 1. 大家常常自定义view,,然后在xml 中添加该view 组件..如果在 ...
- Activity中KeyEvent的传递
2019独角兽企业重金招聘Python工程师标准>>> 我们先来写个测试应用,主要文件如下: MainActivity.java package com.test.keyevent; ...
- 在Activity中响应ListView内部按钮的点击事件的两种方法
转载:http://www.cnblogs.com/ivan-xu/p/4124967.html 最近交流群里面有人问到一个问题:如何在Activity中响应ListView内部按钮的点击事件,不要在 ...
- Android Application中的Context和Activity中的Context的异同
一.Context是什么: 1.Context是维持Android程序中各组件能够正常工作的一个核心功能类,我们选中Context类 ,按下快捷键F4,右边就会出现一个Context类的继承结构图啦, ...
最新文章
- jstatd,VisualVM使用和报错解决:Could not create remote object--java.security.AccessControlException
- trace--求矩阵的迹
- jQuery.protoype.xxx=function(){}
- php nsdata,iOS开发之数据存储之NSData
- cuda nvcc版本不一致_windows 验证CUDA和CUDNN是否安装成功
- WordPress 3.0十大看点 CMS功能进一步增强
- 浅谈中国市场带来的问题
- ALC--软件定义架构的PLC
- 小程序识别车牌php,微信小程序——车牌键盘输入js+css
- shell test
- 企业云存储 | 为什么越来越多的NAS用户转向企业云盘?
- PPAPI开发之路(四)PPAPI开发环境配置到第一个例子整理(详细总结整理,之前遇到的一些问题解决)
- qq令牌64位密钥提取_qq令牌绑定工具无需密码2020
- 【冷门快捷键】设置VSCode终端大小最小化快捷键Alt+PageUp/PageDown、编辑代码窗口切换大小快捷键Alt+数字键盘“+”、Alt+数字键盘“-”、Alt+数字键盘“0”
- 理解析取范式及合取范式的意义
- 苹果手机应用分身_云手机应用多开app推荐 好用的多开分身软件
- 鸡和兔放在一起,一共有20个头和56只脚,问鸡和兔各几只?
- Base64与Gzip编解码插件
- 3698: XWW的难题[有源汇上下界最大流]
- R语言-2*2卡方检验与效应量