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
感知到他们的生命周期,从而进行实例管理的。
ViewModelScope了解吗
这里主要就是考ViewModel
和其他一些组件的关系了。关于协程,之前也专门说过一篇,主要用作线程切换。如果在多个协程中,需要停止某些任务,就必须对这些协程进行管理,一般是加入一个CoroutineScope
,如果需要取消协程,就可以去取消这个CoroutineScope
,他所跟踪的所有协程都会被取消。
GlobalScope.launch { longRunningFunction() anotherLongRunningFunction() }
但是这种全局使用方法,是不被推荐使用的,如果要限定作用域的时候,一般推荐viewModelScope。
viewModelScope
是一个 ViewModel 的 Kotlin 扩展属性。它能在ViewModel
销毁时 (onCleared() 方法调用时) 退出。所以只要使用了 ViewModel,就可以使用 viewModelScope
在 ViewModel 中启动各种协程,而不用担心任务泄漏。
class MyViewModel() : ViewModel() { fun initialize() { viewModelScope.launch { processBitmap() } } suspend fun processBitmap() = withContext(Dispatchers.Default) { // 在这里做耗时操作 } }
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
方法即可。
依赖注入是啥?为什么需要她?
简单的说,依赖注入就是内部的类在外部实例化了。也就是不需要自己去做实例化工作了,而是交给外部容器来完成,最后注入到调用者这边,形成依赖注入。
举个例子:Activity中有一个user类,正常情况下要使用这个user肯定是需要实例化它,不然他是个空值,但是用了依赖注入后,就不需要在Activity
内部再去实例化,就可以直接使用它了。
@AndroidEntryPoint class MainActivity : BaseActivity() { @Inject lateinit var user: User }
这个user
就可以直接使用了,是不是有点神奇,都不需要手动依赖了,当然代码没写完,后面再去完善。只是表达了这么一个意思,也就是依赖注入
的含义。
那么这种由外部容器来实例化对象的方式到底有什么好处
呢?最大的好处就是减少了手动依赖,对类进行了解耦。具体主要有以下几点:
- 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。
- 在配置
scopes
范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。 - 代码变得更具可读性。
- 易于构建对象。
- 编写低耦合代码,更容易测试。
Hilt是啥,怎么用?
很明显,Hilt就是一个依赖注入库,一个封装了Dagger
,在Dagger
的基础上进行构建的一个依赖注入库。Dagger
我们都知道是一个早期的依赖注入库,但确实不好用,需要配置很多东西,那么Hilt简单到哪了呢?我们继续完善上面的例子:
@HiltAndroidApp public class MainApplication extends Application { } @AndroidEntryPoint class HiltActivitiy : AppCompatActivity() { @Inject lateinit var user: UserData override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) showToast(user.name) } } data class UserData(var name: String) { @Inject constructor() : this("bob") }
说下几个注释的含义:
@HiltAndroidApp
。所有使用Hilt的App必须包含一个使用 @HiltAndroidApp 注解的 Application,相当于Hilt的初始化,会触发Hilt代码的生成。@AndroidEntryPoint
。用于提供类的依赖,也就是代表这个类会用到注入的实例。@Inject
。这个注解是用来告诉 Hilt 如何提供该类的实例,它常用于构造函数、非私有字段、方法中。
Hilt支持哪些类的依赖注入。
1) 如果是 Hilt 支持的 Android
组件,直接使用 @AndroidEntryPoint
注解即可。比如Activity,Fragment,Service
等等。
- 如果是
ComponentActivity
的子类Activity,那么直接使用@AndroidEntryPoint就可以了,比如上面的例子。 - 如果是其他的Android类,必须在它依赖的Android类添加同样的注解,例如在 Fragment 中添加
@AndroidEntryPoint
注解,必须在Fragment依赖的Activity上也添加@AndroidEntryPoint注解。
2)如果是需要注入第三方的依赖,可以使用@Module注解,使用 @Module
注解的普通类,在其中创建第三方依赖的对象。比如获取okhttp的实例
@Module @InstallIn(ApplicationComponent::class) object NetworkModule { /** * @Provides * @Singleton 提供单例 */ @Provides @Singleton fun provideOkHttpClient(): OkHttpClient { return OkHttpClient.Builder() .build() } }
这里又有几个新的注解了:
@Module
。用于创建依赖类的对象@InstallIn
。使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围,例如使用 @InstallIn(ActivityComponent::class) 注解的 module 会绑定到 activity 的生命周期上。@Provides
。用于被 @Module注解标记类的内部的方法,并提供依赖项对象。@Singleton
。提供单例
3)为ViewModel提供的专门的注解
@ViewModelInject
,在Viewmodel对象的构造函数中使用 @ViewModelInject 注解可以提供一个 ViewModel。
class HiltViewModel @ViewModelInject constructor() : ViewModel() {} private val mHitViewModule: HiltViewModel by viewModels()
说说DNS,以及存在的问题
之前看过我说的网络问题应该知道DNS
用来做域名解析工作的,当输入一个域名后,需要把域名转化为IP地址,这个转换过程就是DNS解析
。
但是传统的DSN解析会有一些问题,比如:
域名缓存问题
本地做一个缓存,直接返回缓存数据。可能会导致全局负载均衡失败,因为上次进行的缓存,不一定是这次离客户最近的地方,可能会绕远路。域名转发问题
如果是A运营商将解析的请求转发给B运营商,B去权威DNS服务器查询的话,权威服务器会认为你是B运营商的,就返回了B运营商的网站地址,结果每次都会跨运营商。出口NAT问题
做了网络地址转化后,权威的DNS服务器,没法通过地址来判断客户到底是哪个运营商,极有可能误判运营商,导致跨运营商访问。域名更新问题
本地DNS服务器是由不同地区,不同运营商独立部署的,对域名解析缓存的处理上,有区别,有的会偷懒忽略解析结果TTL的时间限制,导致服务器没有更新新的ip而是指向旧的ip。解析延迟
DNS的查询过程需要递归遍历多个DNS服务器,才能获得最终结果。可能会带来一定的延时。域名劫持
DNS域名解析服务器有可能会被劫持,或者被伪造,那么正常的访问就会被解析到错误的地址。不可靠
由于DNS解析是运行在UDP协议之上的,而UDP我之前也说过是一种不可靠的协议,他的优势在于实时性,但是有丢包的可能。
这些问题
不仅会让访问速度变慢,还有可能会导致访问异常,访问页面被替换等等。
怎么优化DNS解析
- 安全优化
总之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();
- 速度优化
如果在测试环境,其实我们可以直接配置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();
注解是什么?有哪些元注解
注解,在我看来它是一种信息描述,不影响代码执行,但是可以用来配置一些代码或者功能。
常见的注解比如@Override
,代表重写方法,看看它是怎么生成的:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
可以看到Override被@interface
所修饰,代表注解,同时上方还有两个注解@Target和@Retention,这种修饰注解的注解叫做元注解
,很好理解吧,就是最基本的注解呗。java中一共有四个元注解:
@Target
:表示注解对象的作用范围。@Retention
:表示注解保留的生命周期@Inherited
:表示注解类型能被类自动继承。@Documented
:表示含有该注解类型的元素(带有注释的)会通过javadoc或类似工具进行文档化。