系统源码参考自 Android API 30 Platform,以及 androidx.lifecycle:lifecycle-viewmodel:2.2.0


在文章开始之前,有如下几点疑惑,先记录下来:

  1. ViewModel#onCleared() 的调用时机 ?
  2. ViewModel 与 onSaveInstanceState() 有联系吗?或者有什么区别吗?
  3. ViewModel 是否涉及序列化与反序列化?
  4. 一个 Activity 中是否支持多个 ViewModel ?
  5. ViewModelProvider#get() 的使用时机。

1. 与 ViewModel 相关的类

1.1 ViewModelStoreViewModelStoreOwner

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 接口,同时内部维护了对应的成员变量 mViewModelStoreViewModelStore 类型的)。

// 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 中的实现原理相关推荐

  1. activity中fragment 返回键不退出_优雅地处理加载中(loading),重试(retry)和无数据(empty)等...

    LoadSir是一个高效易用,低碳环保,扩展性良好的加载反馈页管理框架,在加载网络或其他数据时候,根据需求切换状态页面,可添加自定义状态页面,如加载中,加载失败,无数据,网络超时,占位图,登录失效等常 ...

  2. 多activity中退出整个程序

    问题 多activity中退出整个程序,例如从A->B->C->D,这时我需要从D直接退出程序. 网上资料 finish()和system(0)都只能退出单个activity.杀进程 ...

  3. Android 中的dm-verity原理分析

    [-] Android 中的Verified Boot之dm-verity 相关原理 为什么要使用dm-verity Dm-verity的工作流程 Dm-verity的实现 接口 Deveice Ma ...

  4. Android筑基——Activity的启动过程之同进程在一个Activity中启动另一个Activity(基于api21)

    目录 1. 前言 2. 正文 2.1 Activity类的startActivity()方法 2.2 Instrumentation类的execStartActivity()方法 2.3 Activi ...

  5. 点击事件如何传递到Activity中

    1.首先,当我们触摸屏幕时,通过Android消息机制,从Looper从MessageQueue中取出该事件,发送给WindowInputEventReceiver. 2.WindowInputEve ...

  6. android自定义view获取控件,android 自定义控件View在Activity中使用findByViewId得到结果为null...

    转载:http://blog.csdn.net/xiabing082/article/details/48781489 1.  大家常常自定义view,,然后在xml 中添加该view 组件..如果在 ...

  7. Activity中KeyEvent的传递

    2019独角兽企业重金招聘Python工程师标准>>> 我们先来写个测试应用,主要文件如下: MainActivity.java package com.test.keyevent; ...

  8. 在Activity中响应ListView内部按钮的点击事件的两种方法

    转载:http://www.cnblogs.com/ivan-xu/p/4124967.html 最近交流群里面有人问到一个问题:如何在Activity中响应ListView内部按钮的点击事件,不要在 ...

  9. Android Application中的Context和Activity中的Context的异同

    一.Context是什么: 1.Context是维持Android程序中各组件能够正常工作的一个核心功能类,我们选中Context类 ,按下快捷键F4,右边就会出现一个Context类的继承结构图啦, ...

最新文章

  1. jstatd,VisualVM使用和报错解决:Could not create remote object--java.security.AccessControlException
  2. trace--求矩阵的迹
  3. jQuery.protoype.xxx=function(){}
  4. php nsdata,iOS开发之数据存储之NSData
  5. cuda nvcc版本不一致_windows 验证CUDA和CUDNN是否安装成功
  6. WordPress 3.0十大看点 CMS功能进一步增强
  7. 浅谈中国市场带来的问题
  8. ALC--软件定义架构的PLC
  9. 小程序识别车牌php,微信小程序——车牌键盘输入js+css
  10. shell test
  11. 企业云存储 | 为什么越来越多的NAS用户转向企业云盘?
  12. PPAPI开发之路(四)PPAPI开发环境配置到第一个例子整理(详细总结整理,之前遇到的一些问题解决)
  13. qq令牌64位密钥提取_qq令牌绑定工具无需密码2020
  14. 【冷门快捷键】设置VSCode终端大小最小化快捷键Alt+PageUp/PageDown、编辑代码窗口切换大小快捷键Alt+数字键盘“+”、Alt+数字键盘“-”、Alt+数字键盘“0”
  15. 理解析取范式及合取范式的意义
  16. 苹果手机应用分身_云手机应用多开app推荐 好用的多开分身软件
  17. 鸡和兔放在一起,一共有20个头和56只脚,问鸡和兔各几只?
  18. Base64与Gzip编解码插件
  19. 3698: XWW的难题[有源汇上下界最大流]
  20. R语言-2*2卡方检验与效应量

热门文章

  1. java匿名类的替代使用方法
  2. aircrack加reaver破解带有wps的wifi
  3. 唐僧为什么能领导孙悟空
  4. 想变身“科技型”企业?掌汇云数字化服务平台为工业升级加分
  5. 银行ATM系统——顺序图及文档
  6. glibc下malloc与free的实现原理(三):free函数的实现
  7. 创意Game交互可用性设计——操作、版面
  8. 【儿童文学论文】王立春诗集《骑扁马的扁人》的主题分析(节选)
  9. 泰国TISI标志LOGO
  10. Trax直播预告:数字化赋能零售企业终端管理升级