作性能优化的地方最费的地方莫过于ScrollView,性能优化的主要点就是把一个scrollview分红三个区域:javascript
1:content区域:也就是scrollview须要滚动的节点的区域java
2: 缓冲区:当content中的节点滚出缓冲区以后就会将改节点回收以供下一个节点进行复用node
3:显示区:实际须要显示的节点区域每每是有限的几个在屏幕上显示而已,因此优化的思路也是从这里来,这里仍是参照官方的优化策略,固然还有一些优化策略,好比分帧加载的优化方式可是它的缺点是占用drawcall有点高,这里就不在介绍了性能优化
示意图以下:dom
原理就是在update里面检查每个列表项的相对ScrollView根节点对应的节点坐标,当节点坐标的y值在content的上方或者在content的下方就会更新itemide
对应的 ScrollCtrl.ts:性能
import Con from "./Scriptes/Con"; import Item from "./Scriptes/item"; import Achievement, { AchievementType } from "./Scriptes/Achievement"; const {ccclass, property} = cc._decorator; /** 适合数据量比较大的状况 */ @ccclass export default class NewClass extends cc.Component { @property(cc.Prefab) testPrefab: cc.Prefab = null; @property(cc.Prefab) conPrefab: cc.Prefab = null; @property(cc.ScrollView) scroll: cc.ScrollView = null; /** 缓冲区 */ @property(cc.Integer) public bufferSize: number = 1000; // @property(cc.Node) // testNode: cc.Node = null; private timeInterval: number = 0.2; private totalTime: number = 0; private lastY: number = 0; private spacing: number = 20; /** 实际要显示的元素个数 */ private spawnCount: number = 20; private items: cc.Node[] = []; private testData: AchievementType[]; /** 最后须要加载那个item的索引,例如向上滑动上面的列表项放到下面去首先须要知道显示那个数据 */ private lastUpdateIndex: number; private viewHeight: number = 0; start () { this.items = []; this.testData = this.mockData(); this.init(); this.viewHeight = this.node.height; } private mockData(): AchievementType[] { const list : AchievementType[] = []; for(let i = 0; i < 30000; i++) { let randomNum = Math.floor(Math.random() * 30); const achType = new AchievementType(randomNum + 1); if(achType) { // console.log("achType is ",achType," and index is ",randomNum + 1); achType.achievementItems = []; for(let j = 0; j < achType.achievementNum; j++) { const ach: Achievement = {} as Achievement; ach.id = j; ach.name = `ach_${Math.floor(Math.random() * 123123)}`; ach.des = "adfasd"; ach.compelte = false; achType.achievementItems.push(ach); ach.groupName = achType.name; } list.push(achType); } else { continue; } } return list; } private init() { this.scroll.content.height = (this.conPrefab.data.height + this.spacing) * this.testData.length + this.spacing; for(let m = 0; m < this.spawnCount; m++) { let conNode: cc.Node = cc.instantiate(this.conPrefab); this.scroll.content.addChild(conNode); let conCom: Con = conNode.getComponent(Con); conCom.updateCon(this.testData[m].name + `${m}`); conCom.id = m; conNode.setPosition(0,-(conNode.height + this.spacing) * m - (conNode.height / 2 + this.spacing)); this.items.push(conNode); /** 通知该子节点开始添加子节点 */ conCom.startCreateChildrens(this.testData[m].achievementItems); } } /** 根据数据计算出须要的列表元素的高度 */ private calculateDataHeight(data: AchievementType): number { let height = 0; let offsetX = 20; let offsetY = 20; let lastHeight: number = 0; let row = 0; let col = 0; let conOffsetY: number = 80; let startX = -this.conPrefab.data.width / 2 + offsetX + this.testPrefab.data.width / 2; let startY = -offsetY - this.testPrefab.data.height / 2; for(let i = 0; i < data.achievementItems.length; i++) { let dataItem: Achievement = data.achievementItems[i]; let itemNode: cc.Vec2 = cc.v2(); itemNode.x = startX + col * (this.testPrefab.data.width + offsetX); itemNode.y = startY - row * (this.testPrefab.data.height + offsetY); col++; if(itemNode.x > this.conPrefab.data.width / 2 - this.testPrefab.data.width / 2) { col = 0; itemNode.x = startX; row++; itemNode.y = startY - row * (this.testPrefab.data.height + offsetY); col++; } } console.log("row is ",row + 1); height = (row + 1) * (this.testPrefab.data.height + offsetY) + offsetY; // lastHeight += newNode.height + conOffsetY; lastHeight = height + this.spacing; // this.node.height = lastHeight + this.spacing; return lastHeight; } update (dt) { this.totalTime += dt; if(this.totalTime < this.timeInterval) { return; } this.totalTime = 0; let isDown = this.scroll.content.y < this.lastY; let offset: number = (this.conPrefab.data.height + this.spacing) * this.items.length; // console.log("items is ",this.items); for(let i = 0; i < this.items.length; i++) { let nodeItem: cc.Node = this.items[i]; /** 讲nodeitem转换为世界坐标 */ let worldPos = nodeItem.parent.convertToWorldSpaceAR(nodeItem.position); let itemView = this.scroll.node.convertToNodeSpaceAR(worldPos); // itemView.y -= this.node.parent.height / 2; let nodeCom: Con = nodeItem.getComponent(Con); if(isDown) { let newY = nodeItem.y + offset; if(itemView.y < -this.bufferSize && newY < 0) { nodeItem.y = newY; // let id = let id: number = nodeItem.getComponent(Con).id; // console.log("id is ",id); let targetId: number = id - this.items.length; nodeCom.updateCon(this.testData[targetId].name + `${targetId}`); /** 重要更新id */ nodeCom.id = targetId; /** 更新它的子节点们 */ nodeCom.updateConChildrens(this.testData[targetId].achievementItems); } } else { /** 向上滚动 */ let newY = nodeItem.y - offset; // console.log("itemView is ",itemView.y,`${nodeCom.groupName} 索引 ${i} newY is ${newY} and -offset is ${-offset}`); /** 向下挪 且没有超出下边界 */ if(itemView.y > this.bufferSize && newY > -this.scroll.content.height) { nodeItem.y = newY; let id: number = nodeItem.getComponent(Con).id; // console.log("id is ",id); let targetId: number = id + this.items.length; nodeCom.updateCon(this.testData[targetId].name + `${targetId}`); /** 更新组件的id */ nodeCom.id = targetId; /** 更新它的子节点们 */ nodeCom.updateConChildrens(this.testData[targetId].achievementItems); } } } this.lastY = this.scroll.content.y; } }
由于本次测试的列表项节点还包含几个子节点:因此Con.ts不仅是更新了列表项的信息,每次update执行的时候都会执行列表项的更新子节点的方法:测试
下面是Con.ts:字体
import Item from "./item"; import Achievement from "./Achievement"; import ItemTemp from "./item"; const {ccclass, property} = cc._decorator; @ccclass export default class Con extends cc.Component { public title: cc.Node; /** 盛放子节点的容器 */ public itemCon: cc.Node; /** 在总的元素里面是第几个 */ public id: number; /** 节点池子用于回收那些列表元素不在使用的节点 */ private collectionPool: cc.Node[] = []; /** 最多的子节点数量 */ private maxChildren: number = 4; @property(cc.Prefab) public itemPrefab: cc.Prefab = null; @property(cc.Integer) public poolDeep: number = 60; private offsetX: number = 40; private offsetY: number = 20; private spacing: number = 40; public groupName: string = ""; private pool: cc.NodePool; onLoad () { this.title = this.node.getChildByName("title"); this.itemCon = this.node.getChildByName("itemCon"); this.initPool(); } private initPool() { this.pool = new cc.NodePool(); for(let i = 0; i < this.poolDeep; i++) { let node: cc.Node = cc.instantiate(this.itemPrefab); this.pool.put(node); } } start () { } updateCon(title: string) { this.node.getChildByName("title").getComponent(cc.Label).string = title; } updateHeight(height: number) { this.node.height = height; } /** 更新该节点下的子节点传入须要更新的数据 */ updateConChildrens(items: Achievement[]) { let childrenNodes: cc.Node[] = this.itemCon.children; let itemsLength: number = items.length; /** 显示节点的个数 */ let showNumber: number = 0; childrenNodes.forEach((element,index) => { if(element.active) { showNumber++; } }) if(itemsLength <= showNumber) { /** 须要的子节点数量小于原来的就把原来的多余的子节点删除多退 */ childrenNodes.forEach((element,index) => { if(element.active && index >= itemsLength) { element.active = false; element.setPosition(cc.v2(0,0)); } }) // console.log("子节点数量为:",childrenNodes.length); } else { /** 少补 须要补几个 */ let addLength = itemsLength - showNumber; /** 找到第一个隐藏的节点的索引 */ for(let i = 0; i < addLength; i++) { let index: number = showNumber + i; if(index >= this.maxChildren) { throw new Error("须要补的索引值超出范围了"); } let hideNode: cc.Node = childrenNodes[showNumber + i]; hideNode.active = true; } } /** 统一更新组件和位置 */ childrenNodes = this.itemCon.children; childrenNodes.forEach((element,index) => { /** 只有显示的节点才会更新组件和位置 */ if(element.active) { /** 更新组件 */ let itemCom: ItemTemp = element.getComponent(ItemTemp); itemCom.updateId(index); items[index] ? itemCom.updateName(items[index].groupName) : ""; /** 更新位置 */ let pos = this.setPosition(index,items); childrenNodes[index].setPosition(pos); } }) } private setPosition(index: number,items: Achievement[]): cc.Vec2 { let prefabNodeWidth: number = this.itemPrefab.data.width; let x = 0; let y = 0; switch(items.length) { case 1: x = -index * prefabNodeWidth - this.spacing * index; break; case 2: x = -(this.spacing / 2 + prefabNodeWidth / 2) + index * (prefabNodeWidth + this.spacing); break; case 3: x = -(prefabNodeWidth + this.spacing) + index * (prefabNodeWidth + this.spacing); break; case 4: x = -(3 / 2) * (prefabNodeWidth + this.spacing) + index * (prefabNodeWidth + this.spacing); break; } return cc.v2(x,y); } /** 开始生成子节点 */ startCreateChildrens(items: Achievement[]) { let hidenNodeLen = this.maxChildren - items.length; items.forEach((element,index) => { /** 采用节点池来处理提高性能由于这些都是能够重用的对象 */ let itemNode: cc.Node = this.pool.get(); if(!itemNode) { let node = cc.instantiate(this.itemPrefab); this.pool.put(node); itemNode = this.pool.get(); } this.itemCon.addChild(itemNode); /** 设置组件信息 */ let itemCom: ItemTemp = itemNode.getComponent(ItemTemp); /** 更新索引 */ itemCom.updateId(index); /** 更新名字 */ itemCom.updateName(element.groupName); /** 设置位置 */ let pos: cc.Vec2 = this.setPosition(index,items); itemNode.setPosition(pos); }) for(let i = 0; i < hidenNodeLen; i++) { let hideNode = this.pool.get(); if(hideNode) { let tempNode = cc.instantiate(this.itemPrefab); this.pool.put(tempNode); hideNode = this.pool.get(); } /** 不让它显示用到的时候再显示 */ hideNode.active = false; this.itemCon.addChild(hideNode); } } update (dt) { } }
item的节点结构比较简单就是一个id: Label,LabelName: Label节点:下面是关于item节点的更新方法:优化
const {ccclass, property} = cc._decorator; @ccclass export default class ItemTemp extends cc.Component { private id: cc.Node; private labelName: cc.Label; onLoad () { this.id = this.node.getChildByName("id"); this.labelName = this.node.getChildByName("name").getComponent(cc.Label); } start () { } update (dt) { // console.log(this.node.y); } public updateId(id: string | number) { this.id.getComponent(cc.Label).string = id.toString(); } public updateName(name: string) { this.labelName.string = name; } }
我这里生成了30000个节点作测试,结果drawcall维持在5(实际影响它的取决于spawnCount的数值),由于这里label我也作了优化使用的是bmfont的字体
这里能够看下效果: