九:ViewModel 是什么,说说你所理解的ViewModel?
ViewModel是MVVM架构的一个层级,用来联系View和model之间的关系。而我们今天要说的就是官方出的一个框架——ViewModel。
ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据
官方是这么介绍的,这里面有两个信息:
- 注重生命周期的方式。
由于ViewModel的生命周期是作用于整个Activity的,所以就节省了一些关于状态维护的工作,最明显的就是对于屏幕旋转这种情况,以前对数据进行保存读取,而ViewModel则不需要,他可以自动保留数据。
其次,由于ViewModel在生命周期内会保持局部单例,所以可以更方便Activity的多个Fragment之间通信,因为他们能获取到同一个ViewModel实例,也就是数据状态可以共享了。
- 存储和管理界面相关的数据。
ViewModel层的根本职责,就是负责维护界面上UI的状态,其实就是维护对应的数据,因为数据会最终体现到UI界面上。所以ViewModel层其实就是对界面相关的数据进行管理,存储等操作。
- ViewModel 为什么被设计出来,解决了什么问题?
在ViewModel组件被设计出来之前,MVVM又是怎么实现ViewModel这一层级的呢?
其实就是自己编写类,然后通过接口,内部依赖实现View和数据的双向绑定。所以Google出这个ViewModel组件,无非就是为了规范MVVM架构的实现,并尽量让ViewModel这一层级只触及到业务代码,不去关心VIew层级的引用等。然后配合其他的组件,包括livedata,databindingrang等让MVVM架构更加完善,规范,健硕。
- 解决了什么问题呢?
其实上面已经说过一些了,比如:
1)不会因为屏幕旋转而销毁,减少了维护状态的工作
2)由于在作用域内单一实例的特性,使得多个fragment之间可以方便通信,并且维护同一个数据状态。
3)完善了MVVM架构,使得解耦更加纯粹。
十:说说ViewModel原理。
首先说说是怎么保存生命周期。
ViewModel2.0之前呢,其实原理是在Activity上add一个HolderFragment,然后设置setRetainInstance(true)方法就能让这个Fragment在Activity重建时存活下来,也就保证了ViewModel的状态不会随Activity的状态所改变。
2.0之后,其实是用到了Activity的onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()这两个方法,相当于在横竖屏切的时候会保存ViewModel的实例,然后恢复,所以也就保证了ViewModel的数据。
再说说怎么保证作用域内唯一实例
首先,ViewModel的实例是通过反射获取的,反射的时候带上application的上下文,这样就保证了不会持有Activity或者Fragment等View的引用。
然后实例创建出来会保存到一个ViewModelStore容器里面,其实也就是一个集合类,这个ViewModelStore 类其实就是保存在界面上的那个实例,而我们的ViewModel就是里面的一个集合类的子元素。
所以我们每次获取的时候,首先看看这个集合里面有无我们的ViewModel,如果没有就去实例化,如果有就直接拿到实例使用,这样就保证了唯一实例。最后在界面销毁的时候,会去执行ViewModelStore的clear方法,去清除集合里面的ViewModel数据。
一小段代码说明下:
public <T extends ViewModel> T get(Class<T> modelClass) {
// 先从ViewModelStore容器中去找是否存在ViewModel的实例
ViewModel viewModel = mViewModelStore.get(key);
// 若ViewModel已经存在,就直接返回
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
// 若不存在,再通过反射的方式实例化ViewModel,并存储进ViewModelStore
viewModel = modelClass.getConstructor(Application.class).newInstance(mApplication);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.onCleared();
}
mMap.clear();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mViewModelStore != null && !isChangingConfigurations()) {
mViewModelStore.clear();
}
}
十一:ViewModel怎么实现自动处理生命周期?
为什么在旋转屏幕后不会丢失状态?为什么ViewModel可以跟随Activity/Fragment的生命周期而又不会造成内存泄漏呢?
这三个问题很类似,都是关于生命周期的问题,其实也就是问为什么ViewModel能管理生命周期,并且不会因为重建等情况造成影响。
- ViewModel2.0之前
利用一个无view 的HolderFragment来维持它的生命周期,我们知道ViewModel实例是存储到一个ViewModelStore容器里的,那么这个空的fragment就可以用来管理这个容器,只要Activity处于活动状态,HolderFragment也就不会被销毁,就保证了ViewModel的生命周期。
而且设置setRetainInstance(true)方法可以保证configchange时的生命周期不被改变,让这个Fragment在Activity重建时存活下来。
总结来说就是用一个空的fragment来管理维护ViewModelStore,然后对应的activity销毁的时候就去把viewmodel的映射删除。就让ViewModel的生命周期保持和Activity一样了。这也是很多三方库用到的巧妙方法,比如Glide,也是建立空的Fragment来管理。
- 2.0之后,有了androidx支持
其实是用到了Activity的一个子类ComponentActivity,然后重写了onRetainNonConfigurationInstance()方法保存ViewModelStore,并在需要的时候,也就是重建的Activity中去通过getLastNonConfigurationInstance()方法获取到ViewModelStore实例。这样也就保证了ViewModelStore中的ViewModel不会随Activity的重建而改变。
同时由于实现了LifecycleOwner接口,所以能利用Lifecycles组件组件感知每个页面的生命周期,就可以通过它来订阅当Activity销毁时,且不是因为配置导致的destory情况下,去清除ViewModel,也就是调用ViewModelStore的clear方法。
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// 判断是否因为配置更改导致的destroy
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
这里的onRetainNonConfigurationInstance方法再说下,是会在Activity因为配置改变而被销毁时被调用,跟onSaveInstanceState方法调用时机比较相像,不同的是onSaveInstanceState保存的是Bundle,Bundle是有类型限制和大小限制的,而且需要在主线程进行序列号。而onRetainNonConfigurationInstance方法都没有限制,所以更倾向于用它。
所以,到这里,第三个问题应该也可以回答了,2.0之前呢,都是通过他们创建了一个空的fragment,然后跟随这个fragment的生命周期。
2.0之后呢,是因为不管是Activity或者Fragment,都实现了LifecycleOwner接口,所以ViewModel是可以通过Lifecycles感知到他们的生命周期,从而进行实例管理的。
十二:LiveData 是什么?
LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
官方介绍如下,其实说的比较清楚了,主要作用在两点:
- 数据存储器类。也就是一个用来存储数据的类。
- 可观察。这个数据存储类是可以观察的,也就是比一般的数据存储类多了这么一个功能,对于数据的变动能进行响应。
主要思想就是用到了观察者模式思想,让观察者和被观察者解耦,同时还能感知到数据的变化,所以一般被用到ViewModel中,ViewModel负责触发数据的更新,更新会通知到LiveData,然后LiveData再通知活跃状态的观察者。
var liveData = MutableLiveData<String>()
liveData.observe(this, object : Observer<String> {
override fun onChanged(t: String?) {
}
})
liveData.setVaile("xixi")
//子线程调用
liveData.postValue("test")
十三:LiveData 为什么被设计出来,解决了什么问题?
LiveData作为一种观察者模式设计思想,常常被和Rxjava一起比较,观察者模式的最大好处就是事件发射的上游 和 接收事件的下游 互不干涉,大幅降低了互相持有的依赖关系所带来的强耦合性。
其次,LiveData还能无缝衔接到MVVM架构中,主要体现在其可以感知到Activity等生命周期,这样就带来了很多好处:
- 不会发生内存泄漏 观察者会绑定到 Lifecycle对象,并在其关联的生命周期遭到销毁后进行自我清理。
- 不会因 Activity 停止而导致崩溃 如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。
- 自动判断生命周期并回调方法 如果观察者的生命周期处于 STARTED 或 RESUMED状态,则 LiveData 会认为该观察者处于活跃状态,就会调用onActive方法,否则,如果 LiveData 对象没有任何活跃观察者时,会调用 onInactive()方法。
十四:说说LiveData原理
说到原理,其实就是两个方法:
- 订阅方法, 也就是observe方法。通过该方法把订阅者和被观察者关联起来,形成观察者模式。
简单看看源码:
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
//...
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}
public V putIfAbsent(@NonNull K key, @NonNull V v) {
Entry<K, V> entry = get(key);
if (entry != null) {
return entry.mValue;
}
put(key, v);
return null;
}
这里putIfAbsent方法是将生命周期相关的wrapper和观察者observer作为key和value存到了mObservers中。
- 回调方法,也就是onChanged方法。通过改变存储值,来通知到观察者也就是调用onChanged方法。从改变存储值方法setValue看起:
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}
private void dispatchingValue(@Nullable ObserverWrapper initiator) {
//...
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
这一套下来逻辑还是比较简单的,遍历刚才的map——mObservers,然后找到观察者observer,如果观察者不在活跃状态(活跃状态,也就是可见状态,处于 STARTED 或 RESUMED状态),则直接返回,不去通知。否则正常通知到观察者的onChanged方法。
当然,如果想任何时候都能监听到,都能获取回调,调用observeForever方法即可。
十五:说说DNS,以及存在的问题
之前看过我说的网络问题应该知道DNS用来做域名解析工作的,当输入一个域名后,需要把域名转化为IP地址,这个转换过程就是DNS解析。
但是传统的DSN解析会有一些问题,比如:
- 域名缓存问题 本地做一个缓存,直接返回缓存数据。可能会导致全局负载均衡失败,因为上次进行的缓存,不一定是这次离客户最近的地方,可能会绕远路。
- 域名转发问题 如果是A运营商将解析的请求转发给B运营商,B去权威DNS服务器查询的话,权威服务器会认为你是B运营商的,就返回了B运营商的网站地址,结果每次都会跨运营商。
- 出口NAT(网络地址转换协议)问题 做了网络地址转化后,权威的DNS服务器,没法通过地址来判断客户到底是哪个运营商,极有可能误判运营商,导致跨运营商访问。
- 域名更新问题 本地DNS服务器是由不同地区,不同运营商独立部署的,对域名解析缓存的处理上,有区别,有的会偷懒忽略解析结果TTL的时间限制,导致服务器没有更新新的ip而是指向旧的ip。
- 解析延迟 DNS的查询过程需要递归遍历多个DNS服务器,才能获得最终结果。可能会带来一定的延时。
- 域名劫持 DNS域名解析服务器有可能会被劫持,或者被伪造,那么正常的访问就会被解析到错误的地址。
- 不可靠 由于DNS解析是运行在UDP协议之上的,而UDP我之前也说过是一种不可靠的协议,他的优势在于实时性,但是有丢包的可能。
这些问题不仅会让访问速度变慢,还有可能会导致访问异常,访问页面被替换等等。
十六:怎么优化DNS解析
1)安全优化
总之DNS还是会有各种问题吧,怎么解决呢?就是用HTTPDNS。
HTTPDNS是一个新概念,他会绕过传统的运营商DNS服务器,不走传统的DNS解析。而是换成HTTP协议,直接通过HTTP协议进行请求某个DNS服务器集群,获取地址。
- 由于绕过了运营商,所以可以避免域名被劫持。
- 它是基于访问的来源ip,所以能获得更准确的解析结果
- 会有预解析,解析缓存等功能,所以解析延迟也很小
所以首先的优化,针对安全方面,就是要替换成HTTPDNS解析方式,就要借用阿里云和腾讯云等服务,但是这些服务可不是免费的,有没有免费的呢?有的,七牛云的 happy-dns。添加依赖库,然后去实现okhttp的DNS接口即可,简单写个例子:
//导入库
implementation 'com.qiniu:happy-dns:0.2.13'
implementation 'com.qiniu.pili:pili-android-qos:0.8'
//实现DNS接口
public class HttpDns implements Dns {
private DnsManager dnsManager;
public HttpDns() {
IResolver[] resolvers = new IResolver[1];
try {
resolvers[0] = new Resolver(InetAddress.getByName("119.29.29.29"));
dnsManager = new DnsManager(NetworkInfo.normal, resolvers);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
if (dnsManager == null) //当构造失败时使用默认解析方式
return Dns.SYSTEM.lookup(hostname);
try {
String[] ips = dnsManager.query(hostname); //获取HttpDNS解析结果
if (ips == null || ips.length == 0) {
return Dns.SYSTEM.lookup(hostname);
}
List<InetAddress> result = new ArrayList<>();
for (String ip : ips) { //将ip地址数组转换成所需要的对象列表
result.addAll(Arrays.asList(InetAddress.getAllByName(ip)));
}
//在返回result之前,我们可以添加一些其他自己知道的IP
return result;
} catch (IOException e) {
e.printStackTrace();
}
//当有异常发生时,使用默认解析
return Dns.SYSTEM.lookup(hostname);
}
}
//替换okhttp的dns解析
OkHttpClient okHttpClient = new OkHttpClient.Builder().dns(new HttpDns()).build();
2)速度优化
如果在测试环境,其实我们可以直接配置ip白名单,然后跳过DNS解析流程,直接获取ip地址。比如:
private static class TestDNS implements Dns{
@Override
public List<InetAddress> lookup(@NotNull String hostname) throws UnknownHostException {
if ("www.test.com".equalsIgnoreCase(hostname)){
InetAddress byAddress=InetAddress.getByAddress(hostname,new byte[]{(byte)192,(byte)168,1,1});
return Collections.singletonList(byAddress);
}else {
return Dns.SYSTEM.lookup(hostname);
}
}
}
十七:DNS解析超时怎么办
当我们在用OKHttp做网络请求时,如果网络设备切换路由,访问网络出现长时间无响应,很久之后会抛出 UnknownHostException。虽然我们在OkHttp中设置了connectTimeout超时时间,但是它其实对DNS的解析是不起作用的。
这种情况我们就需要在自定义的Dns类中做超时判断:
public class TimeDns implements Dns {
private long timeout;
public TimeDns(long timeout) {
this.timeout = timeout;
}
@Override
public List<InetAddress> lookup(final String hostname) throws UnknownHostException {
if (hostname == null) {
throw new UnknownHostException("hostname == null");
} else {
try {
FutureTask<List<InetAddress>> task = new FutureTask<>(
new Callable<List<InetAddress>>() {
@Override
public List<InetAddress> call() throws Exception {
return Arrays.asList(InetAddress.getAllByName(hostname));
}
});
new Thread(task).start();
return task.get(timeout, TimeUnit.MILLISECONDS);
} catch (Exception var4) {
UnknownHostException unknownHostException =
new UnknownHostException("Broken system behaviour for dns lookup of " + hostname);
unknownHostException.initCause(var4);
throw unknownHostException;
}
}
}
}
//替换okhttp的dns解析
OkHttpClient okHttpClient = new OkHttpClient.Builder().dns(new TimeDns(5000)).build();