并发加载数据-让你的页面展示的更快

1,703 阅读5分钟

一般人的思维

按照大部分人的逻辑,加载一个页面的正常流程都是:

  • 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,它俩就是解决同时到达问题的。

所以你看,一个小技巧,能归纳出这么多知识点,假如你用这种拓展归纳的思想方法来设计代码,来写代码,何愁知识学不完呢,所以,打开格局发散思维吧。