ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android火车订票源码解析:反编译工程还原与MVC架构实践

2026/9/15 20:17:51 拓冰建站 浏览量
Android火车订票源码解析:反编译工程还原与MVC架构实践 简介这是一份面向Android初学者与中级开发者的火车订票类实战项目源码聚焦出行服务场景帮助开发者系统掌握网络请求、SQLite本地存储、UI布局设计、权限管理及第三方支付集成等核心技能。资源共231个文件涵盖29个Java业务逻辑文件、80个编译后class字节码、37个XML界面与资源配置文件、73个PNG图标资源辅以jar依赖库、APK安装包及数据库文件完整呈现从开发、调试到打包的全流程压缩包仅2.33MB轻量易学。已有182人学习下载适合用于课程设计、毕业项目参考或Android进阶实践。读者可直接运行APK体验订票流程通过源码深入理解订单查询、车次筛选、余票判断、支付回调等关键模块实现并借鉴其分层架构如ActivityDaoModel、异步任务处理Handler/AsyncTask及通知机制等工程化设计思路。1. 这不是Demo而是一套可跑通的火车订票业务闭环源码拆开Android火车订票APP源码.zip后第一眼看到的不是MainActivity.class而是resources.ap_和jarlist.cache——这说明它不是 Gradle 新建的空项目而是从真实 APK 反编译回退生成的完整工程结构。它没有build.gradle的现代依赖声明但ConfirmPayActivity.class、OrderDao.class、Change_order_query_Activity.class这些类名直指核心业务改签查询、订单确认、支付回调、本地订单持久化。它不依赖 Firebase 或云函数所有网络请求走HttpURLConnection 手写 JSON 解析数据库用原生SQLiteOpenHelper封装权限申请集中在AndroidManifest.xml中硬编码声明。适合 Android 开发者做三件事理解传统 MVC 架构在真实购票场景中的落地方式复现一套离线可用、无第三方 SDK 依赖的轻量级订票逻辑把BookingActivity到ConfirmChangeActivity的状态流转链路作为 Activity 生命周期与数据一致性设计的反面教材来精读。如果你正在带新人、维护老系统或需要快速搭建一个可演示的铁路出行原型这套源码比任何教学 Demo 都更贴近“上线前最后一版”的真实感。2. 从反编译产物还原可编译工程资源、类、清单文件的重建逻辑2.1 源码结构识别.class文件与resources.ap_的对应关系反编译得到的.class文件如MainActivity.class、OrderDao.class是 Java 字节码需通过dex2jarjd-gui工具链还原为 Java 源码。但关键在于这些 class 并非孤立存在它们依赖resources.ap_即 AAPT 打包后的资源索引归档和R$id.class资源 ID 映射表。R$id.class中定义了R.id.btn_submit,R.id.tv_train_no,R.id.lv_order_list等字段其值为整型常量如0x7f08003c这些值必须与resources.ap_中实际打包的 layout、drawable、string 资源一一对应。若直接新建 Android Studio 工程并粘贴.class源码编译会因R.id.xxx找不到而失败——因为新工程的R.java生成规则与原 APK 不一致。提示不要试图手动修改R.java。正确做法是先解压resources.ap_用aapt2 dump resources resources.ap_查看资源表再用apktool d train_ticket.apk -r -s完整反编译获取原始res/目录和AndroidManifest.xml最后将反编译出的 Java 源码而非.class导入新工程。2.2AndroidManifest.xml中隐藏的运行时约束该源码的AndroidManifest.xml需从 APK 中提取包含以下关键配置uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name android:themestyle/AppTheme activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity android:name.BookingActivity / activity android:name.ConfirmPayActivity / activity android:name.Change_order_query_Activity / activity android:name.ConfirmChangeActivity / /application注意两点WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE权限在 Android 6.0 需动态申请但源码中未见ActivityCompat.requestPermissions()调用说明它默认目标 SDK ≤ 22即targetSdkVersion22。若要在 Android 10 设备运行必须补全运行时权限逻辑否则BookingActivity中保存订单截图到 SD 卡会静默失败。所有 Activity 均未声明android:configChanges意味着横竖屏切换会触发 Activity 重建而OrderDao中的 SQLite 连接若在onDestroy()中未关闭可能引发IllegalStateException: database not closed。2.3OrderDao.class的 SQLite 实现细节与线程安全陷阱OrderDao.class是整个数据层的核心其关键方法如下还原后 Java 源码public class OrderDao { private SQLiteDatabase db; private DatabaseHelper dbHelper; public OrderDao(Context context) { this.dbHelper new DatabaseHelper(context); } private static class DatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME train_ticket.db; private static final int DB_VERSION 1; public DatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE orders ( _id INTEGER PRIMARY KEY AUTOINCREMENT, train_no TEXT, from_station TEXT, to_station TEXT, date TEXT, price REAL, status INTEGER DEFAULT 0, create_time INTEGER)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 无升级逻辑 —— 重要风险点 } } public long insertOrder(Order order) { ContentValues values new ContentValues(); values.put(train_no, order.getTrainNo()); values.put(from_station, order.getFromStation()); values.put(to_station, order.getToStation()); values.put(date, order.getDate()); values.put(price, order.getPrice()); values.put(status, order.getStatus()); values.put(create_time, System.currentTimeMillis()); return db.insert(orders, null, values); // 注意此处 db 未初始化 } }关键问题暴露insertOrder()方法中db字段从未被赋值实际调用会抛出NullPointerException。正确写法应在insertOrder()内部调用dbHelper.getWritableDatabase()获取实例。onUpgrade()为空意味着数据库版本升级时旧表不会被迁移ALTER TABLE语句缺失。所有 CRUD 方法均未加synchronized或使用ThreadLocalSQLiteDatabase在ConfirmPayActivity发起支付成功回调后立即调用updateStatus()与BookingActivity同时写入同一张表极易触发SQLiteDatabaseLockedException。提示修复方案是将dbHelper.getWritableDatabase()调用移至每个 DAO 方法内部并确保每次操作后调用db.close()或改用try-with-resources。更优解是引入Room替代手写 DAO但本源码定位是学习传统实现故需直面此缺陷。2.4 网络请求模块HttpURLConnection的原始封装与超时控制源码中网络请求集中于NetworkUtil.class需从jarlist.cache推断其存在典型调用如下还原自ConfirmChangeActivity.classString urlStr http://api.12306.cn/query?train_no trainNo date date; URL url new URL(urlStr); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(GET); conn.setConnectTimeout(15000); conn.setReadTimeout(15000); conn.setDoInput(true); int responseCode conn.getResponseCode(); // 此处易阻塞主线程 if (responseCode HttpURLConnection.HTTP_OK) { BufferedReader reader new BufferedReader( new InputStreamReader(conn.getInputStream(), UTF-8)); String line; StringBuilder response new StringBuilder(); while ((line reader.readLine()) ! null) { response.append(line); } parseJson(response.toString()); // 手动解析无 Gson }参数与风险说明setConnectTimeout(15000)和setReadTimeout(15000)是合理设置但conn未在finally块中disconnect()导致连接复用失效频繁请求下会耗尽 socket 资源。parseJson()方法遍历字符串查找train_no:G101等 key用substring()截取对 JSON 格式强依赖一旦接口返回字段名变更如trainNo→train_no解析直接崩溃。最致命的是该代码直接在onCreate()中执行未包裹AsyncTask或Thread在 Android 4.0 会触发NetworkOnMainThreadException。实际能运行说明其targetSdkVersion必定 ≤ 10Android 2.3.3这是必须降级编译环境才能复现的兼容性陷阱。3. 业务流程串联从车次查询到改签确认的 Activity 跳转与数据传递3.1MainActivity到BookingActivity的 Intent 数据契约MainActivity是入口页含出发地、目的地、日期选择器。用户点击“查询”后跳转逻辑如下Intent intent new Intent(MainActivity.this, BookingActivity.class); intent.putExtra(from_station, fromEditText.getText().toString()); intent.putExtra(to_station, toEditText.getText().toString()); intent.putExtra(travel_date, datePicker.getText().toString()); startActivity(intent);BookingActivity在onCreate()中接收String from getIntent().getStringExtra(from_station); String to getIntent().getStringExtra(to_station); String date getIntent().getStringExtra(travel_date); // 注意未校验 null若从桌面图标启动无参数直接 NPE数据校验与容错建议必须添加if (getIntent() ! null getIntent().hasExtra(from_station))判断travel_date格式应为yyyy-MM-dd但源码中未做正则校验^\\d{4}-\\d{2}-\\d{2}$导致后续网络请求拼接 URL 时出现http://api.12306.cn/query?date2024/05/01这类非法路径。3.2BookingActivity中的 ListView 与 ViewHolder 模式实现车次列表使用ListView展示Adapter 关键代码public class TrainListAdapter extends ArrayAdapterTrainInfo { private ListTrainInfo trainList; public TrainListAdapter(Context context, ListTrainInfo trainList) { super(context, R.layout.item_train, trainList); this.trainList trainList; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView LayoutInflater.from(getContext()) .inflate(R.layout.item_train, parent, false); holder new ViewHolder(); holder.tvTrainNo convertView.findViewById(R.id.tv_train_no); holder.tvTime convertView.findViewById(R.id.tv_time); holder.tvPrice convertView.findViewById(R.id.tv_price); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } TrainInfo train trainList.get(position); holder.tvTrainNo.setText(train.getTrainNo()); holder.tvTime.setText(train.getStartTime() → train.getEndTime()); holder.tvPrice.setText(¥ train.getPrice()); return convertView; } static class ViewHolder { TextView tvTrainNo; TextView tvTime; TextView tvPrice; } }性能与兼容性要点item_train.xml中TextView使用android:textSize14sp但在 Android 12 系统字体缩放开启时sp单位会放大导致列表项高度溢出。应改用dp控制基础字号或添加app:autoSizeTextTypeuniform。getView()中未处理train null场景网络请求失败时trainList可能为空会导致NullPointerException。ListView已被RecyclerView替代但本源码保留它正说明其适配目标为 Android 4.x 设备RecyclerView的DiffUtil优化在此无需考虑。3.3 改签核心链路Change_order_query_Activity与ConfirmChangeActivity的状态同步用户在订单页点击“改签”触发// OrderDetailActivity.java Intent intent new Intent(OrderDetailActivity.this, Change_order_query_Activity.class); intent.putExtra(order_id, orderId); startActivity(intent);Change_order_query_Activity查询可改签车次后列表项点击事件为listView.setOnItemClickListener((parent, view, position, id) - { TrainInfo newTrain trainList.get(position); Intent intent new Intent(Change_order_query_Activity.this, ConfirmChangeActivity.class); intent.putExtra(old_order_id, getIntent().getStringExtra(order_id)); intent.putExtra(new_train_no, newTrain.getTrainNo()); intent.putExtra(new_date, newTrain.getDate()); startActivity(intent); });ConfirmChangeActivity中执行最终提交// 构造改签请求体 JSONObject req new JSONObject(); req.put(old_order_id, getIntent().getStringExtra(old_order_id)); req.put(new_train_no, getIntent().getStringExtra(new_train_no)); req.put(new_date, getIntent().getStringExtra(new_date)); // 调用 NetworkUtil.post(...) 发送事务一致性风险源码中无“改签预占席位”逻辑即未在Change_order_query_Activity查询时锁定余票用户点击确认时才真正扣减原订单、生成新订单。若并发改签可能出现“超卖”。ConfirmChangeActivity成功后未向OrderDao插入新订单记录也未更新原订单status为“已改签”导致OrderDao.queryByStatus(0)仍返回该订单形成数据不一致。提示修复需在ConfirmChangeActivity的网络回调成功分支中显式调用OrderDao.insertNewOrder(...)和OrderDao.updateOldOrderStatus(...)并用beginTransaction()/setTransactionSuccessful()/endTransaction()包裹确保原子性。4. 真机调试与常见崩溃定位Logcat 关键日志过滤与修复验证4.1 针对性 Logcat 过滤命令与典型错误模式将源码导入 Android Studio 后真机运行最常遇到三类崩溃对应 Logcat 过滤策略如下错误类型Logcat 过滤命令典型日志片段根本原因修复位置数据库未关闭adb logcatgrep database.*not closedE/Database: close() was never explicitly calledOrderDao中SQLiteDatabase实例未在finally块关闭主线程网络请求adb logcatgrep NetworkOnMainThreadExceptionandroid.os.NetworkOnMainThreadExceptionBookingActivity.onCreate()直接调用NetworkUtil.query()资源 ID 找不到adb logcatgrep Resource ID #0xandroid.content.res.Resources$NotFoundException: Resource ID #0x7f08004aR.id.xxx与resources.ap_中实际 ID 不匹配或findViewById()传入错误 ID4.2ConfirmPayActivity支付回调的模拟验证技巧源码中微信/支付宝支付回调通过onNewIntent()捕获Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); String result intent.getStringExtra(result); if (success.equals(result)) { updateOrderStatusInDb(); // 更新数据库 showSuccessDialog(); // 弹窗提示 } }但真机无法触发支付 SDK 回调可手动模拟在ConfirmPayActivity中添加测试按钮Button android:idid/btn_mock_success android:textMock Pay Success android:layout_widthwrap_content android:layout_heightwrap_content/在onCreate()中绑定点击事件findViewById(R.id.btn_mock_success).setOnClickListener(v - { Intent mockIntent new Intent(); mockIntent.putExtra(result, success); onNewIntent(mockIntent); });运行后点击按钮观察updateOrderStatusInDb()是否执行showSuccessDialog()是否弹出验证支付成功路径是否畅通。4.3AndroidManifest.xml权限声明的最小化裁剪表根据源码实际功能可安全移除的权限如下对比原始声明声明权限是否必需依据裁剪后影响WRITE_EXTERNAL_STORAGE❌ 否源码中仅BookingActivity尝试保存截图但无实际调用MediaStore或FileOutputStream移除后saveScreenshot()方法需注释或替换为ClipboardManager复制文本READ_EXTERNAL_STORAGE❌ 否无读取外部文件逻辑如加载本地图片、读取 CSV移除后避免 Android 11 的分区存储兼容问题ACCESS_NETWORK_STATE✅ 是NetworkUtil.isNetworkAvailable()依赖此权限判断网络类型保留否则无网时无法提示用户INTERNET✅ 是所有 API 请求的基础必须保留注意裁剪后需在AndroidManifest.xml中删除对应uses-permission行并在build.gradle中设置targetSdkVersion 29Android 10以启用分区存储强制策略倒逼开发者改用Context.getExternalFilesDir()存储私有文件。4.4resources.ap_资源重打包验证AAPT2 命令链实操当修改res/values/strings.xml后需重新生成resources.ap_以确保R.id一致性。步骤如下# 1. 编译资源假设 res/ 在当前目录 aapt2 compile --dir res/ -o compiled_resources.zip # 2. 链接资源指定包名、输出 apk生成 resources.ap_ aapt2 link \ -I $ANDROID_HOME/platforms/android-30/android.jar \ --package-id 0x7f \ --version-code 1 \ --version-name 1.0 \ -o resources.ap_ \ compiled_resources.zip # 3. 验证 resources.ap_ 是否包含预期资源 aapt2 dump resources resources.ap_ | grep btn_submit # 输出应含resource 0x7f08003c com.example.train:layout/activity_booking: 0x7f08003c若grep无输出说明activity_booking.xml中未定义android:idid/btn_submit或aapt2 link时未包含该 layout 文件。此时需检查res/layout/目录完整性及aapt2 compile的输入路径。5. 从源码到可部署 APKGradle 构建配置与签名自动化5.1build.gradleModule: app关键配置项由于源码无 Gradle 文件需新建并精准匹配其技术栈。核心配置如下android { compileSdk 30 // 对应 android-30.jar支持 targetSdkVersion29 defaultConfig { applicationId com.example.train minSdk 16 // 源码使用 ActionBar最低支持 Android 4.1 targetSdk 29 // 避免 Android 10 分区存储强制行为 versionCode 1 versionName 1.0 testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner } buildTypes { release { minifyEnabled false // 源码无混淆规则禁用 ProGuard proguardFiles getDefaultProguardFile(proguard-android-optimize.txt) } } // 必须添加否则 R.java 无法生成 sourceSets { main { manifest.srcFile AndroidManifest.xml java.srcDirs [src] res.srcDirs [res] } } } dependencies { implementation fileTree(dir: libs, include: [*.jar]) // 加载源码自带 jar implementation androidx.appcompat:appcompat:1.4.2 // 替代已废弃的 android.support.v7 implementation androidx.constraintlayout:constraintlayout:2.1.4 }参数说明minSdk 16源码中BookingActivity继承AppCompatActivity需androidx.appcompat而appcompat:1.4.2最低支持 SDK 16targetSdk 29与AndroidManifest.xml中权限行为一致避免WRITE_EXTERNAL_STORAGE动态申请弹窗sourceSets块强制指定res/和src/路径否则 Android Studio 默认寻找src/main/res导致资源找不到。5.2 自动化签名与 APK 生成脚本在项目根目录创建sign_apk.sh内容如下#!/bin/bash # 生成密钥库首次运行时执行 # keytool -genkeypair -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias # 构建 release APK ./gradlew assembleRelease # 签名使用已有密钥库 jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \ -keystore my-release-key.jks \ -storepass your_store_password \ -keypass your_key_password \ app/build/outputs/apk/release/app-release-unsigned.apk \ my-key-alias # 对齐优化 zipalign -v 4 app/build/outputs/apk/release/app-release-unsigned.apk app-release-signed.apk echo Signed APK generated: app-release-signed.apk执行前准备将my-release-key.jks密钥库放入项目根目录替换your_store_password和your_key_password为实际密码赋予脚本执行权限chmod x sign_apk.sh运行./sign_apk.sh。生成的app-release-signed.apk可直接安装至 Android 5.0 设备验证MainActivity启动、车次查询、订单提交全流程。5.3AndroidManifest.xml中android:exported的 Android 12 兼容补丁Android 12API 31起所有含intent-filter的组件必须显式声明android:exported。源码中MainActivity有LAUNCHER需补全activity android:name.MainActivity android:exportedtrue !-- 添加此行 -- intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity其他 Activity 若无intent-filter则android:exportedfalse可省略默认为 false但为明确语义建议统一添加activity android:name.BookingActivity android:exportedfalse / activity android:name.ConfirmPayActivity android:exportedfalse /此补丁不影响 Android 11 及以下设备但可避免在 Android 12 设备上安装时报INSTALL_PARSE_FAILED_MANIFEST_MALFORMED错误。本文还有配套的精品资源点击获取