移动开发中的分页 — 类型与工作原理

作者: IT Sectr 发布日期: 2026-03-11 阅读时间: 9 分钟

分页 — 一种逐页加载数据的技术,用于移动应用和Web服务处理大量记录集。根据 Android Developers Documentation (2025),正确实现分页可以降低API负载、节省流量并改善用户体验。逐页加载允许应用程序逐步显示内容,无需等待所有数据完全加载。

要点

  • 分页 — 将大型数据集分块加载以优化性能和流量的方式。
  • Offset分页 使用偏移量(page/offset)进行导航 — 方法简单,但在频繁插入时不稳定。
  • Cursor分页 使用最后一条记录的唯一游标 — 在请求之间数据变化时保持稳定。
  • Keyset分页 根据具有唯一索引的列进行过滤 — 适用于无重复的大型表。
  • Time-based分页 根据时间戳对记录进行分组 — 适用于新闻推送和社交网络。

什么是分页?

分页(来自英语 pagination — 分页) — 将大型数据集分成连续部分(页)的技术。在移动应用中,分页用于加载消息列表、新闻推送、产品目录、订单历史以及任何其他具有潜在无限记录数量的集合。

没有分页,应用程序必须同时加载所有数据,这导致等待时间长、流量消耗大以及在弱设备上运行不稳定。带分页的API请求仅返回一部分数据和用于加载下一部分的元信息 — 这样应用程序控制接收信息的量。

分页的主要指标:页面大小(page size)— 一页上的记录数(通常为10-50),以及页码或游标 — 指向集合中当前位置的指针。页面大小的选择取决于数据类型:对于紧凑元素(名称)20-30就足够,对于带图片的卡片 — 10-15。

为什么移动应用需要分页

移动设备资源有限:RAM容量、处理器速度和流量限制。分页解决三个关键任务:减少内存消耗(内存中只存储可见元素),加快首次显示(第一部分加载比整个集合更快)和节省流量(仅在用户滚动列表时加载数据)。

分页的主要类型

有四种主要的分页类型,每种解决特定的任务。方法的选择取决于对数据一致性的要求、API架构、存储类型以及客户端和服务器端允许的实现复杂度。

类型工作原理稳定性大容量下的速度
OffsetSQL中的LIMIT + OFFSET随OFFSET增加而下降
CursorWHERE id > last_id稳定(O(log n))
KeysetWHERE key > last_key稳定(O(log n))
Time-basedWHERE created_at < last_time有索引时稳定

何时使用哪种类型

Offset分页适用于静态或很少更新的数据集,当简单实现很重要时。Cursor和Keyset — 用于频繁插入的动态数据。Time-based — 用于按创建时间排序的时间线推送。 GraphQL标准 Relay使用游标分页作为唯一推荐的方法。

Offset分页:优点和缺点

Offset分页 — 最简单的逐页加载类型。客户端传递page和limit(或offset和limit)参数,服务器应用SQL OFFSET和LIMIT。例如,page=2, limit=20将返回21到40条记录。这种方法直观易懂,易于在任何技术栈上实现。

python
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items")
async def get_items(
    page: int = Query(default=1, ge=1),
    limit: int = Query(default=20, le=100)
):
    offset = (page - 1) * limit
    items = await fetch_items(offset, limit)
    total = await count_items()
    return {
        "items": items,
        "total": total,
        "page": page,
        "pages": (total + limit - 1) // limit
    }

数据不一致问题

Offset分页的主要缺点 — 跳过和重复记录的问题。如果在两个请求之间向表中添加了新记录,OFFSET会偏移:用户可能会两次看到同一条记录或错过新记录。这对于新闻推送和聊天来说至关重要,因为一致性很重要。

另一个问题 — 在大型OFFSET上性能下降。数据库必须在返回结果之前扫描并跳过前offset条记录。在offset=100000时,即使使用LIMIT 20,服务器也会花费明显的时间进行扫描。PostgreSQL和MySQL表现出随OFFSET增加而线性速度下降。

何时Offset仍然是好选择

Offset分页仍然是以下场景的最佳选择:管理面板(数据很少更改,需要在页面之间导航),报告和历史日志(固定的数据切片),带过滤的目录(可以导航到任何页面)。Offset在客户端实现也最简单 — RecyclerView配合Paging 3开箱即支持。

Keyset和Time-based分页

Keyset分页 使用唯一键(通常是主键)来过滤记录。请求使用WHERE id > last_seen_id而不是OFFSET。这确保了无论记录数量如何都能稳定性能,并且插入时没有重复,因为新记录总是具有更大的id

sql
-- Offset 分页(有问题的)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;

-- Keyset 分页(稳定的)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;

Time-based分页

Time-based分页(或时间游标)使用created_at时间戳进行导航。客户端发送最后加载记录的时间戳,服务器返回在此时间戳之前或之后创建的记录。该方法在社交网络和新闻推送中很流行,其中记录顺序由发布时间决定。

Time-based分页的特点 — 如果两条记录在同一毫秒内创建,可能会出现重复。为了消除这个问题,将time-based键与唯一id结合:WHERE (created_at, id) < (last_time, last_id)。这种复合游标保证了每条记录的唯一性和精确顺序。

Keyset和Time-based比较

Keyset分页需要具有唯一且单调递增值的列(自增id,UUID v7)。Time-based适用于任何具有created_at的表,但需要额外的重复处理。主要区别:Keyset在任何插入操作中稳定工作,而Time-based对相同时间戳敏感。

如何为项目选择分页类型

分页类型的选择取决于数据的性质和对用户体验的要求。以下是移动开发中典型场景的建议。没有通用解决方案 — 每种方法都有其最优的应用领域。

  • 聊天/通讯 — Cursor分页(按消息id)。新消息从顶部添加,游标不会偏移。
  • 新闻推送 — Time-based分页(按created_at)。记录按时间排序,时间顺序很重要。
  • 产品目录 — Offset分页。用户可以导航到特定页面,数据很少更改。
  • 订单历史 — Cursor分页。稳定性很重要,因为新订单会在加载之间添加。
  • 评论 — Keyset分页。每条评论都有唯一id,大容量无重复。

在Android上使用Paging 3实现

Android Paging 3库通过PagingSource支持所有分页类型。对于Offset — 使用Int键(page)的PagingSource,对于Cursor — 使用String或Long键(cursor)的。PagingSource自动管理加载、缓存和错误重试。

kotlin
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> {
        val cursor = params.key ?: Long.MAX_VALUE
        return try {
            val response = api.getPosts(cursor, params.loadSize)
            LoadResult.Page(
                data = response.items,
                prevKey = null,
                nextKey = response.items.lastOrNull()?.id
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Long, Post>): Long? {
        return state.anchorPosition?.let {
            state.closestItemToPosition(it)?.id
        }
    }
}

页面大小建议

页面大小影响加载速度和对性能的感知。对于移动应用,最佳范围是每页10-25个项目。少于10 — 对API的请求过于频繁,滚动不连贯。超过25 — 在慢速网络上第一部分加载时间长。

对于图片和视频,页面大小减少到5-10,因为每个项目需要额外时间加载媒体。对于文本元素列表(评论、日志),大小可以增加到30-50条记录。建议页面大小通过API可配置,以便客户端可以适应不同的网络条件。

常见问题

什么是分页(简单来说)?

分页 — 分块加载数据,而不是一次全部加载。就像一本书:你读一页,然后翻到下一页。在应用程序中,这意味着在滚动列表时,下一部分数据被加载,而不是整个列表一次加载,从而节省流量和内存。

Offset和Cursor分页有什么区别?

Offset计算记录:“跳过20条,返回接下来的10条”。如果在加载之间添加了新记录 — 编号就会偏移。Cursor使用最后一条记录的唯一标识符:“返回ID = 100之后的10条记录”。新记录不影响位置。

分页的最佳页面大小是多少?

对于移动应用程序,最佳为10-25个项目。对于带图片的列表 — 5-10,对于文本推送 — 20-30。大小取决于单个项目的平均大小:项目越重,为了快速显示,页面应该越小。

如何在RecyclerView中实现分页?

使用Android Jetpack的Paging 3库。它提供PagingSource用于加载,PagingData用于响应式流,PagingDataAdapter用于滚动时自动加载。该库通过自定义PagingSource支持Offset、Cursor和Keyset分页。

什么是无限滚动,它和分页有什么区别?

无限滚动 — 一种UI模式,在接近列表末尾时自动加载新的数据部分。分页 — 是分块加载数据的机制,而无限滚动是其显示方式之一。替代方案是“加载更多”按钮。

总结

  • 分页 — 分块加载数据的技术,对于具有列表的移动应用程序至关重要。
  • Offset分页 实现简单,但存在插入时不一致和大型OFFSET下性能下降的问题。
  • Cursor分页 使用唯一标识符进行导航 — 在任何规模下都稳定高效。
  • Keyset分页 按主键过滤,通过使用索引提供最大性能。
  • Time-based分页 根据时间戳对记录进行分组 — 适用于时间线推送和社交网络。
  • 方法选择 取决于数据性质:动态数据用Cursor/Keyset,静态数据用Offset,推送用Time-based。
  • Android上的Paging 3和iOS/Web上的标准游标解决方案为任何分页类型提供现成的基础设施。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读