1. 做云笔记之前,先想清楚“云”到底解决什么问题
很多同学一接到“基于Android的云笔记系统”这个题目,第一反应就是“做个App,能写笔记,能上传服务器”。如果只是这个层面,那它跟“记事本”没什么本质区别。我做这个项目之前也这样想,结果做到一半发现,真正的难点根本不在“能不能写”,而在“写完以后数据怎么安全、可靠地流动起来”。
云笔记的核心矛盾,其实一句话就能说清楚:本地操作是即时的,云端数据是全量的,两者之间永远存在时间差和状态差。 你在手机上打了一行字,这行字此刻只存在于你这台设备的SQLite数据库里;如果你换了手机、清了缓存、或者App被杀掉,这行字可能就永远消失了。云笔记要做的事情,就是想办法把这种“本地即时性”和“云端可靠性”之间的缝隙补上。
所以我在立项时给自己定了几个边界条件:
- 单设备编辑必须零延迟,不能因为网络波动影响打字体验;
- 多设备同步必须做到“最终一致”,不要求实时,但必须保证不丢数据;
- 离线状态必须完整可用,飞行模式下打开笔记、编辑、退出,再联网时自动合并;
- 服务器端只存全量快照和操作日志,不参与业务逻辑判断。
这四条定下来之后,整个项目的技术选型和架构设计都有了明确的约束方向。云笔记不是一个“上传下载”的玩具,而是一个小型的分布式数据同步系统。你心里要一直绷着这根弦,后面每写一行代码、每设计一张表,都会发现这些约束在帮你做决策。
关于技术栈,我最终选的是Kotlin + Jetpack Compose + Room + Retrofit + WorkManager。Kotlin没得说,Google官方力推,协程写异步逻辑比RxJava舒服太多;Compose是2024年之后新项目的主流选择,声明式UI做笔记编辑器这种状态频繁变化的界面,比XML布局写起来省心;Room负责本地持久化,提供SQLite的编译期检查,表结构改动能提前暴露问题;Retrofit用它做网络层主要是生态成熟,配合OkHttp拦截器做日志和鉴权非常方便;WorkManager专门跑后台同步任务,它能自动处理电量优化、重试策略、进程被杀之后的恢复,比我自己写Service靠谱得多。
这套组合拳打下来,基本覆盖了一个云笔记客户端在“存储、展示、通信、调度”四个维度上需要的全部能力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据层设计:笔记在本地到底怎么存
2.1 表结构:别小看这三张表
客户端数据层是整个云笔记的地基。地基不稳,上面建什么都白搭。我用Room建了三张核心表:笔记主表、操作日志表、元信息表。
笔记主表的结构,实测下来比较稳的字段设计是下面这样:
kotlin复制@Entity(tableName = "notes")
data class NoteEntity(
@PrimaryKey val id: String, // 全局唯一ID,客户端生成,UUID
val title: String,
val content: String, // 正文内容,富文本或Markdown原文
val updatedAt: Long, // 本地最后修改时间戳
val createdAt: Long,
val deleted: Boolean = false, // 软删除标记
val version: Long = 0, // 本机版本号,每次修改+1
val dirty: Boolean = true // 是否需要同步到云端
)
需要注意的有几个点:
ID用UUID,而且必须在客户端生成,不能等服务端返回。 这是离线优先架构的铁律。如果ID由服务器生成,那离线创建的笔记在恢复网络之前就没有合法ID,整个数据结构就要为“待上传”状态妥协。UUID虽然在数据库索引性能上不如自增整数,但换来的是离线语义的完整性和多端合并时的绝对唯一性,非常值。
软删除标记deleted必须留。 用户删笔记,你在本地只是把deleted置为true,等同步完成后再真正物理删除,或者干脆永远不物理删除,只做逻辑删除。为什么?因为同步协议里最难处理的就是删除冲突。A设备删了笔记,B设备离线时改了同一篇笔记,等两边同步时你怎么办?有软删除标记,至少还能保留“已删除”这个事实,给冲突处理留出最后的兜底空间。
version字段是本机版本号,不是全局版本号。 全局版本号的维护需要服务器协作,在没有专门同步服务的情况下,客户端自己维护version就是“改一次加一”,用它在本地做乐观锁判断,检测“我改的同时别人也改了没”。
操作日志表是同步引擎的数据源,设计思路在后面同步章节展开,这里先放结构:
kotlin复制@Entity(tableName = "operation_log")
data class OperationLogEntity(
@PrimaryKey val opId: String,
val noteId: String,
val opType: String, // CREATE / UPDATE / DELETE
val contentSnapshot: String,
val timestamp: Long,
val synced: Boolean = false
)
元信息表不复杂,就是存一些key-value对,比如最后一次成功同步的时间、当前登录用户的ID、服务端下发的配置项等等。实测下来用一个通用表就行,没必要一配置一表,浪费开发时间。
2.2 用LiveData还是Flow:我选Room + Flow
Room从2.2版本开始官方支持协程Flow作为查询的返回类型。我的选择是数据库层所有查询都返回Flow,UI层用repeatOnLifecycle收集。这样一个查询在任何表数据变化后都会自动重新发射,笔记列表、笔记详情天然具备响应式更新的能力。
kotlin复制@Query("SELECT * FROM notes WHERE deleted = 0 ORDER BY updatedAt DESC")
fun observeAllNotes(): Flow<List<NoteEntity>>
最关键的是标题列表的实时刷新:你在编辑界面改一行字,返回列表页时列表自动更新了,不需要手动刷新。这个体验对笔记类应用来说,好感度提升是质变的。
2.3 全文搜索:SQLite的FTS4
云笔记基本逃不掉搜索功能。如果笔记量大,用SQL的LIKE '%关键词%'做搜索,表数据过万后明显变卡。SQLite自带FTS4/FTS5全文搜索扩展,Room可以配合支持,效果是关键词搜索的响应速度从几百毫秒降到几毫秒。
我的做法是建一个虚拟表:
sql复制CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts4(
title, content, content='notes', content_rowid='id'
)
同时手工维护触发器和同步逻辑,笔记表增删改时同步更新FTS索引。这个方案比每次搜索时全表扫描快一个数量级,而且SQLite的FTS还支持中文分词——当然,默认分词器对中文支持不算好,但如果你的笔记以Markdown和代码块为主,FTS4的默认表现已经足够用了。真要上专业的中文分词,可以挂ICU分词器,但会增加打包体积和初始化时间,取舍看需求。
2.4 SharedPreferences的误区
很多项目喜欢把“用户设置项”丢进SharedPreferences,比如主题色、字体大小、默认编辑器字体。如果只是本机单机App,这没问题。但只要涉及多端同步,“设置项要不要上云”就是一个必须提前回答的问题。
我的建议是:客户端偏好设置留在本地,笔记相关的配置(比如默认笔记本、是否公开)跟笔记数据一起走同步协议。 因为用户换手机之后,最在意的是笔记内容有没有跟着过来,而不是字体设置有没有跟着过来。把同步的粒度控制在“笔记数据”上,能让同步引擎的职责非常清晰,不会出现“配置同步了一半”这种尴尬状态。
3. 云同步引擎:本地数据库和服务器之间的搬运工
3.1 同步的核心不是“上传下载”,而是“操作日志补发”
每天在应用市场能看到的那些网络请求一坨一坨的App,大多是直接把整个JSON文件往服务器丢。数据量小的时候没什么感觉,笔记一多、图片一加,每次同步都是全量上传,消耗流量不说,服务端处理负荷也扛不住。
我的做法是基于操作日志做增量同步。本地的每一次修改,除了更新notes表,还会往operation_log表里插入一条操作记录。同步引擎启动时,只把“上次同步点之后新增的操作日志”批量发给服务端,服务端按顺序重放这些操作,再返回“服务端确认到哪条日志”的水位信息。
这个思路本质上借鉴了数据库主从复制里的binlog回放,以及事件溯源架构里的Event Sourcing。好处非常明显:
- 流量小,只传增量;
- 服务器不需要知道业务细节,只需要“按顺序执行指令”并回执水位;
- 客户端和服务端的“同步状态”可以被精确描述为“本地日志已同步到某个位置”,排查问题非常方便。
3.2 同步协议设计:干净、可扩展
给服务器定的同步接口很简单,就两个:
text复制POST /api/v1/sync/push // 客户端将新增操作日志批量上传
POST /api/v1/sync/pull // 客户端拉取其他设备产生的操作日志
push请求体:
json复制{
"deviceId": "device-uuid",
"lastSyncedOpId": "op-uuid-12345",
"ops": [
{
"opId": "op-uuid-12346",
"noteId": "note-uuid-1",
"opType": "UPDATE",
"content": "{\"title\":\"买菜清单\",\"body\":\"...\"}",
"timestamp": 1714012345678
}
]
}
服务端收到之后按opId做幂等去重,已经处理过的opId直接跳过。返回体是一个水位指针,表示服务端当前已重放到哪条日志。
pull请求更简单,携带上次拉取到的opId,服务端把之后的所有日志一次性返回。
这个协议设计有几个好处:无状态、幂等、支持批量、天然支持多设备。设备A在没网的时候攒了10条日志,设备B也在没网的时候改了同一篇笔记,两边都把自己的日志推到服务器,服务器重放完各自返回水位,矛盾就只剩下“同一篇笔记同一条内容被两边同时改”这个语义层面的冲突,那是写冲突处理逻辑要关心的事,协议本身是干净纯粹的。
3.3 冲突处理:没有银弹,只有取舍
云笔记的冲突处理,我实测下来觉得最稳妥的策略是“LWW,最后写入者胜”,再加一个“冲突副本保留机制”兜底。
具体做法是:每条操作日志的时间戳就是用户操作发生的设备本地时间。同步引擎拉取日志后,对于同一篇笔记,比较本地修改时间和远端修改时间,谁晚用谁的版本。同时,被覆盖的版本存进一个“历史版本表”,不删,普通用户永远看不到,但在客户端设置页加一个隐藏入口,高级用户可以恢复旧版本。
为什么不做更复杂的“三向合并”?我用过基于OT(操作转换)和基于CRDT(无冲突复制数据类型)的方案,实话说,它们适合多人实时协作编辑器(比如Google Docs那种),对一个单用户、多设备的云笔记来说,工程复杂度暴涨,收益却非常有限。单用户的编辑序列本质上是一条长链上的操作,真正并发的场景极少。LWW在95%的情况下都能给出正确结果,剩下5%用历史版本兜底,已经是一个非常成熟且不糟心的方案了。
3.4 WorkManager的调度策略
同步任务不能动不动就启动一个Service,Android系统对后台启动的限制越来越苛刻。WorkManager是官方推荐的后台调度方案,它能在满足系统限制的前提下让同步任务尽可能执行:
xml复制<workmanager-配置>
OneTimeWorkRequest
PeriodicWorkRequest
监听网络状态
监听充电状态
退避策略
</workmanager-配置>
核心调度逻辑:
- 应用启动时用OneTimeWorkRequest立即触发一次同步;
- 网络变化时(用ConnectivityManager监听)再触发一次;
- 每30分钟一个周期性的同步任务,使用PeriodicWorkRequest,系统可能延迟,但允许一定误差;
- 只在连上Wi-Fi时执行大体积同步(比如首次全量拉取、图片文件同步),移动网络下只同步纯文本日志,图片走懒加载。
这一套调度策略跑下来,绝大多数用户是感知不到同步过程的,后台悄悄就完成了,这就是好的同步体验。
4. Android权限适配:从7.0到14.0,一个都不省心
权限这块,我踩过的坑比写业务逻辑多得多。云笔记如果要支持“插入图片”,就要跟文件系统打交道,而这个领域的坑是逐年递增的,每个安卓版本都在收紧权限。
4.1 权限全景图
云笔记典型需要的权限如下:
| 权限 | 用途 | 适配建议 |
|---|---|---|
| CAMERA | 拍照插入笔记 | 运行时申请,拒绝后降级为选图 |
| READ_MEDIA_IMAGES | 读取图库(Android 13+) | 不需要申请,直接走照片选择器 |
| READ_EXTERNAL_STORAGE | 读取图库(Android 12及以下) | 运行时申请,注意Android 13开始被拆分 |
| POST_NOTIFICATIONS | 通知栏显示同步状态 | Android 13+需要在运行时申请 |
| INTERNET | 网络通信 | Manifest声明即可 |
4.2 分区存储:别再用File路径了
Android 10开始强制分区存储,非MediaStore管理的目录,App没权限直接用File API乱写。云笔记存储图片的最佳姿势是:
kotlin复制// 保存图片到应用专属目录,避免申请存储权限
val dir = File(context.filesDir, "note_images")
// 用Uri传递,而不是反复拼接路径
val uri = FileProvider.getUriForFile(context, "${packageName}.fileprovider", file)
在笔记中插入图片时,我是先把图片复制到应用私有目录,再用FileProvider对外提供URI。这样做的三个好处:归档由自己掌控,不受源文件位置影响;后续同步上传时直接上传私有目录里的图片;权限模型清清爽爽,不需要向用户索取存储权限。
4.3 FileProvider的配置细节
FileProvider的坑在Manifest配置上,如果你照抄网上的模板,极容易出现“provider冲突”或者“路径暴露”的问题。
xml复制<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
file_paths.xml里我是这么写的:
xml复制<paths>
<files-path name="note_images" path="note_images/" />
<cache-path name="cache_images" path="images/" />
<external-files-path name="external_note_images" path="note_images/" />
</paths>
其中的${applicationId}千万不要写成死的包名,否则你以后换应用ID或者做多渠道打包的时候,所有依赖authorities匹配的代码全部崩溃,别问我怎么知道的。
4.4 Android 13的Notification权限
从Android 13开始,POST_NOTIFICATIONS变成运行时权限。如果你不在运行时显式申请,用户永远收不到你发的新通知。很多云笔记App在Android 13设备上装完,同步失败通知、分享成功通知全被系统静默吞掉,用户一脸懵。
正确的做法是在创建通知渠道的同时,检查并申请通知权限。注意,这个权限不能偷偷申请,必须在业务场景里自然申请,比如用户第一次开启“同步提醒”的时候。违反的话Google Play审核和国内应用市场审核都会卡你。
5. 编辑器体验:云笔记的另一半江山
5.1 编辑器选型:综合权衡后选了Markdown
富文本编辑器在Android上能用的开源库就那几款,每一款都有令人崩溃的坑。我曾经调研过基于webview的富文本方案(像Quill、TipTap、Editor.js这些前端库),也试过原生Android的Spannable方案。最后的选择是:正文用Markdown格式存储,UI层以纯文本为主、关键信息用Compose的AnnotatedString渲染。 这样做的考虑是:
- Markdown是纯文本,同步数据量小,存储和传输都轻量;
- 作为云笔记,用户可能需要在电脑上编辑同一篇笔记,Markdown可以无缝对接桌面端Markdown编辑器;
- 富文本编辑器的跨端解析是地狱级复杂度,为了一个斜体效果在不同平台渲染一致,不值得投入那么大成本;
- Markdown渲染器的选择范围很大,Android上有commonmark-java、marked、markwon等成熟轮子,不用重复造。
当然纯Markdown对小白用户不太友好,所以我在UI层做了一个折中:标题区域显示纯文本,正文区域支持一种“所见即所得”的简化MDRender——把### ## #以及列表符号渲染成样式,但底层存的是原始Markdown文本。这样编辑时用户看到的是“有点好看”的文本,导出时又能拿到干净的数据。
5.2 自动保存:防崩溃的最后一根救命稻草
云笔记的自动保存策略,我用的是“防抖 + 定时兜底”。用户在EditText或者Compose的TextField里打字,不可能每输入一个字符就触发一次数据库写入,一是性能扛不住,二是会导致Room被频繁刷新,其他协程查询也被拖慢。
我的方案是:
- 每次输入变化后,开启一个500ms的防抖窗口,如果500ms内没有新的输入,就执行一次保存;
- 同时,无论有没有输入变化,每30秒强制执行一次保存,防止用户一直打字导致防抖窗口永远在刷新;
- 页面进入onPause/onStop时立即保存,这一步是保命操作,App被系统杀掉的时候,最近几秒的输入就靠它保住;
- 保存采用WorkManager封装成任务,扔到后台线程执行,不阻塞主线程。
实测下来这套策略的数据丢失率几乎为零。我把这个“自动保存”功能视为云笔记的守门员,没了它,其他功能做得再好也白搭。
5.3 列表页的加载优化
笔记列表需要展示标题、摘要、更新时间。如果直接加载所有笔记的完整content字段,内存和IO压力都很大。Room查询时我直接用SQL做截断,只查需要的字段:
kotlin复制@Query("SELECT id, title, substr(content, 1, 100) AS excerpt, updatedAt FROM notes WHERE deleted = 0 ORDER BY updatedAt DESC")
fun observeNoteList(): Flow<List<NoteListItem>>
总结起来就是:永远不要一次性把笔记正文全部加载到内存,尤其是列表页要加载几十上百篇的时候。这个优化在几千条笔记时感受不深,但到了几万条时会天差地别,启动速度和列表滑动顺滑度完全不在一个量级。
6. 服务器端设计:只做存储和分发,不做业务
客户端做完,还需要一个服务端来承接数据。这是整个系统能被称为“云”笔记的前提。
服务端我选了最省心的方案:用Spring Boot搭一个简单的HTTP服务,数据库用MySQL,数据表设计非常简单——一张sync_ops表存操作日志,一张users表做简单的账号体系。为什么不用更复杂的云厂商Serverless数据库?一是本地测试不方便,二是需要联调调试工具,本机起Spring Boot整条链路都在自己手里,排错效率翻倍。
服务端的核心接口,除了前面说的sync/push和sync/pull,还需要一个鉴权接口、一个获取用户信息接口。鉴权用JWT就够用,不要自己去搞Session、Cookie那套,移动端请求风格是“一次请求一个凭证”,JWT无状态、可水平扩展,简单且现代。
服务端要注意的一个细节是push接口必须做幂等处理。客户端在弱网环境下可能发了请求但没收到响应,很自然就会重试同一批日志。服务端如果按opId做了去重,就不会重复插入同一条笔记或产生重复操作。
6.1 数据库表:sync_ops
sql复制CREATE TABLE sync_ops (
op_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
note_id VARCHAR(64) NOT NULL,
op_type VARCHAR(16) NOT NULL,
content TEXT,
created_at BIGINT NOT NULL,
device_id VARCHAR(64) NOT NULL,
INDEX idx_user_time (user_id, created_at)
);
这里把op_id设为主键本身就是在做幂等,MySQL遇到重复主键插入会报错,代码里捕获DuplicateKeyException直接当作成功处理,干净利落。
6.2 用户体系:别做密码存储
账号系统如果自己存密码,就要考虑加盐hash、防暴力破解、改密、找回密码这一整套流程,对一个云笔记项目来说,开发周期完全不可控。我做了最轻量的方案手机号 + 验证码登录。
但验证码发送依赖于短信服务商,本地开发和测试阶段根本没法验证。如果项目只是课设、毕设或个人练手,直接用最简单的用户名+密码登录就行,但密码存储一定用BCrypt,不要用MD5、SHA1,那些都是秒破解的。BCrypt加盐后即使数据库泄露了,攻击者也拿不到明文密码。
7. 崩溃排查与数据安全:云笔记不能丢的底线
7.1 数据损坏的预防
云笔记最恐怖的事故不是功能不好用,而是用户写了一年的笔记,某天升级App后数据全没了。我见过好几个开发者在社区发帖说“Room数据库升级崩了,笔记全丢”的惨案。为了避免这类问题,我的经验是:
- 数据库版本升级时,Migration必须备份原数据;
- 所有表结构变更的Migration都要写单元测试,用SQLite的inMemory数据库验证数据迁移正确性;
- 每次升级前,把数据库文件备份到应用外部目录一份,万一Migration写错了,还有机会手动恢复。
Migration测试代码长这样:
kotlin复制@Test
fun migrate1To2() {
// 创建版本1的数据库
// 插入测试笔记
// 执行Migration
// 验证版本2的表结构和数据完整性
}
这套测试跑通了,数据库升级就不用再提心吊胆。
7.2 崩溃现场记录与上报
Android的崩溃日志默认只能看logcat,很难拿不到真实用户的设备日志。接入一个轻量级的崩溃上报SDK是必要的。不用上企业级的Bugly、Firebase Crashlytics,项目初期用开源的ACRA(Application Crash Reports for Android)就够了,它能自动把崩溃堆栈和用户操作路径同步发到你的后端接口,比用户截图反馈效率高一个量级。
7.3 数据备份与导出
云笔记产品要有“导出”能力。用户信任云笔记才有长期粘性,而信任最直接的来源就是“我随时能把数据带走”。
我在App的设置页加了三个导出入口:导出全部笔记为JSON文件、导出单个笔记为Markdown、导出全部笔记为压缩包(内含正文和图片)。实现上也不复杂,就是把Room里的笔记数据序列化后写到应用专属目录,然后通过FileProvider弹分享菜单。这个功能开发量不大,但用户感知非常强,也是很多云笔记App口碑两极分化的关键功能。
8. 优化点与踩坑清单
最后把这轮开发踩过的坑和优化点集中列一下,算是一个可直接“抄作业”的检视清单。
8.1 性能优化点
- 数据库批量插入用transaction:同步引擎pull日志后批量重放操作时,如果逐条insert,几千条日志能卡死UI线程。必须用Room的withTransaction包裹,实测有50倍以上的速度差。
- AsyncListDiffer或Paging3做列表分页:笔记上千条后直接全部加载会让Compose LazyColumn的首次composition时间拉长。我用的是Paging3,按需加载,体验顺滑很多。
- 图片压缩在上传前做:一张相机照片动辄3-5MB,上传到服务器和用户流量都是负担。拍照插入后先压缩到1080px宽、质量80%,肉眼几乎看不出差别,体积却能缩小到300KB以内。
- 网络层OkHttp加缓存:对于不需要实时刷新的接口(比如用户头像、个人设置),OkHttp的Cache-Control策略能显著减少重复请求。
8.2 必须避开的坑
- ContentResolver的query游标必须close:Android的MediaStore查询如果游标忘记关闭,早期版本会内存泄漏,后期版本直接报错。建议总是用use块包裹。
- FileProvider的Uri授权要给足:通过Intent打开第三方图片裁剪、分享图片时,如果没用grantUriPermission或者没在Intent里加FLAG_GRANT_READ_URI_PERMISSION,对方App会直接Crash onPermissionDenied,这个问题排查起来极度折磨人。
- Room主键不能用AutoGenerate:云笔记这种需要离线创建的ID,自增主键在离线时无法分配,必须用客户端生成的UUID当主键。哪怕牺牲一点索引性能,这点代价很有限。
- 网络请求超时时间必须单独设置:默认OkHttp的10秒超时在弱网环境下根本不够。同步接口我设的是30秒连接超时+60秒读取超时,这样4G下传几十条操作日志才稳。
8.3 后续可以扩展的方向
这个项目做完基础版本后,我觉得往下走可以做这么几个有意义的延伸:
- 端到端加密:笔记数据在客户端加密后再上传,服务端只能拿到密文。这是隐私笔记产品(如Standard Notes)的核心卖点,对于云笔记来说技术上完全可行,只需在操作日志写入前做AES-GCM加密,密钥存Keystore。
- 笔记间的双向链接:支持[[笔记名]]语法,实现知识库互联,这是Obsidian和Roam Research的核心体验,技术上也就是做一次笔记名的索引映射。
- 语音笔记转文字:Android系统SpeechRecognizer可以做离线语音识别,虽然准确率不如云端方案,但胜在不依赖网络,适合快速记录场景。
9. 写在最后:做云笔记最值得投入的一件事
整个项目做下来,我自己的体感是:功能列表永远写不完,架构设计才是真正的胜负手。很多教程上来就教你怎么写笔记增删改查,但那只是“记事本”。如果你照着“记事本”的思路做云笔记,做到多端同步的那一天,所有代码都面临推倒重来。
我能给到最实在的建议是:先写同步协议,再写客户端功能。把“本地数据如何变成操作日志”“日志如何安全地上传”“远端日志如何无冲突地合并”这三件事想清楚,再去写笔记列表、编辑器、图片插入,你会发现整个工程从头到尾都非常丝滑,因为数据流动的方式从第一天起就是明确的。
如果你打算用这个项目找工作或者做毕设,面试官/评委最关心的也是这个问题——他问的不是“你怎么做的图片上传”,而是“你的同步协议怎么设计,遇到冲突怎么办”。能把同步引擎讲到“我用的是增量操作日志加LWW策略,历史版本兜底,协议支持幂等”这个层次,这一轮的深度一下子就跟普通作业拉开了。
最后分享一个开发过程中的小技巧:测试同步功能的时候别只开着Wi-Fi测,试试开飞行模式编辑几篇笔记,再关掉飞行模式看同步结果;也试试在A设备编辑完立刻在B设备拉取,制造各种“时间线交错”。这些边界场景跑一遍,你比跑十遍“正常操作”学到的都多。
