一般人的思维
按照大部分人的逻辑,加载一个页面的正常流程都是:
- 1 启动页面(
startActivity
); - 2 初始化View(
findViewById
); - 3 从服务器拉数据(
requestServer
); - 4 渲染页面,也就是将数据展示出来。
现在,我们将第一步到第三步的页面状态记为P1,因为这三步的页面都是一样的,都是空的; 我们将第一步耗费的时间记为T1,第二步耗费的时间忽略不计(因为很快),第三步耗费的时间记为T2,第四步耗费的时间也忽略不计(以为也很快)。
那么,上述流程耗费的总时间就是T1+T2,也就是一个人要等待T1+T2的时间,才看到页面从P1切换到P2。
这是很不友好的,所以,狡猾的产品经理就发明了loading页面,也就是大家常说的菊花转,在等待P1切换到P2的过程中,把"菊花"亮出来让它使劲转,告诉用户我正在玩命加载呢,你先别猴急,让子弹飞一会儿再说。
其实,这是非常不好的,吾有一计,可解此危,且听我细细道来。
非一般人的思维
那么,我们先分析上述流程:
- 第一步和第二步,是在UI线程中执行的。
- 第三步,是在IO线程中执行的。
- 第四步,是在UI线程中执行的。
那么,第一二步完全可以和第三步一起执行啊,他俩又不互相依赖,不存在竞争关系,完全可以一边吃饭一边上厕所啊,没毛病,就这么干!
于是,流程就变成了如下:
- 1 启动页面(
startActivity
),并从服务器拉数据(requestServer
) - 2 初始化View(
findViewById
); - 3 渲染页面,也就是将数据展示出来。
有人说,你这不对啊,按照正常逻辑,我是页面起来了才去拿数据,数据就可以放在页面里;而你这顺序都不能保证,万一你数据先来了,页面还没创建出来呢,你数据放哪里?
先别急,先别急。
我们首先创建两个存储器,一个放页面,一个放页面需要的数据,如果数据来了,那么就去找页面,找到就直接把数据塞给页面让它去渲染,没找到就把自己洗白白存起来等着页面来找;如果页面来了,就去找数据,找到数据直接拿过来自己玩就行,找不到也把自己洗白白存起来等着数据来找。
就跟男女约会一样,先来的等着,等后来的找就行了,如此简单的道理是个人应该都懂。
那么,这样一来,耗费的时间就是T1和T2的最大值了,也就是max(T1,T2)
。
道理我是懂了,代码该怎么写呢,我不可能为每个页面都创建两个存储器吧,也太费劲了。
放心,你都看到这了,我不给你准备好拆箱即可用的代码怎么行呢?
上代码
首先,我们创建一个接口,来标记这个页面要使用并发加载数据功能:
interface IPreLoad {
// 一个tag,用来作为数据和页面互找的信物
fun tag(): String
// 用于通知页面更新
fun <T> notify(data: T)
// 是否是一次性的,一次性表示:得到数据后就不再关注该数据
// 非一次性表示,持续关注该数据,只要有更新,就通知数据给页面
fun oneShot(): Boolean = true
}
然后,我们定义存储器并处理数据分发逻辑:
object PreLoadCenter {
private var isInit = false
private val dispatchScope = MainScope()
// 存储,需要预加载的页面
private val preLoadPage: MutableMap<String, WeakReference<IPreLoad>> = mutableMapOf()
// 存储,需要预加载的页面对应的数据
private val preLoadData: MutableMap<String, Any?> = mutableMapOf()
fun init(application: Application) {
isInit = true
application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity is IPreLoad) {
try {
getOrWait(activity)
} catch (t: Throwable) {
log("getOrWait error: $t")
}
}
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
if (activity is IPreLoad) {
// 清除页面
removePreLoadPage(activity.tag())
// 清除数据
removePreLoadData(activity.tag())
}
}
})
}
/**
* 数据过来后调用,用于通知页面 或者 存储
* @param tag 预加载的tag,用于寻找对应的页面
* @param data 预加载的页面需要的数据]
* 这个调用需要放在Main线程
*/
fun notifyOrSave(tag: String, data: Any?) {
if (!isInit) return
if (data == null) return
dispatchScope.launch {
// 如果,此时已经有页面,则直接通知页面即可,不需要存储;
// 此时:页面先于数据到达,已经注册了观测器,直接将数据通知到页面即可
if (preLoadPage.containsKey(tag)) {
preLoadPage[tag]?.get()?.let { preLoad -> preLoad.notify(data)
// 如果,页面是只需要获取一次数据,则通知后就移除
if (preLoad.oneShot()) {
removePreLoadPage(tag)
}
}
return@launch
}
// 否则,就记录下来,等待页面启动后找过来
addPreLoadData(tag, data)
}
}
/**
* 页面过来后,去获取数据 或者 等待
* @param preLoad 需要预加载的页面
*/
private fun getOrWait(preLoad: IPreLoad) {
// 如果,此时已经有数据,则直接获取即可
// 此时:数据先于页面到达
if (preLoadData.containsKey(preLoad.tag())) {
preLoadData[preLoad.tag()]?.let {
preLoad.notify(it)
}
// 如果,页面不是只获取一次数据,而是持续性观测,则添加到观测队列
if (!preLoad.oneShot()) {
addPreLoadPage(preLoad)
}
// 移除数据
removePreLoadData(preLoad.tag())
} else {
// 数据尚未到达,就加入观测队列等待数据
addPreLoadPage(preLoad)
}
}
private fun addPreLoadPage(preLoad: IPreLoad) {
preLoadPage[preLoad.tag()] = WeakReference(preLoad)
log("put page by tag: [${preLoad.tag()} = $preLoad], after put size is: ${preLoadPage.size}")
}
private fun removePreLoadPage(tag: String) {
if (preLoadPage.containsKey(tag)) {
val removeObj = preLoadPage.remove(tag)
log("remove page by tag: [$tag= ${removeObj?.get()}], after remove size is: ${preLoadPage.size}")
}
}
private fun addPreLoadData(tag: String, data: Any) {
preLoadData[tag] = data
log("put data by tag: [$tag = $data], after put size is: ${preLoadData.size}")
}
private fun removePreLoadData(tag: String) {
if (preLoadData.containsKey(tag)) {
val removeObj = preLoadData.remove(tag)
log("remove data by tag: [$tag= $removeObj], after remove size is: ${preLoadData.size}")
}
}
fun log(msg: String) {
Log.d("PreLoadCenter", "[$msg]")
}
}
看注释相信都能看懂,怎么用呢?很简单,看下面例子:
- 1 初始化,在Application的onCreate()里面调用如下方法:
fun onCreate() {
PreLoadCenter.init(this)
}
- 2 让
Activity
实现IPreLoad
接口,并实现相关方法:
class TestActivity : IPreLoad {
override fun tag(): String {
return TAG
}
override fun <T> notify(data: T) {
// 这里建议加数据校验,保证安全。
if (data is TestData) {
// TODO 用这个数据渲染页面
}
}
}
- 3 启动
Activity
的时候,同时调用接口:
startActivity(TestActivity);
ServerApi.requestTestData(TestActivithy.TAG)
这里要注意传递TAG,这是页面和数据互找的定情信物。
- 4 最后,在服务器数据过来后,通知到页面即可。
fun onSuccess() {
PreLoadCenter.notifyOrSave(TAG,TestData("success"))
}
fun onFailure() {
PreLoadCenter.notifyOrSave(TAG,TestData("failure"))
}
就这么简单,没有一点耦合性,每个页面都可以使用,爽的一批。
反思
我们来归纳一下上述的出发点,以及逻辑。
我们发现页面加载慢,经过分析得出有一些步骤可以同时执行的,换句话说,我们做任何事情都需要想想是否能并发执行,也就是串行变为并行,或者说单队变多队,这样能加快速度,我们称这个为并发加速思想,别管名字好听不好听,只要有这个意识就行。
其次,我们发现上述其实是个观察者模式,数据通知页面,这明显就是个观察者模式,只不过扩大了用而已。
第三,上述逻辑还是个广义的生产者消费者问题,请求数据的动作是生产者,生产数据这个产品,页面是消费者,消费数据这个产品。
最后,上述还是个同时到达问题,了解过并发的同学应该知道CountDownLatch和CyclicBarrier
,它俩就是解决同时到达问题的。
所以你看,一个小技巧,能归纳出这么多知识点,假如你用这种拓展归纳的思想方法来设计代码,来写代码,何愁知识学不完呢,所以,打开格局发散思维吧。