由于公司结构变动,需要接手之前组长的虚拟形象渲染算法,在稍作改动迁移至小程序之后,这里我主要修改的部分并不是渲染算法,而是在大列表情况下渲染任务的调度实现。

UPDATE:
继以实现的虚拟形象渲染算法,新增缓存、优先级等功能以实现优化。

调度模型

首先我们需要先明白实现一个虚拟形象,需要被控制调度的任务是 medel 局部图片的下载工作。
因此在商品列表页、专题页等会出现多个 medel 的场景下,我预想的调度模型如下图:

这是一个嵌套双队列的任务调度模型,最外层是当前场景下需要渲染的任务,每个渲染任务里面又必须依赖多个图片资源。

渲染任务队列

废话不多说,先上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import RenderTask from './renderTask'
const objToString = Object.prototype.toString.call
class RenderManager {
constructor (canvasId = null, runningCallback = null) {
this.canvasId = canvasId
this.pendingRenderTask = []
this.runningRenderTask = null
this.runningCallback = runningCallback
}
add (task) {
if (typeof task === 'object' && task !== null && !Array.isArray(task)) {
const task = new RenderTask(this.canvasId, task)
this.pendingRenderTask.push(task)
} else if (Array.isArray(task)) {
const tasks = task.map(item => new RenderTask(this.canvasId, item))
this.pendingRenderTask = [
...this.pendingRenderTask,
...tasks
]
}
this.run()
}
shift () {
return this.pendingRenderTask.shift()
}
run () {
if (this.runningRenderTask || (!this.runningRenderTask && this.pendingRenderTask.length <= 0)) return
this.runningRenderTask = this.shift()
this.runningRenderTask.execute().then(result => {
this.runningRenderTask = null
this.runningCallback(result)
this.run()
})
}
size () {
return this.pendingRenderTask.length
}
clear () {
this.canvasId = null
this.pendingRenderTask = []
this.runningRenderTask = null
this.runningCallback = null
}
}
export default RenderManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import MedelCanvas from "../../utils/medelCanvas";
class RenderTask {
constructor (canvasId, task) {
this.canvasId = canvasId
this.medelCanvas = new MedelCanvas(canvasId, task)
}
execute () {
return this.medelCanvas.draw()
}
}
export default RenderTask

以上就是渲染任务队列类和渲染任务类的代码,这里的调度机制是,当有任务被添加到队列时,会检查当前是否有正在执行的任务,如果没有就调出队首的任务开始执行,否则就等待当前任务执行完毕后再调出队首任务执行。

这里需要注意一点,添加任务的方式可能是推入一个任务,也有可能是一次性推入一个列表的渲染任务,所以需要区分当前推入的是一个对象还是数组,在区分数据是对象还是数组时,通常的做法是使用 Object.prototype.toString.call,但是如果数据是用户自定义数据时,Object.prototype.toString.call(data) 会输出 [object, Undefined]

一个任务模型里面会包含一个渲染算法的实例,渲染算法里面就包含了图片下载任务队列。

图片资源下载任务队列

图片资源下载任务队列的健壮性要比渲染任务队列更高,除了普通队列的实现,还带有缓存图片的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import CacheManager from './cacheManager'
class TaskManager {
constructor ({ cacheCapacity = 20, concurrentCapcity = 5 }) {
this.pendingTaskQueue = []
this.runningTasks = new Set()
this.handlerGroups = {}
this.cacheManager = new CacheManager({ capacity: cacheCapacity })
this.concurrentCapcity = concurrentCapcity
}
runTaskPromise (task) {
return new Promise((resolve, reject) => {
this.addTask(task, (result, error) => {
if (error) {
reject(error)
} else {
resolve(result)
}
})
})
}
addTask (task, handler) {
const cache = this.cacheManager.getCache(task.hash)
if (task.loadCache && cache !== undefined) {
handler(cache.content)
} else if (this.handlerGroups[task.hash]) {
this.handlerGroups[task.hash].push(handler)
} else {
this.handlerGroups[task.hash] = [handler]
this.pendingTaskQueue.push(task)
}
this.run()
}
deleteHandlerGroup (key) {
const handlerGroup = this.handlerGroups[key]
delete this.handlerGroups[key]
return handlerGroup
}
shiftTask () {
const task = this.pendingTaskQueue.shift()
return task
}
run () {
if (this.pendingTaskQueue.length === 0 || this.runningTasks.size >= this.concurrentCapcity) {
return
}
const task = this.shiftTask()
const key = task.hash
this.runningTasks.add(task)
task.execute()
.then(result => {
if (task.updateCache) {
this.cacheManager.addCache(task.toCache())
}
this.deleteHandlerGroup(key).forEach(handler => handler(result))
this.runningTasks.delete(task)
this.run()
})
.catch(error => {
this.deleteHandlerGroup(key).forEach(handler => handler(null, error))
this.runningTasks.delete(task)
this.run()
})
}
clear () {
this.cacheManager.clear()
}
}
export default TaskManager

整体上和渲染任务队列实现是一致的,不过在此基础上还添加了缓存功能,用户缓存图片资源,这里简要说明一下,由于一个用户在访问一个带有 medel 的商品列表时,每个商品都要携带一个用户的 medel。因为每个 medel 都由 12 - 15 张局部图片组成,其中部分图片是重复的,比如虚拟形象的腿、胳膊等,因此会以图片的 url 为 key,将下载到本地的地址为 value 缓存起来。

这里需要注意一点就是,在浏览器中,如果一张图片资源正在下载中,又去下载同一张图片,浏览器是会正常下载的,因此我们需要将执行任务定义为一个 Set 实现去重。同时将任务完成后的回调以数组形式保存起来,然后依次执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class CacheManager {
constructor ({ capacity = 20 }) {
this.cacheMap = {}
this.cacheList = []
this.capacity = capacity
}
getCache (key) {
const cache = this.cacheMap[key]
if (cache !== undefined) {
cache.hit()
}
return cache
}
addCache (cache) {
if (this.cacheList.length >= this.capacity) {
const cacheNumToDel = Math.floor(this.capacity / 3)
this.cacheList.sort((a, b) => b.priority() - a.priority())
const offset = this.capacity - cacheNumToDel
const delList = this.cacheList.splice(offset, this.cacheList.length - offset)
delList.forEach(item => delete this.cacheMap[item.key])
}
if (this.cacheMap.hasOwnProperty(cache.key)) {
this.cacheMap[cache.key].content = cache.content
} else {
this.cacheMap[cache.key] = cache
this.cacheList.push(cache)
}
}
size () {
return this.cacheList.length
}
clear () {
this.cacheList = []
this.cacheMap = {}
}
}
export default CacheManager

这里就是缓存实现,在缓存队列里面,我们做了容量大小的限制,以及优先级的控制。

总结

以上就是在虚拟形象生成算法里面的两个任务调度算法,代码的健壮性和性能的优化还需要更加深挖,下一步的任务是在渲染队列上添加缓存以及优先级的功能,将普通的队列结构替换成优先队列结构。

优化

这里主要是实现两个方面的优化:

  • 已渲染过的 medel,没必要二次渲染,虽然图片下载会有缓存,但是调用 canvas 本身就会有一定的开销,我们可以将已渲染好的 medel 的本地图片地址缓存起来。
  • 支持优先级,这一部分主要为了满足一定业务场景下的需求,当用户从微信公众号的推广软文进入小程序,滚动条的初始位置会定位到指定的商品,这时候应该优先渲染该商品的 medel。

缓存

1
2
3
4
5
6
7
8
9
10
11
import CacheManager from '@/utils/medelCanvas/cacheManager'
class RenderCacheManager extends CacheManager {
// 不共用同一缓存作用域
// 逻辑实现保持一致
constructor() {
super({ capacity: 20 })
}
}
export default new RenderCacheManager()

这里缓存我们可以复用之前图片缓存的 cacheManager,这里需要单例化我们的缓存管理对象,因为 canvas 渲染在不同页面需要用到的是同一个缓存管理对象。

配合缓存管理对象,在渲染管理对象的代码上也需要做一些改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class RenderManager {
//...
add (task) {
if (typeof task === 'object' && task !== null && !Array.isArray(task)) {
const renderTask = new RenderTask(this.canvasId, task)
const cache = this.renderCacheManager.getCache(renderTask.hash)
if (cache) {
renderTask.runningCallback({ id: renderTask.id, resultTmpFile: cache.content })
} else {
this.pendingRenderTask.push(renderTask)
}
} else if (Array.isArray(task)) {
const length = task.length
for (let i = 0; i < length; i++) {
const item = task[i]
const renderTask = new RenderTask(this.canvasId, item)
const cache = this.renderCacheManager.getCache(renderTask.hash)
if (cache) {
renderTask.runningCallback({ id: renderTask.id, hash: renderTask.hash, resultTmpFile: cache.content })
} else {
this.pendingRenderTask.push(renderTask)
}
}
}
this.run()
}
run () {
if (this.runningRenderTask || (!this.runningRenderTask && this.pendingRenderTask.length <= 0)) return
const startTime = Date.now()
if (this.renderType === 'stock') {
this.runningRenderTask = this.pop()
} else if (this.renderType === 'queue') {
this.runningRenderTask = this.shift()
}
this.runningRenderTask.execute()
.then(result => {
const endTime = Date.now()
console.log('costTime =>', endTime - startTime)
this.runningRenderTask.content = result
if (this.runningRenderTask.hash && this.runningRenderTask.content) {
this.renderCacheManager.addCache(this.runningRenderTask.toCache())
}
this.runningRenderTask.runningCallback({ id: this.runningRenderTask.id, hash: this.runningRenderTask.hash, resultTmpFile: result })
this.runningRenderTask = null
this.run()
})
.catch(() => {
this.runningRenderTask = null
this.run()
})
}
}

以上代码的主要思路是:我们在渲染任务添加进队列时就去检索缓存 Map 里面是否存在可用缓存图片,如果存在直接推出该任务,不再进入任务缓冲池中。当一个任务渲染完成之后,会以图片的本地地址作为 value,指定的 hash 值作为 key,存入缓存管理对象中。

优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class RenderManger {
// ...
pop () {
const length = this.pendingRenderTask.length
let entry = length - 1
for (let i = length - 2; i >= 0; i--) {
if (this.pendingRenderTask[i].priority > this.pendingRenderTask[entry].priority) {
entry = i
}
}
return this.pendingRenderTask.splice(entry, 1)[0]
}
shift () {
const length = this.pendingRenderTask.length
let entry = 0
for (let i = 1; i < length; i++) {
if (this.pendingRenderTask[i].priority > this.pendingRenderTask[entry].priority) {
entry = i
}
}
return this.pendingRenderTask.splice(entry, 1)[0]
}
}

我们允许一个任务对象有 priority 属性,该属性值表示这个任务的优先级。在从任务缓冲池中推出任务时,优先推出优先级较高的任务,其次才是根据队列或者栈的方式推出对首或队尾的任务。