多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Android景区旅游APP开发:百度地图API与LoadRunner压测

Android景区旅游APP开发:百度地图API与LoadRunner压测 简介本资源是一份面向高校学生与Android初学者的移动端开发实践文档围绕某景区旅游软件的设计与实现展开可用于课程设计、毕业设计选题参考或移动应用入门练手。压缩包内共1个PDF文件约2.96MB内容为完整的论文式技术报告涵盖绪论、相关技术、需求分析与系统设计、系统实现与优化、测试与评估等章节。全文以Android原生开发为基础讲解三层架构划分、旅游信息展示、天气预报接口调用、地图导航集成等模块设计并说明使用HashMap缓存图片等资源以降低性能损耗、借助百度地图API实现地图与导航功能同时给出基于LoadRunner的并发与内存占用测试思路。资源还对比了原生、Web与H5等开发模式及主流地图API的差异可帮助读者理清从需求分析到功能实现、性能优化与测试评估的完整链路。目前已有1088人学习适合需要系统了解景区类旅游App开发流程的读者参考。1. 从景区信息孤岛到 Android 端一站式入口景区旅游信息经常散落在公众号、第三方平台和纸质地图里游客要查景点、天气、酒店、路线往往在多个应用之间来回跳。这个 Android 项目把某景区的移动端旅游软件当成一个完整系统来设计原生 Android 做壳百度地图 API 做地图与导航景点查询和酒店预订走 REST 接口图片资源用 HashMap 做内存缓存最后用 LoadRunner 压并发。它不是单页 demo而是包含三层架构、数据表设计、移动端性能优化和真机联调的课程设计级工程。后面按工程骨架、地图集成、数据链路、并发测试、上线前验证五段推进代码和参数都能直接对照改造。2. Android 原生选型与工程骨架从 Android Studio 到三层架构落地2.1 为什么这个景区项目选原生而不是 H5景区软件的核心页面不是静态图文而是地图、定位、图片列表和离线数据。移动端 H5 开发快改版不用发版但调用百度地图原生 SDK、蓝牙、传感器、文件缓存时受限明显。原生 Android 可以直接拉起地图 SDK、写入 SQLite、控制线程池弱网下能先把景点列表和酒店信息落到本地。混合开发适合旅游信息平台这种资讯页壳工程提供 WebView内容由 H5 更新。| 维度 | 原生 Android | 移动端 H5 | 混合开发 | | 地图能力 | 可直接调百度地图 SDK | 只能用 JS API | 桥接调原生地图 | | 离线缓存 | SQLite/文件缓存完整 | 弱 | 取决于壳能力 | | 性能 | 高可控制线程与内存 | 受浏览器内核限制 | 中等 | | 发版 | 应用市场审核 | 服务端更新 | 壳审核加网页更新 | | 适合模块 | 地图导航、图片列表 | 旅游资讯、活动页 | 资讯加分享 |选型结论是地图导航、景点查询、酒店预订做原生旅游信息平台可嵌 H5。这样既能保证移动端性能又不会把资讯运营绑死在发版上。2.2 用 Android Studio 建工程与 Gradle 依赖清单Android Studio 安装后先别急着写代码SDK Manager 里把 Platform-Tools、Build-Tools、对应 Android SDK Platform 补齐否则 adb 和 aapt 会缺。汉化插件不影响编译但团队协作时建议保持英文界面报错搜索命中率更高。创建 Empty Views Activity包名用com.example.scenic语言可选 Java 或 Kotlin课程设计里 Java 资料更多企业项目更偏向 Kotlin。android { compileSdk 34 defaultConfig { applicationId com.example.scenic minSdk 23 targetSdk 34 versionCode 1 versionName 1.0 } sourceSets { main { jniLibs.srcDirs [libs] // 百度地图 so 文件放这里 } } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.recyclerview:recyclerview:1.3.2 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.12.0 // 百度地图 SDK官方下载 jar 放 libsso 放 jniLibs }compileSdk和targetSdk决定系统行为34 会启用分区存储和前台服务限制minSdk 23覆盖 Android 6.0定位权限必须运行时申请。Retrofit 2.9.0 与 OkHttp 4.12.0 是常见组合实际项目按官方最新稳定版锁定。百度地图 SDK 如果不用 Maven 坐标就要手动放 jar 和 so并把jniLibs.srcDirs指向libs否则真机运行会报UnsatisfiedLinkError。2.3 三层架构在包结构中的映射三层架构不是画在论文里的方框它要落到包名和依赖方向。表现层只处理 UI 和用户输入业务层做 Repository 和 ViewModel数据层封装 Retrofit、Room、LruCache。依赖方向从 ui 指向 domain再由 domain 指向 data数据层不反向引用 ui。com.example.scenic ├── ui │ ├── scenic # 景点列表与详情 │ ├── hotel # 酒店查询与预订 │ ├── weather # 天气卡片 │ └── map # 地图与导航 ├── domain │ ├── model # Scenic、Hotel、Weather 实体 │ └── repository # 业务仓库接口 ├── data │ ├── remote # Retrofit Service │ ├── local # Room DAO、SharedPreferences │ └── cache # LruCache、HashMap 图片缓存 └── common ├── net # 拦截器、超时配置 └── util # Bitmap、日期、坐标工具ui里的 Activity 不直接 new Retrofit而是通过 Repository 拿数据方便测试时替换假数据。domain/model保持纯 Java 对象别塞 Android 依赖。data/cache放图片缓存和 URL 索引后面 LoadRunner 压测时也方便单独统计缓存命中率。2.4 权限与网络配置别让第一个请求就失败Android 6.0 之后定位权限要动态申请Android 9 之后默认禁止明文 HTTPAndroid 10 之后外部存储分区。很多旅游软件第一次打开地图空白不是代码写错而是权限或网络安全配置没补齐。AndroidManifest 里先把静态权限声明好运行时再按场景申请。uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / application android:name.ScenicApplication android:usesCleartextTrafficfalse android:networkSecurityConfigxml/network_security_config /applicationnetwork-security-config domain-config cleartextTrafficPermittedfalse domain includeSubdomainstrueapi.example.com/domain /domain-config /network-security-configINTERNET和ACCESS_NETWORK_STATE安装时自动授予ACCESS_FINE_LOCATION进入地图页时用ActivityCompat.requestPermissions申请用户拒绝后要给出跳转设置页的入口。usesCleartextTrafficfalse强制 HTTPS如果调试接口只有 HTTP可以临时在 debug 包里放开发布包不要放开。network_security_config可以精确控制允许明文传输的域名比全局开关安全。| 权限 | 用途 | 申请时机 | | INTERNET | 请求景点、酒店、天气接口 | 安装时自动 | | ACCESS_NETWORK_STATE | 判断网络是否可用 | 安装时自动 | | ACCESS_FINE_LOCATION | 地图定位与路线规划 | 进入地图页时动态申请 | | ACCESS_COARSE_LOCATION | 网络定位兜底 | 与精确定位一起申请 |3. 百度地图 API 集成与景区导航模块实现3.1 申请密钥与 SHA1 签名绑定百度地图开放平台创建应用时选 Android SDK填包名和 SHA1。SHA1 不是随便复制的debug 和 release 签名不同两个都要填否则装 release 包后地图直接白屏。获取 debug SHA1 可以用 keytool也可以直接在 Android Studio Gradle 窗口执行signingReport。keytool -list -v -keystore ~/.android/debug.keystore \ -alias androiddebugkey \ -storepass android \ -keypass android命令里的-keystore指向调试密钥库Windows 一般在C:\Users\用户\.android\debug.keystore。输出里找 SHA1 那一行复制到百度地图后台。release 签名要用自己的 jks 文件执行同样命令。包名必须和applicationId完全一致包括大小写。后台保存后有一定生效时间刚改完 AK 别急着判断代码问题。3.2 地图初始化与景点标注百度地图 SDK 初始化建议放 Application不要在 Activity 里反复初始化。隐私合规接口要在initialize之前调用否则部分版本会拒绝启动。public class ScenicApplication extends Application { Override public void onCreate() { super.onCreate(); SDKInitializer.setAgreePrivacy(this, true); SDKInitializer.initialize(this); } }地图页用MapView生命周期必须和 Activity 对齐。onResume、onPause、onDestroy都要转发否则切后台再回来地图灰屏或者退出后地图线程还在跑。public class ScenicMapActivity extends AppCompatActivity { private MapView mMapView; private BaiduMap mBaiduMap; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scenic_map); mMapView findViewById(R.id.bmapView); mBaiduMap mMapView.getMap(); LatLng point new LatLng(30.123456, 120.123456); MarkerOptions options new MarkerOptions() .position(point) .title(游客中心) .icon(BitmapDescriptorFactory.defaultMarker()); mBaiduMap.addOverlay(options); } Override protected void onResume() { super.onResume(); mMapView.onResume(); } Override protected void onPause() { super.onPause(); mMapView.onPause(); } Override protected void onDestroy() { super.onDestroy(); mMapView.onDestroy(); } }LatLng第一个参数是纬度第二个是经度顺序写反地图会飘到海里。MarkerOptions可以设title、icon、draggable点击标记后弹 InfoWindow 显示景点简介。MapView的onDestroy必须调用它内部持有 GL 线程漏掉会泄漏 Activity。3.3 步行/驾车路线规划与导航跳转景区内部适合步行路线规划周边酒店适合驾车路线。百度地图 SDK 提供RoutePlanSearch回调里拿WalkingRouteResult再用 Overlay 画线。RoutePlanSearch search RoutePlanSearch.newInstance(); search.setOnGetRoutePlanResultListener(new OnGetRoutePlanResultListener() { Override public void onGetWalkingRouteResult(WalkingRouteResult result) { if (result null || result.error ! SearchResult.ERRORNO.NO_ERROR) return; WalkingRouteOverlay overlay new WalkingRouteOverlay(mBaiduMap); mBaiduMap.setOnMarkerClickListener(overlay); overlay.setData(result.getRouteLines().get(0)); overlay.addToMap(); overlay.zoomToSpan(); } // 驾车回调同理换 DrivingRouteResult 和 DrivingRouteOverlay }); PlanNode start PlanNode.withLocation(new LatLng(30.123, 120.123)); PlanNode end PlanNode.withLocation(new LatLng(30.456, 120.456)); search.walkingSearch(new WalkingRoutePlanOption().from(start).to(end));PlanNode.withLocation接收 BD09LL 坐标如果第三方接口返回 GPS 坐标要先转成百度坐标否则路线会偏几百米。zoomToSpan自动缩放显示整条路线。搜索实例用完要destroy()避免重复注册监听。导航跳转可以拉起手机里已安装的地图应用但坐标类型和包名要匹配调不起来就回退到应用内地图。3.4 常见报错与排查表| 现象 | 常见原因 | 处理 | | 地图空白 | AK 与包名或 SHA1 不匹配 | 重新生成 SHA1检查applicationId| | 定位失败 | 未动态申请定位权限 | 进入地图页时申请ACCESS_FINE_LOCATION| | 路线规划无结果 | 起终点坐标类型不对 | 统一转成 BD09LL | | 混淆后闪退 | 未 keep 地图类 | 加-keep class com.baidu.** { *; }| | 内存持续增长 | MapView 未销毁 | 对齐 onResume/onPause/onDestroy | | 真机无 so | jniLibs 目录没配 | 确认jniLibs.srcDirs [libs]|4. 景点查询、酒店预订与天气模块的数据链路设计4.1 接口分层Retrofit OkHttp 封装景点列表、酒店列表、天气数据都走 REST 接口Retrofit 负责声明式调用OkHttp 负责超时、日志和拦截器。接口定义尽量按业务模块拆不要把所有方法塞进一个类。public interface ScenicApi { GET(scenic/list) CallApiResponseListScenic getScenicList( Query(city) String city, Query(page) int page); GET(hotel/list) CallApiResponseListHotel getHotelList( Query(scenicId) String scenicId); GET(weather) CallApiResponseWeather getWeather( Query(city) String city); }OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .addInterceptor(new HttpLoggingInterceptor() .setLevel(HttpLoggingInterceptor.Level.BASIC)) .build(); Retrofit retrofit new Retrofit.Builder() .baseUrl(https://api.example.com/) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); ScenicApi api retrofit.create(ScenicApi.class);baseUrl必须以/结尾否则GET(scenic/list)会拼错。Query自动做 URL 编码中文城市名不用手动转。connectTimeout和readTimeout按景区弱网环境调大10 秒和 15 秒是经验值。HttpLoggingInterceptor只在 debug 包开 BASIC别在 release 里打印完整响应体。4.2 RecyclerView 列表与多布局景点卡片景点列表用 RecyclerViewAdapter 里要处理 ViewHolder 复用和图片错位。绑定数据时先查内存缓存缓存没有再用线程池下载下载完成前给占位图。public class ScenicAdapter extends RecyclerView.AdapterScenicAdapter.VH { private final ListScenic data new ArrayList(); private final LruCacheString, Bitmap imageCache; public ScenicAdapter(LruCacheString, Bitmap cache) { this.imageCache cache; } NonNull Override public VH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_scenic, parent, false); return new VH(v); } Override public void onBindViewHolder(NonNull VH holder, int position) { Scenic item data.get(position); holder.name.setText(item.getName()); holder.desc.setText(item.getDesc()); Bitmap cached imageCache.get(item.getImageUrl()); if (cached ! null) { holder.cover.setImageBitmap(cached); } else { holder.cover.setImageResource(R.drawable.placeholder_scenic); loadImage(item.getImageUrl(), holder); } } Override public int getItemCount() { return data.size(); } static class VH extends RecyclerView.ViewHolder { ImageView cover; TextView name, desc; VH(View v) { super(v); cover v.findViewById(R.id.iv_cover); name v.findViewById(R.id.tv_name); desc v.findViewById(R.id.tv_desc); } } }onBindViewHolder里必须用position取数据不要持有外部可变下标。图片加载前先校验 ViewHolder 当前绑定的 URL避免快速滑动时旧图片覆盖新卡片。多布局用getItemViewType区分景点卡片、酒店卡片、天气卡片类型值从 0 开始递增。4.3 HashMap 与 LruCache 图片缓存策略原文用 HashMap 缓存图片资源HashMap 本身没有淘汰策略图片多了会 OOM。工程里常见做法是 LruCache 做一级内存缓存HashMap 记录 URL 到本地文件路径的索引或者做二级缓存标记。public class ImageCacheManager { private static final int MAX_MEMORY (int) (Runtime.getRuntime().maxMemory() / 8); private final LruCacheString, Bitmap lruCache new LruCacheString, Bitmap(MAX_MEMORY) { Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount() / 1024; } }; private final HashMapString, String urlIndex new HashMap(); public Bitmap get(String url) { return lruCache.get(url); } public void put(String url, Bitmap bitmap) { if (get(url) null) { lruCache.put(url, bitmap); urlIndex.put(url, url); } } }MAX_MEMORY取可用堆的 1/8是移动端图片缓存的常见上限。sizeOf返回 KBLruCache 按这个值累计淘汰。urlIndex可以换成 URL 到文件路径的映射做内存加磁盘二级缓存。如果项目已经引入 Glide优先用 Glide它内部已经处理生命周期和 Bitmap 复用。| 缓存方案 | 命中率 | 内存占用 | 适用场景 | | HashMap 裸存 | 高但无上限 | 容易 OOM | 小图、临时演示 | | LruCache | 高 | 可控 | 景点封面、酒店缩略图 | | LruCache 磁盘 | 很高 | 低 | 弱网、离线景区 | | Glide 默认 | 高 | 自动管理 | 正式项目推荐 |4.4 酒店预订与天气模块的本地缓存酒店列表不常变天气变化快缓存时长要分开。用 Room 存酒店用 SharedPreferences 或 Room 存天气 JSON表里加updateTime判断过期。Entity(tableName hotel) public class HotelEntity { PrimaryKey NonNull public String hotelId; public String name; public String price; public String address; public long updateTime; }public boolean isExpired(long updateTime, long ttlMillis) { return System.currentTimeMillis() - updateTime ttlMillis; }酒店缓存 TTL 可以设 24 小时天气缓存 TTL 设 1 小时。进入页面先读本地再发请求刷新界面不会白屏。预订动作不能只靠本地缓存提交前要校验库存和价格服务端返回成功后再写本地订单状态。| 模块 | 缓存时长 | 存储方式 | 失效策略 | | 景点列表 | 12 小时 | Room | 下拉刷新强制更新 | | 酒店列表 | 24 小时 | Room | 超过 TTL 后台刷新 | | 天气 | 1 小时 | SharedPreferences | 超过 TTL 立即请求 | | 图片 | 内存内 | LruCache | 按内存上限淘汰 |5. 移动端性能优化与 LoadRunner 并发测试5.1 内存与线程池把主线程留给 UI移动端性能优化第一条是别在主线程做 JSON 解析、图片解码、数据库写入。线程池核心线程按 CPU 核数算队列设上限拒绝策略用丢弃最旧任务防止内存无限涨。public class ThreadPoolManager { private static final int CPU_COUNT Runtime.getRuntime().availableProcessors(); private static final int CORE Math.max(2, Math.min(CPU_COUNT - 1, 4)); private static final int MAX CORE * 2 1; private static final ThreadPoolExecutor EXECUTOR new ThreadPoolExecutor( CORE, MAX, 30L, TimeUnit.SECONDS, new LinkedBlockingQueueRunnable(128), new ThreadPoolExecutor.DiscardOldestPolicy()); public static void execute(Runnable r) { EXECUTOR.execute(r); } }CORE取 CPU 核数减一最少 2 个最多 4 个避免占满 CPU。MAX是核心线程两倍加一适合 IO 和计算混合任务。队列 128 是移动端经验值太大容易堆积请求。DiscardOldestPolicy在队列满时丢最旧任务保证新请求有机会执行。图片、网络、数据库操作都走这个线程池主线程只刷 UI。5.2 图片缓存命中率与 HashMap 的取舍图片缓存命中率可以用日志统计每次get和put计数压测时看命中率是否稳定在 80% 以上。内存用 adb 查看adb shell dumpsys meminfo com.example.scenic重点看Java Heap、Native Heap、PSS Total。Java Heap 持续上涨且不回落通常是 Bitmap 没释放或 HashMap 无限增长。用 Android Studio Profiler 抓 Heap Dump找Bitmap实例数和HashMap引用链。如果 LruCache 的size()接近上限说明缓存配置合理如果远低于上限但内存还高查泄漏。5.3 LoadRunner 并发场景设计LoadRunner 压测脚本可以用 VuGen 写 HTTP 请求参数化city和page设置集合点模拟同一时刻并发。下面是一段 C 语言风格脚本片段Action() { web_reg_find(TextscenicList, LAST); web_custom_request(scenic_list, URLhttp://api.example.com/scenic/list?city{city}page{page}, MethodGET, Resource0, Referer, LAST); lr_think_time(3); return 0; }web_reg_find检查响应里是否包含scenicList防止只压到错误页。{city}和{page}来自参数列表按 CSV 轮换。lr_think_time(3)模拟用户看页面的停留时间去掉思考时间会得到虚高 TPS。场景设计按 100、300、500 并发逐级加压观察错误率和响应时间拐点。| 并发用户数 | 平均响应时间目标 | TPS 目标 | 错误率 | 内存观察 | | 100 | 小于 1.5s | 大于 60 | 小于 1% | Java Heap 平稳 | | 300 | 小于 2s | 大于 150 | 小于 1% | 无持续上涨 | | 500 | 小于 3s | 大于 200 | 小于 2% | Native Heap 可控 | | 800 | 小于 5s | 大于 220 | 小于 3% | 出现瓶颈定位 |5.4 测试结果怎么看响应时间与内存占用响应时间曲线在某个并发数突然抬头说明到达瓶颈。先看服务端 CPU 和数据库连接池再看客户端线程池队列是否满。内存占用在压测中如果每轮都涨压完不降重点查 MapView 生命周期、图片缓存和未取消的 Retrofit Call。错误率超过 2% 时先排除接口限流和超时再查客户端 DNS 和连接复用。LoadRunner 的Transaction Response Time图比平均值更有用P95 和 P99 能看出长尾请求。6. 签名、混淆与真机验证上线前最后三公里6.1 生成签名与 SHA1 复用的坑release 签名用 Android Studio 生成 jks别把密码和密钥文件提交到 Git。Gradle 里配置签名release 包才能装到真机做最终验证。android { signingConfigs { release { storeFile file(scenic.jks) storePassword ****** keyAlias scenic keyPassword ****** } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } }release SHA1 和 debug SHA1 不同百度地图后台两个都要填。签名文件丢失后无法覆盖安装只能换包名重新上架所以 jks 至少备份两份。6.2 混淆规则对地图 SDK 与数据模型的影响开启混淆后地图 SDK 的 JNI 回调和 Gson 模型最容易出问题。地图类被混淆会导致初始化失败或点击标记闪退模型字段被混淆会让 JSON 解析为 null。-keep class com.baidu.** { *; } -keep class vi.com.gdi.bgl.android.** { *; } -keep class com.example.scenic.domain.model.** { *; } -keepattributes Signature -keepattributes *Annotation*-keep class com.baidu.**保留地图 SDKvi.com.gdi.bgl.android.**是地图依赖的底层类domain.model保留 Gson 实体字段。Signature和Annotation保留泛型和注解信息。混淆后必须真机跑一遍地图、列表、天气、预订四条链路。6.3 真机验证清单| 验证项 | 操作 | 预期 | | 地图初始化 | 冷启动进入地图页 | 底图加载无白屏 | | 定位权限 | 首次进入地图页 | 弹出授权拒绝后提示设置 | | 图片列表 | 快速滑动景点列表 | 无错位无 OOM | | 路线规划 | 选两个景点算步行路线 | 画出折线缩放合适 | | 弱网 | 限速后下拉刷新 | 显示缓存超时提示 | | 释放安装 | 安装 release 包 | 签名一致可覆盖 |真机安装后先用adb logcat | grep -i baidumap过滤地图初始化日志再点开景点详情连续旋转屏幕三次确认 MapView 生命周期没有重复初始化。本文还有配套的精品资源点击获取
返回列表