观测记录本App,光听名字会以为是个备忘录换皮。真正做技术支持之后我才发现,这个品类踩过的坑,几乎覆盖了中小型工具类App从开发到上线维护的完整链路:数据模型怎么设计、离线数据怎么存储、安卓机型碎片化怎么适配、iOS怎么分发、线上同步故障怎么定位、用户工单怎么转化成测试用例。这篇文章不聊花哨的架构,就是围绕观测记录本App在真实运营中遇到的技术问题、排查过程和最终沉淀下来的方案,完整盘一遍。如果你正在做工具类App,或者准备把一个垂直领域做成App,很多经验可以直接抄。
全文的核心就一句话:观测记录本App不是"能记就行",而是要在"户外环境差、网络不稳定、用户设备五花八门"的现实条件下,做到快速录入、离线可靠、同步不丢数据。围绕这个目标,我把整个项目拆成八个部分来复盘。
1. 观测记录本App的定位与数据模型:先把"记什么"想清楚
1.1 三类典型用户和他们的记录习惯
我在做技术支持时接触到的用户,主要可以分成三类:观鸟爱好者、物候记录者、小型气象/环境观测爱好者。这三类人的记录习惯差异非常大,直接决定了数据模型不能只设计成"标题加正文"。
观鸟爱好者讲究"在野外快速记一笔",他们最在意的是录入速度。看到一只不认识的鸟,可能只有几秒钟时间掏出手机,需要一键新建记录、默认带上当前时间和定位,然后快速选择鸟种、数量、行为状态。物候记录者则更关注"周期性",比如某棵树的发芽日期、某片区域的初雪日期,每年同一时间都要记,所以App必须支持"去年的今天记了什么"这种回顾逻辑。小型气象观测爱好者往往会搭配传感器设备,比如用ESP32做的温湿度采集器,通过蓝牙把数据直接喂进App,而不是手输数字。
用户的差异化需求说明一个道理:观测记录本App的核心竞争力不在"笔记功能",而在"结构化字段"和"现场录入效率"。我把每一条观测记录抽象成了统一的字段模型,不管用户是观鸟还是记物候,底层都是一套结构。
1.2 单条观测记录的数据模型是怎么设计的
最初版本我参考的是传统纸质观测记录表的格式,字段包括:观测对象、观测时间、经纬度、数量、数值(温度/湿度/风速等)、照片、备注。后来发现还缺了几样东西:观测者的自定义标签、记录的状态(草稿/已提交)、以及关联设备数据。
最终的数据模型长这样:
| 字段 | 类型 | 说明 |
|---|---|---|
| record_id | 字符串(UUID) | 主键,客户端生成,离线也能保证唯一 |
| object_name | 字符串 | 观测对象,比如"白头鹎"、"樱花花期" |
| object_type | 整数枚举 | 区分观鸟/物候/气象等类型,决定表单样式 |
| observed_at | 时间戳(毫秒) | 观测时间,默认当前时间,可修改 |
| latitude / longitude | 浮点数 | 定位坐标,允许手动修正 |
| quantity | 整数 | 数量,观鸟场景常用 |
| metric_value | 浮点数 | 温湿度、风速等数值 |
| photo_paths | JSON数组 | 本地照片路径,同步时上传 |
| tags | JSON数组 | 用户自定标签 |
| extra_json | JSON文本 | 各类型专属扩展字段 |
| status | 整数 | 0草稿 / 1已提交 / 2已同步 |
| updated_at | 时间戳(毫秒) | 最后修改时间,同步冲突时用 |
这里特别说明一下record_id为什么用客户端生成的UUID。因为观测场景经常在完全没有信号的山区,用户必须先离线保存,回到有网的地方再同步。如果主键依赖服务端自增ID,离线记录根本没法生成,后面所有逻辑都会卡住。UUID方案配合updated_at做最后写入优先,基本能覆盖绝大部分同步冲突场景。
1.3 为什么必须离线优先
在野外做观测,网络信号是奢侈品。很多用户是在山里、湖边、自然保护区里用这个App,那里4G/5G信号基本为零。如果App把数据存到云端、本地只做缓存,用户一进山区就直接歇菜。
所以我把存储逻辑定成了"本地数据库为唯一数据源,云端只是备份和同步目标"。所有的增删改查都先写本地SQLite,然后由同步引擎在后台排队上传。这个模式和一般的联网App有本质区别,它要求每条记录在创建时就必须具备"离开服务器也能完整存活"的能力,所以上面提到的客户端UUID、本地照片路径、状态机设计都是围绕这个目标展开的。
这个设计带来的好处是:用户永远不会因为没网而丢掉一条记录,最多是看到"待同步"的提示。坏处也很明显,就是同步冲突处理、多端一致性这两块复杂度会变高,后面第六章会专门讲我踩过的一个同步大坑。
2. 技术选型:跨平台框架、本地数据库和定位模块怎么组合
2.1 跨平台方案:为什么我没有选纯H5壳
观测记录本App的早期原型其实是一个H5页面套壳,开发速度确实快,但上线后问题不少。最典型的是在弱网环境下,H5页面的加载体验非常差,用户经常打开后白屏等好几秒;另外相机拍照、蓝牙连接这类系统能力,H5套壳做起来要么绕路要么不稳。
后来我换成了跨平台原生渲染方案,具体是Flutter。选它不是因为它比React Native强多少,而是因为我对Dart语言的熟悉度更高,而且它自带的渲染引擎在低端安卓机上的表现更稳定。这里有个经验:跨平台框架的选择,别只看社区热度,要看你的核心场景是哪一类。观测记录本的核心场景是表单录入、地图定位、相册、蓝牙,这些在Flutter里都有成熟插件;如果你主要是视频类App,那又是另一套选法。
2.2 本地数据库:SQLite系比想象中更可靠
离线优先的App,本地数据库就是心脏。我用的是SQLite系的Drift库,而不是NoSQL方案。原因很朴素:观测记录的数据模型高度结构化,字段固定、关联明确(记录和照片、记录和标签都是明确关系),SQL在查询和统计上的表达能力更强。比如用户问"去年3月记录过哪几种鸟",一条简单的SQL查询就能搞定,换成文档数据库反而要写一堆mapping逻辑。
Drift库在安卓和iOS上都表现稳定,事务机制完善。我最看重的一点是它支持类型安全的表定义,编译期就能发现SQL语句里的字段拼写错误,这在后期维护时省了非常多的心。如果你用的是原生安卓,Room也是同样的思路,核心原则就是:离线数据必须有可靠的事务机制,绝对不能"写着写着崩了,数据全没了"。
2.3 定位与地图:省电和精度怎么平衡
观测记录对定位的精度要求不算特别高,但在野外,GPS定位模块的电量消耗非常可观。我采用的策略是"循环定位+超时降级":进入新建记录页面时,先读取上一次缓存的位置立即填充,然后在后台启动一次精度较高的定位请求,如果10秒内拿到新位置就刷新,拿不到就沿用缓存。
地图展示选的是高德系SDK,主要考虑国内用户的使用习惯和离线地图支持。一个反直觉的坑是:地图SDK初始化必须放在App启动后的一个独立线程,不能在主线程同步等待,否则低端安卓机会出现明显的启动卡顿。很多用户反馈"App打开慢",最后查下来不是业务代码的问题,而是地图SDK初始化阻塞了主线程。
2.4 后端服务:用Django快速搭同步接口
后端我用的是Python的Django框架。观测记录本的同步接口并不多,核心就三个:批量上传记录、拉取远端更新、图片文件上传。Django的ORM和Admin后台帮我省了很多重复工作,特别是调试阶段,直接在Admin后台就能看到用户提交的原始数据,排查问题效率极高。
Django项目里我按业务域拆了几个子应用,分别是records(记录)、users(用户)、files(附件)。每个子应用独立管理自己的模型和视图,接口统一走DRF(Django REST Framework)。这套组合拳对于中小型App后端来说性价比很高,真要到了用户量爆炸的阶段,再考虑拆分微服务也不迟,前期别过度设计。
3. 被问得最多的交互问题:字太小、表单太长、户外看不清
3.1 "字太小"工单催生的字体设置功能
我接到过一条典型工单:一位做物候记录的老用户,说在户外阳光下看手机,记录页的字小到看不清,每天要眯着眼睛填数据,特别难受。这条工单让我意识到,观测记录本的用户里有相当比例是退休的科研人员、自然爱好者,他们的视力条件和使用场景(户外强光)决定了"字体大小可调"不是锦上添花,而是刚需。
字体设置功能听着简单,实现起来有讲究。不能只做一个全局开关,而是要分为三档:界面字体大小、记录内容字号、辅助信息(时间戳、坐标)字号。我采用了全局字体系数方案,在设置页保存一个scale值(比如1.0/1.2/1.4),所有文本组件统一乘以这个系数。这样用户调一次,整个App的阅读体验都跟着变,不会出现"列表变大了、详情页没变"的割裂感。
这里有个容易被忽视的细节:字体变大后,很多固定高度的卡片和按钮会溢出。所以做字体缩放功能时,所有容器不能写死高度,要用最小高度加自适应约束。这个适配工作量比想象中大,但做完后用户好评度非常高,评论区很多人专门感谢这个功能。
3.2 动态表单:让不同观测类型共用一套录入界面
观测记录本要支持观鸟、物候、气象等多种类型,每种类型的字段都不一样。如果给每种类型单独写一个页面,代码会爆炸式增长,后面加新类型就得改一版。
我采用的是配置驱动表单方案:后台定义每种观测类型的字段配置(字段名、类型、是否必填、取值范围),客户端拿到配置后动态渲染表单。比如观鸟类型需要"数量"和"行为状态"字段,气象类型需要"温度"和"湿度"字段,这些通通在配置中心维护。这个方案的另一个好处是,服务端可以在不发布新版本的情况下,给老用户增加新字段。
不过动态表单的代价是,用户输入的校验逻辑也要配置化。我必须在配置里声明每个字段的校验规则,比如温度范围是-50到60,数量只能是正整数。这块如果规则写得不严谨,就会导致用户填了非法数据,同步后后端再报错,一来一回用户体验很糟糕。
3.3 深色模式和高对比度不只是审美问题
观测用户在户外使用手机,强烈阳光下屏幕的可读性非常差。我一开始只做了深色模式适配,后来实测发现,深夜观星场景下用户需要深色背景,白天户外强光下用户需要高对比度浅色背景,两者完全不同。
最终我做的是三套主题:浅色、深色、高对比。高对比模式专门针对户外强光,使用纯黑文字、纯白背景、大号字体。系统自动切换之外,也允许用户在设置页手动锁定。这个功能在气候、天文观测场景里特别受欢迎,尤其是拍星空时需要看星等数据,屏幕太暗或者反光都会让用户崩溃。
4. Android用户装不上App:一条典型的"解析包错误"排障链路
4.1 排障顺序:先分版本,再分机型
安卓无法安装的问题,是我处理工单时最头疼的一类,因为"装不上"背后的原因可能五花八门。我的经验是:拿到"无法安装"的反馈后,第一件事不是猜原因,而是问清楚两个信息——安卓系统版本和手机品牌型号。这两个信息能过滤掉80%的可能性。
比如用户说"安装时提示解析包错误",在安卓7以下的机型上,最常见的原因是APK的targetSdkVersion过高,系统不认。而在安卓12以上的机型上,多半是安装来源未授权问题。如果提示的是"安装失败,与现有应用签名不一致",那是签名冲突。每一类问题对应完全不同的处理路径,所以技术支持的第一步永远是分级分类,而不是拿着一个方案去套所有机器。
4.2 一个让我折腾一晚上的ABI拆分教训
有一次,为了减小安装包体积,我尝试了APK按ABI拆分,分别构建出armeabi-v7a、arm64-v8a、x86三个版本。构建成功之后,我拿自己的arm64测试机装上没问题,就发了链接给用户。结果有用户反馈:手机是安卓9,安装包下载后点击安装,提示"未安装应用"。
排查到最后才发现,那位用户的手机是几年前的入门机,CPU是32位的,只能装armeabi-v7a包,但我的下载链接默认给的是arm64-v8a。这种问题在测试阶段很难暴露,因为我的测试机都是64位。解决方案是:用App Bundle格式替代手动拆分,让应用商店按设备自动下发对应ABI;同时在做灰度分发时,准备一个包含所有ABI的fat APK作为兜底。安装包体积可以后面再优化,但用户装不上才是真灾难。
4.3 64位适配不是选择题
安卓市场对64位架构的要求已经是硬性的了。观测记录本App早期用过一个老旧的定位SDK,它只有32位版本。这导致我在打包时必须额外包含armeabi-v7a的so库,包体变大不说,还拖累了64位机型的启动速度。
后来我把所有原生依赖全部排查了一遍,凡是没提供64位版本的库,尽量替换掉或升级到新版本。其中最痛苦的是一个蓝牙通信库,项目老,维护少,一直停留在32位。最后方案是fork源码自己编译64位版本,这件事让我深刻体会到:选第三方SDK时一定要提前确认架构支持情况,不然早晚卡在打包上线这一步。
4.4 targetSdkVersion升级踩坑
为了满足应用市场上架要求,targetSdkVersion需要不断往上升级。这个升级不是改个数字那么简单,每次升级都意味着新的系统行为变更。从API 29升级到API 30那次,我遇到的坑是分区存储(Scoped Storage)导致照片路径失效;再往上升级时,又遇到了前台服务定位权限的调整。
这些系统级别的变更,不会在开发阶段立刻暴露,因为开发调试用的是debug包,很多权限默认宽松。真正的麻烦在于老用户升级新版本后,原本能用的功能突然不能用。我给自己的铁律是:升级targetSdkVersion前,必须把release包在自己手里完整过一遍全部主流程,尤其是存储、定位、相机、蓝牙这几个最容易踩坑的模块。
5. iOS安装、分发与浏览器唤起:从小范围测试到正式上线的几条路
5.1 TestFlight、企业证书和App Store怎么选
iOS这边的分发方式,是很多开发者的知识盲区。观测记录本App在测试阶段用TestFlight,发布正式版走App Store,这是最规范也最安全的路。但有些场景下,比如给一个植物园做了定制版,不方便上架App Store,就需要企业证书分发。
企业证书分发的坑特别多:证书很容易被苹果封禁,一旦被封,所有装了该App的用户的App都会闪退打不开。所以我的建议是:能上App Store就上App Store,能用TestFlight就用TestFlight。企业签名只在特定项目里用,并做好用户端无法升级的预案。很多"安装后打不开"的工单,最后查下来是企业证书被吊销了,这种问题你技术再强也救不了,只能引导用户走正规渠道。
5.2 浏览器唤起安装App的实现细节
用户反馈里有一个高频场景:在浏览器里打开官方网站,看到下载App的按钮,点击后希望直接唤起App或跳转到App Store。这个功能名叫"浏览器唤起App",实现方式主要有两种:URL Scheme和Universal Links。
URL Scheme是历史悠久的方式,比如注册一个obsrecord://这样的协议,网页里用location.href跳转。它的缺点是:如果用户没装App,跳转会失败且没有优雅的降级。Universal Links是苹果推荐的方案,通过配置apple-app-site-association文件实现域名与App的关联,用户体验更好。我实际做下来最大的坑是:Universal Links在App首次安装后、直接点击链接时不一定能正常唤起,需要用户先从App内打开过一次,域名关联才会生效。这不算bug,但需要运维在说明页里写清楚引导步骤。
5.3 聊天软件内链接打不开的坑
很多用户是在聊天软件里看到朋友分享的下载链接,点击后默认使用内置浏览器打开。这种内置浏览器对Universal Links和URL Scheme的支持参差不齐,经常出现"打开了网页,但点击安装按钮没反应"的情况。
我最终的方案是:在网页上做检测,如果是聊天软件内置浏览器,就强制展示一张"右上角选择用系统浏览器打开"的引导图。虽然多了一步,但能显著减少用户被卡住的概率。这里各位要注意,引导图要做得简单直接,最好是一张带红框的截图,用户一眼就能看清往哪点。
6. 同步失败类工单的实战排查:从抓包到服务器时间的完整链路
6.1 "上传失败"工单如何分类
"我明明有网络,为什么记录同步不上去"是我处理最多的工单类型。后来我总结出一套分类模板:先把问题分成"完全不能同步"和"部分记录同步失败"两类。完全不能同步,优先检查网络权限、账号状态、服务端接口状态。部分同步失败,优先检查失败记录本身的内容,比如某条记录的照片太大、某个字段格式不对、某条记录的时间字段异常。
分类之后,排查路径就清晰了。很多开发者一上来就查代码,其实是走了弯路。我通常会先让用户提供两条信息:一条是App版本号,另一条是失败记录大概是什么时候创建、带不带照片。这两个信息能快速定位是不是特定版本引入的bug,以及是不是多媒体文件导致的超时。
6.2 抓包失败的三种原因
要定位同步问题,抓包是绕不开的手段。但很多开发者刚接触抓包时会遇到一个现象:手机电脑都设置好了,代理也配了,可就是看不到请求内容,App直接报网络错误。抓包失败,通常有三个原因。
一是App启动了证书校验(SSL Pinning)。服务器证书被固定了,抓包工具的证书会被判定为不合法,TLS握手直接失败。这种情况需要开发阶段在代码里留一个调试开关,允许关闭证书校验。二是抓包工具本身没配置好,比如只抓了HTTP没抓HTTPS,或者证书没有安装到系统信任区。三是最容易被忽略的:服务器的系统时间不对,导致TLS证书在握手时验证失败,抓包工具显示一片红。
6.3 一个被忽略的元凶:服务器系统时间
我印象最深的同步故障,是某段时间用户反馈集中爆发,App显示"无法连接服务器",但服务器负载很低、接口看起来完全正常。我抓包看到的画面是:请求全部走到TLS握手阶段就断了,错误是证书过期,可这张证书明明刚续过费。
折腾了大半天,最后登录服务器检查系统时间,发现服务器时间慢了整整两个月,导致所有依赖系统时间做校验的Https证书全部判定为不在有效期内。问题根源是云服务器的时间同步服务没有启动,修复就一行命令。这个教训让我养成了习惯:遇到所有TLS握手异常,第一件事先检查服务器时间,而不是怀疑证书配置。这类问题极其隐蔽,网络排查经验不足的人很容易在这个坑里浪费好几个小时。
6.4 给App加上可读的错误码体系
之前的App在同步失败时只弹一个"网络错误",用户根本看不懂,提交工单也只能说"不知道哪里错了"。后来我引入了一套错误码体系:本地错误码、服务端错误码、网络层错误码分层定义。比如E_SYNC_TIMEOUT表示同步超时,E_SYNC_AUTH_FAILED表示登录态失效,E_SYNC_CONFLICT表示记录冲突。
每个错误码在界面上都有对应的文案和解决建议。用户以后反馈问题时,只要把屏幕上的错误码发过来,我就能在后台快速定位。这个改动看起来不起眼,但对技术支持效率的提升是革命性的,因为以前平均每个工单要来回问三句话才能定位问题,现在一眼就能看到是哪一类故障。
7. 维护期才懂的自动化测试与真机适配策略
7.1 自动化测试环境从零搭建
观测记录本App功能不算多,但改一个字段、动一次数据库表结构,都可能影响全流程。我早期靠手工回归测试,后来发现每次发布前都要花大半天,而且容易漏。于是我搭了一套App自动化测试环境,核心是Appium加WebDriverAgent的方案。
测试脚本覆盖的是主流程:启动App、新建观测记录、拍照、离线保存、恢复网络、触发同步、检查服务端数据。这些用例写好后,每次发版前只需要跑一遍,约二十分钟就能完成基础回归。自动化脚本最大的价值不在于"不用手工点",而在于它能在晚上睡觉时把老版本兼容性、新版本数据迁移这些容易出问题的环节快速跑一遍。真机测试设备,我保留了四台:一台安卓低端32位机、一台安卓主流中端机、一台旧iOS设备、一台新iOS设备,基本覆盖了用户群体的设备分布。
7.2 真机适配清单和优先级
安卓设备碎片化是绕不开的。我做了一套适配优先级:核心流程(新建记录、同步、登录)必须覆盖所有主流厂商ROM,次要流程(蓝牙、地图)做到主流机型不闪退。所谓主流厂商ROM,就是用户量最大的那几个品牌的系统UI,它们对后台权限、通知权限的处理方式差异巨大,很多"App闪退"问题其实是某个ROM的后台限制导致。
具体来说,我在测试清单里列了:模拟原生安卓、品牌A系列、品牌B系列各一台,覆盖安卓10到安卓14。iOS那边相对简单,但我也会保留一台旧机型,因为旧系统的WebView组件和新系统行为差异可能导致某些页面渲染错乱。适配这种事,花钱买真机比花时间跟用户来回沟通要划算得多。
7.3 蓝牙传感器联动的回归测试
观测记录本的进阶功能是连接BLE传感器,比如ESP32自制的温湿度采集器。这个功能在测试阶段非常容易遗漏,因为需要同时具备硬件设备和App环境两个条件,手工测试成本很高。
自动化测试里我加了一条专用链路:用一个模拟BLE设备的脚本,向App广播标准的温湿度数据,验证App端能正确解析、显示并生成观测记录。这个脚本虽然写起来费了点功夫,但后期每次升级蓝牙相关代码,跑一遍就能拦住80%的回归问题。需要提醒的是,BLE测试最怕的是各种安卓厂商对蓝牙权限的差异化限制,这部分自动化没法完全覆盖,还是需要配合真机手动验证。
7.4 把用户工单改造成自动化用例
我最想分享的一个习惯是:把用户反馈过的每一个bug,都转化成一条自动化测试用例。比如"用户把温度字段填成-999导致同步失败",修正之后我就在测试用例库里加了一条"输入极端数值后同步"的用例。这样做的效果是,同样的bug不会在后续版本里回归复发,技术支持的压力也会越来越小。
刚开始这个改造过程很痛苦,因为很多工单问题根本没有清晰的复现步骤,需要自己先反推。但坚持大半年后,自动化用例覆盖了绝大多数历史bug,新版本发布时我不再提心吊胆。我甚至会把用户工单里的原话写进用例备注,这样跑测试时能看到当初用户遇到的实际场景,比干巴巴的测试数据有温度得多。
8. 发布、升级与加固的最后一公里
8.1 一个小型工具App的上架成本与周期
常有朋友问我,开发一个App并上架大概要多少钱。以观测记录本这类中小型工具App为例,如果完全找外包,功能完整、含前后端和测试,市场报价通常在十几万到几十万不等,周期大概两到三个月。如果自研,成本主要是一个开发加半个设计的工资,加上苹果开发者账号年费和各平台分发费用,经济成本低很多,但时间成本会很高,我前后用了差不多半年业余时间才稳定下来。
上架周期上,Android相对灵活,主流应用商店审核一般几天到两周。iOS的App Store审核更严格,首次上架可能需要一周左右,如果涉及隐私权限说明不完整,还可能被驳回。我的经验是:提前准备好隐私政策页面、权限用途说明、截图素材这些材料,审核被拒时心态放平,按反馈逐条整改就是。
8.2 OTA升级要谨慎
观测记录本App有部分定制渠道用户不通过应用商店安装,这就涉及到OTA升级。我曾经有一版升级包出现了崩溃,发出去后一部分老用户升级完App就闪退,而那部分用户根本不会再从应用商店更新,只能手动找客服要修复包。
这让我养成了两条铁律:第一,OTA包发布前必须先小范围灰度,比如先发给内部测试群,观察半天再来全量;第二,OTA升级一定要有强制版本回滚机制,如果用户App连续崩溃两次,自动弹窗引导安装上一个稳定版本。OTA升级看着方便,实际风险不比上架审核低,因为少了应用商店那层审核把关,所有问题都要自己兜住。
8.3 应用加固与数据安全
本地存储的数据安全,对观测记录本这类App来说容易被忽略,但其实很重要。用户的观测记录、定位坐标、照片都属于个人敏感信息。安卓端我对APK做了加固处理,防止被反编译直接拖走数据库文件;iOS端的Keychain用来存登录令牌,本地数据库也做了加密处理。
这里有个实用细节:数据库加密不是上来就用AES把所有字段加密,那样会导致查询性能大幅下降。我采用的是SQLCipher方案,它是对整个数据库文件加密,但上层SQL语法完全不变,性能损失可以接受。配置好之后,就算有人拿到用户的数据库文件,没有密钥也读不出明文内容。数据安全做在前面,后面接到隐私相关的合规要求时就不用手忙脚乱。
最后再分享一个经验:观测记录本这类垂类工具App,技术支持工作不能只站在"代码修bug"的角度,很多用户遇到的问题其实是使用习惯和设备环境造成的。我在处理工单时学到的做法是,先复制一份测试环境尽量复现用户路径,再打开日志看异常,最后才动手改代码。这个流程虽然慢,但让我避开了很多"凭空猜问题"的弯路。如果你也正在维护一个小而美的工具App,建议从一开始就把用户工单当测试用例用起来,维护成本会低很多。