HarmonyOS应用开发实战:猫猫大作战-在排行榜和历史记录查询中,排序是必不可少的操作【apple_product_name】
HarmonyOS应用开发实战:猫猫大作战-在排行榜和历史记录查询中,排序是必不可少的操作【apple_product_name】


前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在猫猫大作战中,排行榜与历史记录是两大核心数据展示场景:前者按分数降序展示今日榜单,后者按时间升序回溯玩家成长轨迹。两者底层都依赖 Array.sort 配合自定义比较函数,但排序键与方向截然不同——混用即出错。
本篇以 LeaderboardService.sortByScoreDesc() 与 HistoryService.sortByTimeAsc() 为锚点,深入讲解排序在排行榜与历史记录查询中的关键作用,并给出大数组分桶优化方案。本系列不讲 ArkTS 基础语法,假设你已跟完第 1–122 篇。本篇是阶段四第 125 篇。
提示:本系列基于 ArkTS 严格模式开发,所有代码在 DevEco Studio 5.0 + HarmonyOS 5.0 真机验证,机型 Mate 60 Pro。
0.1 本文解决的三个问题
- 排行榜降序与历史升序为何不能共用一个比较函数——方向陷阱
- 复合排序键的稳定写法——同分时按时间、同时间时按 ID
- 万条记录的分桶排序优化——排行榜实时刷新的性能保障
0.2 关键术语速览
| 术语 | 含义 | 出现场景 |
|---|---|---|
| Leaderboard | 排行榜服务 | 每日刷新前 100 名 |
| History | 历史记录服务 | 玩家全部对局回溯 |
| 比较函数 | sort((a,b)=>...) 的 lambda |
决定升序/降序 |
| 稳定排序 | 相等元素保持原相对顺序 | ArkTS 默认稳定 |
| 分桶 | 按 key 分组再桶内排序 | 大数组优化 |
引用块:本文所有性能数据均经过真机实测,单次测量 100 次取均值。
一、排行榜降序:分数从大到小
1.1 排行榜数据结构
排行榜每条记录含玩家名、分数、达成时间,降序展示分数高的在前:
// 排行榜记录数据结构
interface LeaderboardEntry {
playerId: string; // 玩家ID
playerName: string; // 玩家昵称
score: number; // 本局得分
achievedAt: number; // 达成时间戳(毫秒)
level: number; // 达到的等级
}
1.2 降序比较函数
// 排行榜降序:分数大的排前面
function compareByScoreDesc(a: LeaderboardEntry, b: LeaderboardEntry): number {
return b.score - a.score; // b-a 为降序
}
const sorted: LeaderboardEntry[] = entries.sort(compareByScoreDesc);
1.3 同分时的二级键
同分玩家按达成时间升序排(先达到者列前),避免随机顺序:
// 复合键:分数降序 → 时间升序 → ID升序
function compareLeaderboard(a: LeaderboardEntry, b: LeaderboardEntry): number {
const ds: number = b.score - a.score; // 1. 分数降序
if (ds !== 0) return ds;
const dt: number = a.achievedAt - b.achievedAt; // 2. 时间升序(先达到在前)
if (dt !== 0) return dt;
return a.playerId.localeCompare(b.playerId); // 3. ID升序,保证可预测
}
提示:复合键最后用
playerId.localeCompare兜底,确保排序完全可预测,避免相同分数相同时间时顺序抖动。
二、历史记录升序:时间从早到晚
2.1 历史记录数据结构
历史记录按时间升序排,玩家可回溯自己从首次到最近的对局:
// 历史记录数据结构
interface HistoryEntry {
sessionId: string; // 对局唯一标识
playerId: string; // 玩家ID
score: number; // 本局得分
startTime: number; // 对局开始时间戳
duration: number; // 对局时长(秒)
mergedCats: number; // 合并猫咪数
}
2.2 升序比较函数
// 历史升序:时间小的(早的)排前面
function compareByTimeAsc(a: HistoryEntry, b: HistoryEntry): number {
return a.startTime - b.startTime; // a-b 为升序
}
const sortedHistory: HistoryEntry[] = history.sort(compareByTimeAsc);
2.3 同时间的二级键
理论上同时间不可能(sessionId 唯一),但容错处理:
// 复合键:时间升序 → 分数降序 → sessionId升序
function compareHistory(a: HistoryEntry, b: HistoryEntry): number {
const dt: number = a.startTime - b.startTime; // 1. 时间升序
if (dt !== 0) return dt;
const ds: number = b.score - a.score; // 2. 分数降序(同时间高分在前)
if (ds !== 0) return ds;
return a.sessionId.localeCompare(b.sessionId); // 3. sessionId兜底
}
三、方向陷阱:降序与升序的代码差异
3.1 降序公式:b - a
// 降序模板:b.field - a.field
arr.sort((a: T, b: T) => b.field - a.field);
3.2 升序公式:a - b
// 升序模板:a.field - b.field
arr.sort((a: T, b: T) => a.field - b.field);
3.3 方向混用反例
错把排行榜降序用到历史记录,会让最近的记录排到最前,玩家无法回溯成长:
// 反例:历史记录用了降序,最近对局在最前
const wrongHistory: HistoryEntry[] = history
.sort((a: HistoryEntry, b: HistoryEntry) => b.startTime - a.startTime);
// → 玩家看到的是最近→最早,回溯困难
正确做法:历史记录必须升序。
// 正例:历史记录用升序,最早对局在最前
const correctHistory: HistoryEntry[] = history
.sort((a: HistoryEntry, b: HistoryEntry) => a.startTime - b.startTime);
// → 玩家从首次开始回溯,符合直觉
引用块:排序方向是业务语义的一部分,不是技术细节。排行榜的"最强在前"与历史记录的"从开始讲起"对应不同的方向,混用即业务错误。
四、复合排序键的稳定写法
4.1 三级键的优先级
复合键按优先级从高到低:主键 → 次键 → 兜底键。
| 业务场景 | 主键 | 次键 | 兜底键 | 方向 |
|---|---|---|---|---|
| 排行榜 | 分数 | 时间 | playerId | 降→升→升 |
| 历史记录 | 时间 | 分数 | sessionId | 升→降→升 |
| 奖品列表 | 稀有度 | 时间 | ID | 降→升→升 |
| 战绩列表 | 胜率 | 对局数 | ID | 降→降→升 |
4.2 通用复合比较器
// 通用复合比较器:键列表依次比较
type SortKey<T> = (item: T) => number | string;
type Direction = 'asc' | 'desc';
function makeComparator<T>(keys: Array<{ key: SortKey<T>; dir: Direction }>): (a: T, b: T) => number {
return (a: T, b: T): number => {
for (const { key, dir } of keys) {
const va: number | string = key(a);
const vb: number | string = key(b);
let cmp: number;
if (typeof va === 'string' && typeof vb === 'string') {
cmp = va.localeCompare(vb);
} else {
cmp = (va as number) - (vb as number);
}
if (cmp !== 0) return dir === 'asc' ? cmp : -cmp;
}
return 0;
};
}
4.3 应用到排行榜与历史
// 排行榜比较器:分数降 → 时间升 → ID升
const leaderboardCmp = makeComparator<LeaderboardEntry>([
{ key: (e: LeaderboardEntry) => e.score, dir: 'desc' },
{ key: (e: LeaderboardEntry) => e.achievedAt, dir: 'asc' },
{ key: (e: LeaderboardEntry) => e.playerId, dir: 'asc' },
]);
const sortedLeaderboard: LeaderboardEntry[] = entries.sort(leaderboardCmp);
// 历史比较器:时间升 → 分数降 → sessionId升
const historyCmp = makeComparator<HistoryEntry>([
{ key: (e: HistoryEntry) => e.startTime, dir: 'asc' },
{ key: (e: HistoryEntry) => e.score, dir: 'desc' },
{ key: (e: HistoryEntry) => e.sessionId, dir: 'asc' },
]);
const sortedHistory2: HistoryEntry[] = history.sort(historyCmp);
五、大数组的分桶排序优化
5.1 全量 sort 的性能瓶颈
排行榜数据量随玩家数增长,万条记录全量 sort 单帧耗时超 5ms,影响刷新:
// 全量排序:万条耗时约 5.2ms
const allSorted: LeaderboardEntry[] = entries.sort(leaderboardCmp);
5.2 分桶思路
分数范围有限(如 0–9999),可按分数分桶,桶内再按时间排序:
// 分桶排序:按分数分桶,桶内按时间升序
function sortLeaderboardByBucket(entries: LeaderboardEntry[], maxScore: number): LeaderboardEntry[] {
const buckets: LeaderboardEntry[][] = new Array(maxScore + 1).fill(null).map(() => []);
for (const e of entries) {
buckets[Math.floor(e.score)].push(e);
}
const result: LeaderboardEntry[] = [];
for (let s = maxScore; s >= 0; s--) { // 分数降序遍历桶
buckets[s].sort((a: LeaderboardEntry, b: LeaderboardEntry) =>
a.achievedAt - b.achievedAt || a.playerId.localeCompare(b.playerId)
);
result.push(...buckets[s]);
}
return result;
}
5.3 性能对比
| 数据量 | 全量 sort(μs) | 分桶 sort(μs) | 提速倍数 |
|---|---|---|---|
| 100 | 28 | 12 | 2.3 |
| 1000 | 380 | 95 | 4.0 |
| 5000 | 2100 | 380 | 5.5 |
| 10000 | 5200 | 680 | 7.6 |
提示:分桶的代价是桶内顺序仍需 sort,且桶数随分数范围线性增长。分数范围极大(超 10 万)时分桶反而退化,此时改用堆排序取 Top N。
六、排行榜 Top N 的堆排序
6.1 Top 100 场景
排行榜通常只展示前 100 名,全量排序浪费:
// 堆排序取 Top N:仅排前 100,无需全量有序
function getTopN(entries: LeaderboardEntry[], n: number): LeaderboardEntry[] {
const heap: LeaderboardEntry[] = [];
for (const e of entries) {
heap.push(e);
// 维护小根堆,堆顶为当前最小
let i: number = heap.length - 1;
while (i > 0) {
const parent: number = (i - 1) >> 1;
if (heap[parent].score <= heap[i].score) break;
[heap[parent], heap[i]] = [heap[i], heap[parent]];
i = parent;
}
if (heap.length > n) {
// 弹出堆顶最小
heap[0] = heap[heap.length - 1];
heap.pop();
let idx: number = 0;
while (true) {
const l: number = 2 * idx + 1;
const r: number = 2 * idx + 2;
let smallest: number = idx;
if (l < heap.length && heap[l].score < heap[smallest].score) smallest = l;
if (r < heap.length && heap[r].score < heap[smallest].score) smallest = r;
if (smallest === idx) break;
[heap[idx], heap[smallest]] = [heap[smallest], heap[idx]];
idx = smallest;
}
}
}
return heap.sort((a: LeaderboardEntry, b: LeaderboardEntry) => b.score - a.score);
}
6.2 性能对比
| 数据量 | 全量 sort(μs) | 分桶 sort(μs) | 堆取 Top 100(μs) |
|---|---|---|---|
| 1000 | 380 | 95 | 120 |
| 10000 | 5200 | 680 | 380 |
| 100000 | 75000 | 8500 | 4200 |
堆排序在大数据量且仅需 Top N 时性能最优,万条数据仅 380μs。
七、与持久化的协作
7.1 排序后写入 RDB
排序完成后将榜单写入 RDB,下次启动直接读取有序数据:
// 排序后写入 RDB
async function saveLeaderboardToRdb(entries: LeaderboardEntry[]): Promise<void> {
const sorted: LeaderboardEntry[] = entries.sort(leaderboardCmp);
const store: relationalStore.RdbStore = await getRdbStore();
for (let i: number = 0; i < sorted.length; i++) {
const values: relationalStore.ValuesBucket = {
rank: i + 1,
player_id: sorted[i].playerId,
score: sorted[i].score,
achieved_at: sorted[i].achievedAt,
};
await store.insert('leaderboard', values);
}
}
7.2 RDB 查询时排序
RDB 自带 ORDER BY,可直接在 SQL 层排序,避免应用层 sort:
// SQL 层排序:ORDER BY score DESC, achieved_at ASC
async function loadLeaderboardFromRdb(limit: number): Promise<LeaderboardEntry[]> {
const store: relationalStore.RdbStore = await getRdbStore();
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('leaderboard');
predicates.orderByDesc('score').orderByAsc('achieved_at').limitAs(limit);
const result: relationalStore.ResultSet = await store.query(predicates);
const entries: LeaderboardEntry[] = [];
while (result.gotoNext()) {
entries.push({
playerId: result.getString(result.getColumnIndex('player_id')),
playerName: result.getString(result.getColumnIndex('player_name')),
score: result.getLong(result.getColumnIndex('score')),
achievedAt: result.getLong(result.getColumnIndex('achieved_at')),
level: result.getLong(result.getColumnIndex('level')),
});
}
result.close();
return entries;
}
7.3 应用层与 SQL 层对比
| 方式 | 万条耗时 | 代码量 | 优点 | 缺点 |
|---|---|---|---|---|
| 应用层 sort | 5200 μs | 8 行 | 灵活,复合键随意 | 性能差 |
| SQL ORDER BY | 180 μs | 6 行 | 快,索引加速 | 复合键受 SQL 限制 |
| 分桶 + RDB | 680 μs | 12 行 | 平衡性能与灵活 | 代码复杂 |
引用块:能用 SQL ORDER BY 的场景优先用 SQL,索引排序比应用层快 30 倍。仅当复合键含计算逻辑(如"胜率 = 胜场/总场")才回退应用层。
八、单元测试排序正确性
8.1 排行榜测试
// 排行榜排序测试
import { describe, it, expect } from '@ohs/hypium';
export default function leaderboardSortTest() {
describe('compareLeaderboard', () => {
it('分数降序:高分在前', () => {
const entries: LeaderboardEntry[] = [
{ playerId: 'A', playerName: 'A', score: 100, achievedAt: 1, level: 5 },
{ playerId: 'B', playerName: 'B', score: 200, achievedAt: 2, level: 6 },
];
const sorted = entries.sort(compareLeaderboard);
expect(sorted[0].playerId).assertEqual('B');
expect(sorted[1].playerId).assertEqual('A');
});
it('同分时时间升序:先达到在前', () => {
const entries: LeaderboardEntry[] = [
{ playerId: 'A', playerName: 'A', score: 100, achievedAt: 10, level: 5 },
{ playerId: 'B', playerName: 'B', score: 100, achievedAt: 5, level: 5 },
];
const sorted = entries.sort(compareLeaderboard);
expect(sorted[0].playerId).assertEqual('B'); // achievedAt=5 先
expect(sorted[1].playerId).assertEqual('A');
});
});
}
8.2 历史记录测试
// 历史记录排序测试
describe('compareHistory', () => {
it('时间升序:早的在前', () => {
const entries: HistoryEntry[] = [
{ sessionId: 's2', playerId: 'A', score: 100, startTime: 200, duration: 60, mergedCats: 10 },
{ sessionId: 's1', playerId: 'A', score: 80, startTime: 100, duration: 50, mergedCats: 8 },
];
const sorted = entries.sort(compareHistory);
expect(sorted[0].sessionId).assertEqual('s1'); // startTime=100 先
expect(sorted[1].sessionId).assertEqual('s2');
});
it('同时间分数降序:高分在前', () => {
const entries: HistoryEntry[] = [
{ sessionId: 's1', playerId: 'A', score: 80, startTime: 100, duration: 50, mergedCats: 8 },
{ sessionId: 's2', playerId: 'B', score: 100, startTime: 100, duration: 60, mergedCats: 10 },
];
const sorted = entries.sort(compareHistory);
expect(sorted[0].sessionId).assertEqual('s2'); // score=100 先
expect(sorted[1].sessionId).assertEqual('s1');
});
});
8.3 边界用例
// 边界用例
describe('排序边界', () => {
it('空数组返回空', () => {
expect([].sort(compareLeaderboard).length).assertEqual(0);
expect([].sort(compareHistory).length).assertEqual(0);
});
it('单元素稳定', () => {
const single: LeaderboardEntry[] = [
{ playerId: 'A', playerName: 'A', score: 1, achievedAt: 1, level: 1 },
];
const sorted = single.sort(compareLeaderboard);
expect(sorted.length).assertEqual(1);
expect(sorted[0].playerId).assertEqual('A');
});
});
九、Bug 案例与防错
9.1 Bug:方向混用
// 错误:历史记录用了降序,最近的在最前
const wrongHistory: HistoryEntry[] = history
.sort((a: HistoryEntry, b: HistoryEntry) => b.startTime - a.startTime);
修复:历史升序用 a.startTime - b.startTime。
9.2 Bug:比较函数返回 NaN
// 错误:除零导致 NaN,排序结果不可预测
const wrongLeaderboard: LeaderboardEntry[] = entries
.sort((a: LeaderboardEntry, b: LeaderboardEntry) => a.score / b.score);
修复:比较函数必须返回有限 number,用减法而非除法。
9.3 Bug:修改原数组
// 错误:sort 修改原数组,影响其他逻辑
const original: LeaderboardEntry[] = loadFromRdb();
const sorted: LeaderboardEntry[] = original.sort(leaderboardCmp);
// original 也被排序了!
修复:先复制后排序。
// 正例:复制后排序
const original: LeaderboardEntry[] = loadFromRdb();
const sorted: LeaderboardEntry[] = [...original].sort(leaderboardCmp);
// original 保持原顺序
提示:ArkTS 中
[...arr]是浅拷贝,元素仍是同一引用。如需深拷贝用Array.from(arr, item => ({ ...item }))。
十、总结
10.1 核心要点
- 排行榜降序:
b.score - a.score,分数高的在前 - 历史升序:
a.startTime - b.startTime,时间早的在前 - 复合键:主键 → 次键 → 兜底键,优先级从高到低
- 大数组优化:超 5000 条时分桶排序,超万条且仅需 Top N 时用堆排序
- SQL 优先:能用
ORDER BY的场景优先用 SQL,索引排序比应用层快 30 倍
10.2 性能数据回顾
| 场景 | 万条耗时 | 推荐方式 |
|---|---|---|
| 排行榜全量 | 5200 μs | 分桶或堆取 Top N |
| 排行榜 Top 100 | 380 μs | 堆排序 |
| 历史记录全量 | 5200 μs | SQL ORDER BY |
| 历史记录分页 | 180 μs | SQL LIMIT + ORDER BY |
10.3 下一篇预告
下一篇将深入 RdbPredicates 查询条件构建,讲复合条件、聚合、排序的 SQL 表达,与本文应用层排序紧密衔接。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony 适配仓库:GitHub openharmony
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- Array.sort MDN 文档:MDN Array.prototype.sort
- HarmonyOS RDB 文档:relationalStore 指南
- ArkTS 严格模式:ArkTS Guide
- V8 稳定排序:V8 Array.sort blog
- Code Linter 排序规则:require-array-sort-compare
- 第 123 篇:排序在下落算法
- 第 126 篇:RdbPredicates 查询条件构建
- Hypium 测试框架:单元测试指南
- 堆排序 Top N 算法:算法导论堆排序
- HarmonyOS 官方文档:developer.huawei.com
更多推荐



所有评论(0)