简介:一份面向Android初学者的结课大作业资料包,由王英强老师编写,内含完整源码、APK安装包和实训报告,帮助计算机专业学生巩固Android Studio开发技能,系统覆盖项目结构、Activity与Fragment生命周期、Intent组件通信、Service后台任务、SQLite数据存储、Gradle构建配置及调试工具等核心知识点。压缩包共625个文件,总体积约19.38MB,主要类型包括Java/Kotlin源码、XML布局与资源文件、Gradle脚本、JSON配置文件、编译生成的DEX/Class文件以及最终APK,目录结构清晰,便于按模块对照学习。目前已有15241人学习下载,热度较高。通过阅读源码,可以掌握多Activity协作、布局组织与数据持久化的设计方式;安装APK能直观验证应用打包、签名与资源压缩流程;实训报告则完整记录了需求分析、设计思路与排错过程,适合作为结课作业的参考模板或自学巩固的实践素材。
1. Android Studio结课大作业:别让交付环节毁掉你一个学期的功能
你花三周写完了一个功能完整的 Android 应用,结果答辩前一天发现模拟器一启动就黑屏,换成真机又连不上调试,最后只能拿着录屏跟评委说“其实它本来是能跑的”。这不是个别人的剧本,是太多人用血泪总结出来的结课大作业常态。Android Studio 结课大作业真正要面对的问题,不是“会调用多少 API”,而是在有限工期内选一个不翻车的主线方案,把界面、数据、打包、演示这几件事串成一条能跑通的链路。这篇文章从选题、工程搭建、核心功能实现、避坑、打包到答辩自检,按真实交付顺序讲透,适合正在赶大作业的同学,也适合第一次独立做 Android 小项目、想少走弯路的开发者。
2. 结课大作业选题与工程搭建:先定一个不翻车的技术骨架
2.1 怎么选题:三个判断标准过滤掉高风险方向
一个结课大作业能拿多少分,核心看两件事:功能是不是完整、答辩现场能不能稳定跑起来。很多同学栽在第二个问题上,不是代码写得少,而是选了一个“演示条件稍微变化就会崩”的方向。比如实时音视频通话、蓝牙外设联动、复杂动画流,这类功能开发周期长,而且对设备、网络、环境依赖极强,现场一旦连不上耳机、配对失败,整个演示就卡住了。
我一般会拿三个标准筛题。第一,这个功能能否在三十秒内让评委看懂它在干什么。像记账、笔记、待办清单、天气查询、课程表这类应用,界面一打开就是熟悉的交互逻辑,不需要解释。第二,核心数据能不能持久化到本地。这意味着即使断网、服务器挂了,你依然可以打开 App 展示历史数据,演示主动权在自己手里。第三,功能范围能否在两周内裁剪到一个可演示的最小闭环。不要把“仿微信”当目标,把“单聊加图片发送”当目标,后者才是一个大作业能承受的颗粒度。
如果你实在不知道怎么选,本地记事本是我见过最稳妥的模板。它有界面、有数据存储、有增删改查,还能加搜索、标签、提醒作为加分项,每一步都能控制在你的掌控范围内。后面几章我会以这个方向为例展开写法和参数,你可以直接替换成自己的业务场景。
2.2 工程骨架:Gradle 配置与项目模板的选型
选题定了,接下来是新建工程。这里有个经常被跳过的选择题:项目模板选 Empty Views Activity 还是 Empty Activity。如果你选的是 Empty Activity,Android Studio 默认生成的是 Jetpack Compose 写法的模板,界面代码全部是 Kotlin 函数式布局。Compose 本身没问题,但对于第一次做 Android 大作业的同学,它把“布局怎么写”这个问题的答案藏得更深了,网上大量教程还是基于 XML 布局的,你遇到问题去搜索时,两边代码对不上会非常痛苦。常见做法是选 Empty Views Activity,拿回传统的 XML 布局和 findViewById 体系,等到后期熟练了再回来看 Compose 也不迟。
新建项目后用 Gradle 配置把依赖基线定好。这里最关键的是三个版本参数的语义要搞清楚:compileSdk 表示你拿哪个版本的 SDK 来编译代码,targetSdk 表示系统按哪个版本的行为标准来运行你的应用,minSdk 表示最低支持到哪个安卓版本。三者不是越大越好,targetSdk 选得太新,系统可能对你的明文网络请求、后台服务做额外限制,反而增加演示风险;minSdk 选得太低,又会多写一堆兼容代码。我一般把 minSdk 定到 23,targetSdk 选当前官方推荐的稳定版本,compileSdk 对应本机 SDK Manager 里已安装的那个版本号,三者相差不超过两个大版本。
android { namespace "com.example.notedemo" compileSdk 34 // 改成你本机 SDK Manager 已安装的版本号 defaultConfig { applicationId "com.example.notedemo" minSdk 23 targetSdk 34 versionCode 1 versionName "1.0" } }这段配置里,namespace 和 applicationId 在绝大多数项目里保持一致,它们是应用的唯一标识,后面签名打包、adb 安装命令都要用到这个包名。versionCode 是给系统判断版本新旧的整数,每次升级必须递增;versionName 是展示给用户看的字符串,答辩时评委可能打开“关于”页面看,写“1.0”就行,不用花哨。
依赖部分,如果你做的是纯本地应用,最少只需要三样:RecyclerView 用于列表展示、Room 用于数据库、Lifecycle 的 ViewModel 用于界面数据隔离。Retrofit 和 OkHttp 先别急着加,等你确认选题真的要联网再说。很多人一上来把网上教程的依赖全拷一遍,项目里多出十几个用不到的库,构建时间拉长,混淆规则还要多配一堆,纯属给自己挖坑。
2.3 让第一个 Debug 包跑起来:模拟器与真机的基本功
工程创建完之后,第一件事不是写代码,而是确认空项目能在设备上跑起来。这一步能提前暴露环境问题,不要攒到最后一起解决。启动模拟器前,先看两处:一是创建虚拟设备时系统镜像优先选 x86_64 架构,二是确认本机已开启硬件加速。很多同学的电脑明明是 Intel 处理器,却在 Device Manager 里选了 ARM 镜像,启动速度慢到像在开一台虚拟机里的虚拟机。
打开模拟器后,如果卡在 “Waiting for target device to come online” 半天不动,多半是硬件加速没生效。Windows 上可以在任务管理器“性能”页看“虚拟化”是否显示已启用,没启用就需要进 BIOS 打开 Intel VT-x 或 AMD-V。这一项属于“看起来是软件问题、实际上是硬件设置”的经典玄学问题,后面第四章我会专门展开。
模拟器能跑通之后,再准备一台真机做搭档,因为真实手机的传感器、摄像头、通知权限和模拟器行为有差异,答辩时评委大概率会问“你手机上有跑过吗”。真机调试的第一步是打开开发者选项,连续点击“版本号”七次,然后到“开发者选项”里打开“USB 调试”。插上数据线后手机弹窗选“允许调试”,电脑端用 adb devices 确认设备状态是 device。
我见过不少同学卡在这一步,USBS调试开了却一直显示 unauthorized。原因是手机上没点确认授权弹窗,或者数据线是纯充电线。换一根有数据传输能力的线,问题基本消失。先把这些环境验证完,你的开发机器才算真正进入干活状态,后面写代码时才有“改完就能跑”的正反馈。
3. 核心功能怎么落地:以本地记事本为例讲三层写法
3.1 界面层:RecyclerView + ViewBinding 的适配器写法
一个记事本应用的主界面,核心是一个可滚动的列表。RecyclerView 是这里绕不开的控件,它负责回收不可见的条目、复用视图,保证列表长到几百条时依然流畅。写适配器时,我推荐用 ViewBinding 替代 findViewById,它能减少空指针风险,也能让代码缩短不少。在模块的 build.gradle 里开启:
android { buildFeatures { viewBinding true } }开启后,每个 XML 布局会自动生成对应的 Binding 类。比如 item_note.xml 会生成 ItemNoteBinding,你在适配器里直接引用即可。下面是一个最简适配器的写法:
class NoteAdapter( private val onClick: (NoteItem) -> Unit ) : RecyclerView.Adapter<NoteAdapter.NoteViewHolder>() { private val items = mutableListOf<NoteItem>() fun submitList(newItems: List<NoteItem>) { items.clear() items.addAll(newItems) notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder { val binding = ItemNoteBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return NoteViewHolder(binding) } override fun onBindViewHolder(holder: NoteViewHolder, position: Int) { holder.bind(items[position]) } override fun getItemCount(): Int = items.size inner class NoteViewHolder( private val binding: ItemNoteBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(note: NoteItem) { binding.tvTitle.text = note.title binding.tvTime.text = note.updateTime binding.root.setOnClickListener { onClick(note) } } } }这里要理解两个关键点。第一,onCreateViewHolder 负责“创建视图”,onBindViewHolder 负责“填充数据”,两者分离是 RecyclerView 复用的基础;getItemCount 告诉列表一共有多少条。第二,submitList 是统一的更新入口,把新数据整体替换掉再刷新,简单粗暴但对大作业足够了。如果你想让列表刷新更精细,可以在后续用 DiffUtil 替代 notifyDataSetChanged,它会对新旧列表做差异计算,只刷新变化的那一行,避免整个列表闪烁。
注意 bind 方法里 updateTime 是 Long 类型的时间戳,直接 setText 会显示一长串数字。常见做法是在实体类里写一个格式化函数,或者用 SimpleDateFormat 转成“昨天 14:30”这种人类可读格式,这也是答辩时评委喜欢问的一个细节,提前处理好能加分。
3.2 数据层:用 Room 代替手工 SQLite 的收益与参数
数据层是整个大作业的压舱石。很多课程还在教 SQLiteOpenHelper,但实际开发中直接手写 SQLite 的坑非常多:游标忘记关闭、SQL 拼接出错、字段类型写错导致运行时崩溃。Room 在 SQLite 之上封装了一层,编译期就帮你校验 SQL 语句是否正确,配合协程还能避免在主线程操作数据库导致的卡顿或崩溃,是大作业最合适的持久化方案。
一个最小可用的 Room 数据层需要三个文件:Entity 定义表结构、DAO 定义增删改查方法、Database 定义数据库本身。先看实体类:
@Entity(tableName = "note_table") data class NoteItem( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val content: String, val updateTime: Long = System.currentTimeMillis() )@PrimaryKey 指定主键,autoGenerate = true 表示自增。title 和 content 是记事内容,updateTime 默认取当前时间戳。这里有一个容易被忽视的点:字段名就是列名,如果你需要改列名,要用 @ColumnInfo(name = "xxx") 注解,后续数据库升级会涉及这件事。
DAO 是操作数据库的接口,Room 会帮你生成实现类:
@Dao interface NoteDao { @Insert suspend fun insert(note: NoteItem): Long @Update suspend fun update(note: NoteItem) @Delete suspend fun delete(note: NoteItem) @Query("SELECT * FROM note_table ORDER BY updateTime DESC") fun observeAll(): Flow<List<NoteItem>> }insert 返回 Long 是新插入行的 id,update 和 delete 按主键匹配。observeAll 返回 Flow<List >,这是一个响应式查询,数据库内容变化时会自动发出新列表,界面层只要收集这个 Flow,列表就自动刷新,不需要手动调用 notifyDataSetChanged。这也是 Room 优于手工 SQLite 的关键体验:数据驱动界面,而不是界面驱动数据。
最后是数据库类:
@Database(entities = [NoteItem::class], version = 1, exportSchema = false) abstract class AppDatabase : RoomDatabase() { abstract fun noteDao(): NoteDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun get(context: Context): AppDatabase = INSTANCE ?: synchronized(this) { INSTANCE ?: Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_demo.db" ).build().also { INSTANCE = it } } } }version = 1 是数据库版本号,每当你修改表结构,这个数字必须递增加一,否则 Room 会直接抛异常提示 Migration 错误。exportSchema 设为 false 可以避免编译期导出 schema 的警告,写起来省事。databaseBuilder 的第三个参数是数据库文件名,它会在应用私有目录下生成 app_demo.db 文件,你可以用 Android Studio 自带的 Database Inspector 直接查看表内容,这对答辩前数据自查非常有用。
ViewModel 中调用 DAO 时,要拿到 applicationContext,不要传 Activity 的 context,防止内存泄漏。这也是 Room 官方反复强调的一个细节,写的时候养成习惯就好。
3.3 网络层:如果选题需要联网,最少要配好的三个参数
如果你的大作业选题真的绕不开网络功能,比如做了一个新闻客户端或天气应用,那么 Retrofit 加 OkHttp 是当前最主流的选择。Retrofit 负责把接口定义转换成 HTTP 请求,OkHttp 负责底层的连接管理和超时控制,Gson 做 JSON 解析。这三者的组合熟悉度高、出问题也好搜,比你自己手写 HttpURLConnection 靠谱得多。
创建 Retrofit 实例时,有三个参数我每次都会检查一遍。第一个是超时时间,connectTimeout 建议 10 秒,readTimeout 和 writeTimeout 建议 15 秒,不要用默认值,移动网络下默认超时往往偏长,失败时用户要等很久才有反馈。第二个是 baseUrl,它必须以斜杠结尾,否则运行时会直接抛出 IllegalArgumentException,这是 Retrofit 最经典的报错之一。第三个是拦截器,用于统一添加请求头,比如 Token:
val retrofit = Retrofit.Builder() .baseUrl(BuildConfig.BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .client( OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .addInterceptor { chain -> val request = chain.request().newBuilder() .addHeader("Authorization", "Bearer ${token}") .build() chain.proceed(request) } .build() ) .build()这里有个大作业特有的坑:如果你访问的是本地电脑上跑的后端服务,模拟器里不能写 localhost,要写 10.0.2.2,这是模拟器访问宿主机固定保留的地址。如果你的 targetSdk 比较高,系统默认禁止明文 HTTP 请求,日志里会给出 CLEARTEXT communication 的报错提示,解决方案是在 AndroidManifest.xml 的 application 节点加上 android:usesCleartextTraffic="true",或者配置网络安全配置文件只对调试域名放开明文。这个我一般会建议直接加,省去答辩现场打不开数据的尴尬。
网络请求在演示时还有一层风险:一旦现场 WiFi 连不上,你的应用就会变成“打开就空白”。所以我在做有网络功能的大作业时,一定会给关键页面加一层本地缓存逻辑,把最后一次成功的数据存进 Room,网络失败时先展示缓存,再提示当前未联网。这也是一个很实在的加分设计,后面第四章会解释相关细节。
4. 结课大作业避坑指南:5 个演示前最常翻车的点
这一章的内容全部来自我带过的实际案例和自己在真机上的反复验证,每一条都是“现象 → 原因 → 解决”的完整链路。建议你在答辩前一周逐条对照排查一遍,能提前解决大部分现场问题。
4.1 模拟器卡在 Waiting for target device to come online
现象:模拟器启动后,状态栏一直转圈,底部提示 Waiting for target device to come online,有时候等了十分钟还是黑屏,强行冷启动也不行。这个问题出现频率极高,几乎每个学期都有同学在答辩前夜被它拦住。
原因:最常见的是宿主机的虚拟化功能没开启,Android Studio 的模拟器底层依赖系统级硬件加速,没有它就只能软件模拟,速度慢到像死机。其次是镜像架构选择错误,Windows 电脑选了 ARM 架构的系统镜像,性能同样极差。还有一个容易被忽略的原因是磁盘空间不足,模拟器创建时会申请数 GB 空间,磁盘满了它会一直卡在启动流程。
解决:先确认虚拟化是否开启。Windows 打开任务管理器,切到“性能”页面,看 CPU 那一栏的“虚拟化”是否显示“已启用”。如果显示“已禁用”,需要重启进 BIOS,找到 Intel VT-x 或 AMD-V 选项并打开,这一步每个主板界面不同,搜一下自己主板的型号即可。虚拟化没问题的话,回到 Device Manager,把这个虚拟设备删掉重建,系统镜像选择 x86_64 架构,API 级别别选最新的,选前一个大版本更稳定。重建后如果还不行,试试“Cold Boot Now”强制冷启动,很多启动中的异常状态靠这个是能恢复的。
4.2 真机 adb 识别不到设备或者一直 offline
现象:手机通过 USB 线连接电脑,开发者选项和 USB 调试都打开了,但 adb devices 要么什么都不显示,要么显示 unauthorized。换成别人的电脑又是好的,很让人怀疑人生。
原因:第一是 USB 连接模式不对,很多手机默认走“仅充电”模式,需要下拉通知栏改成“文件传输”或“传输文件”。第二是手机上的调试授权弹窗没点确认,显示 unauthorized 就是这个原因。第三是数据线质量或接口问题,不少所谓的“数据线”其实只支持充电,没有数据通道。第四是 ADB 服务状态异常,插拔次数太多导致服务卡死。
解决:按顺序操作——先拔掉数据线,在电脑终端执行 adb kill-server 再执行 adb start-server,然后重新插线;手机上把手动选择 USB 配置改为“文件传输”;这时候手机会弹出“允许 USB 调试吗”的对话框,勾选“始终允许”并确定;最后再执行 adb devices 确认状态变为 device。如果还是 offline,换一根数据线,或者换个 USB 接口,实测很多问题是线的问题。
另外一个容易被忽视的点:华为、小米等品牌的手机需要额外打开“USB 安装”和“USB 调试(安全设置)”选项,不同系统版本的位置不同,搜一下自己的品牌型号就有答案。这不是玄学,是各家系统安全策略不一样,提前在真机上验证一次就不会在答辩现场惊慌。
4.3 Release 包一打开就闪退,Logcat 里是 ClassNotFoundException
现象:Debug 模式在模拟器和真机上跑都很正常,但用 Build > Generate Signed Bundle or APK 打出来的正式包,一安装打开就闪退,Logcat 报 java.lang.ClassNotFoundException 或 NoSuchFieldException。
原因:这是混淆导致的。Android 构建工具默认开启代码压缩和混淆,它会把类名、方法名缩短成 a、b、c 这样的随机名称。问题在于你的代码里有些类是通过反射访问的,比如 Gson 反序列化的数据类、接口返回的实体类,名字被改掉之后,运行时按原名字反射找不到了,直接抛异常。Room 数据库的实体类也有同样问题,因为 SQLite 查询结果要通过字段名映射到对象属性,字段名被混淆就全乱了。
解决:在 proguard-rules.pro 里把数据类和实体类排除,不去混淆它们。常见做法是统一放在 model 包下,然后加一条 keep 规则,把整个包的类都保留:
-keep class com.example.notedemo.model.** { *; } -keep class com.example.notedemo.data.** { *; }如果使用了 Gson,还要加上对泛型、注解的保留规则,否则 JSON 解析会静默失败,返回空对象而不是报错。最省事的方法是在实体类上加 @Keep 注解,让它无条件保留。写完混淆规则后,记得打包后先自己安装一遍,重点测试登录、列表加载、数据保存这三条主流程,不要等到答辩现场才发现。
4.4 数据库升级后提示 no such table
现象:你给实体类加了一个新字段,把数据库版本从 1 改成 2,运行到数据库操作时报 SQLiteException: no such table: note_table,或者 IllegalStateException: Migration didn't properly handle。
原因:这是一个典型的理解偏差。Room 的数据库版本升级不是改个数字就完事的,它要求你提供从版本 1 到版本 2 的 Migration 迁移逻辑,告诉它“旧表如何变成新表”。如果你只是改了实体类和版本号,没有写 Migration,Room 宁可报错也不会自动帮你改表,因为它不知道你的意图。
解决:课程作业的推荐做法是先在数据库构建时加 fallbackToDestructiveMigration():
Room.databaseBuilder(context, AppDatabase::class.java, "app_demo.db") .fallbackToDestructiveMigration() .build()这个方法的意思是:检测到版本不匹配且没有迁移逻辑时,直接删除旧表重建新表。代价是用户已有的数据会丢失,但对你大作业而言,数据是可以重新录入的,保住演示流程比保住旧数据更重要。如果你希望保留数据,那就必须写 Migration 了,比如给现有表加列:
val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE note_table ADD COLUMN isFavorite INTEGER NOT NULL DEFAULT 0") } }写完这个 Migration 对象,还要在 databaseBuilder 里通过 .addMigrations(MIGRATION_1_2) 注册。这是生产环境的标准做法,你也可以在答辩时给评委展示这一段,体现你对数据迁移的理解。
4.5 模拟器上网络请求超时,但电脑浏览器一切正常
现象:模拟器里打开你的 App,明明电脑上浏览器访问后端接口是通的,App 里却一直转圈,过一会儿提示超时。把代码检查了三遍也找不到问题。
原因:这个方向通常有两个原因。第一个是模拟器的网络地址特殊,你在 App 里写 localhost 访问的是模拟器自己,而不是你电脑,正确地址应该是 10.0.2.2。第二个是明文流量被拦,Android 高版本默认要求使用 HTTPS,你本地开发用的 HTTP 接口会被系统直接拒绝,控制台里通常会有 CLEARTEXT communication 的警告。
解决:baseUrl 改成 http://10.0.2.2:8080/ 这种形式,然后在 AndroidManifest.xml 的 application 标签下加一行:
<application android:usesCleartextTraffic="true" ... >如果你在真机上调试,10.0.2.2 就不好使了,需要改成电脑的局域网 IP。常见的做法是在 BuildConfig 里根据 Debug/Release 动态切换地址,但如果你的选题不需要联网,这段直接跳过就好,别给自己增加变量。记住一个原则:毕业设计不是上线产品,稳定演示的优先级最高。
5. 打包签名与答辩演示:从 Debug 到能装进老师手机的 APK
5.1 签名配置:创建 keystore 与 Gradle 参数说明
Debug 包只能在开发机上安装,答辩时你要交给评委的 APK 必须是签名过的 Release 包。签名相当于应用的身份证,系统靠它确认 APK 的来源。用 Android Studio 的菜单操作可以生成签名包,但更可控的做法是在 Gradle 里直接配置签名信息,这样每次构建都走同一条路,不会出现“这次装得上、下次装不上”的怪现象。
第一步是生成 keystore 签名文件。Android Studio 自带的 keytool 命令行工具就可以完成,我习惯把签名文件放在项目的 app 模块目录下,方便相对路径引用:
keytool -genkeypair -v \ -keystore release.jks \ -alias notedemo \ -keyalg RSA \ -keysize 2048 \ -validity 10000执行后会要求你设置密码并填写姓名、组织等信息,这些信息不影响使用,随便填即可但要记住密码和别名,后面配置要用。参数的用途说明:-keystore 指定生成的文件名,-alias 是密钥的别名,签名配置里要靠它找到对应的密钥,-keyalg RSA 是密钥算法,-keysize 2048 是密钥长度,越长越安全,-validity 10000 是有效期天数,约 27 年,足够覆盖你的学业周期。
第二步是在模块的 build.gradle 里配置签名信息:
android { signingConfigs { release { storeFile file("release.jks") storePassword "你的存储密码" keyAlias "notedemo" keyPassword "你的密钥密码" } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" } } }这里 storeFile 是 keystore 文件路径,storePassword 是 keystore 的存储密码,keyAlias 是刚才创建的别名,keyPassword 是别名对应的密钥密码。把密码直接写在 build.gradle 里只适合大作业这种场景,公司项目一般会用 keystore.properties 文件在外部管理密码,这点可以在答辩时提一嘴,让评委看到你知道这个最佳实践。
这里还需要注意签名版本的问题。Android 7.0 以上推荐使用 APK Signature Scheme v2,Android 11 及以上还支持 v3,但为了兼容低版本设备,我在打包时通常勾选 v1 和 v2 两个方案都支持。菜单操作时在你点击 Generate Signed Bundle or APK 之后会有签名版本的选择页面,两个都勾上最稳。
5.2 混淆与资源压缩:不是所有类都能被重命名
混淆这块在第四章的避坑清单里已经提到过,因为 Release 包默认开启混淆,它既能缩小 APK 体积,也能提高代码被逆向的难度。但混淆是有代价的,它按“有没有被引用”来决定要不要保留一个类,按“有没有反射调用”来决定能不能重命名一个类。你的数据和实体类就属于典型的反射调用场景,必须显式排除。
先看两个开关的含义。minifyEnabled true 表示开启代码压缩和混淆,shrinkResources true 表示开启资源压缩,它会把未使用的资源文件从 APK 里移除。这两个开关经常一起开,效果很直观:一个几十 MB 的 APK 打包后能缩到十几 MB。但坑也在这里,资源压缩有时候会误删动态引用的资源,比如用 getIdentifier() 动态获取的资源名,这类资源需要在 keep 规则里额外声明。
我的习惯是给数据层建一个统一的包名,把实体类、DTO、数据库相关类全部放进去,然后在 proguard-rules.pro 里整体保留:
-keep class com.example.notedemo.model.** { *; } -keep class com.example.notedemo.data.** { *; } -keep class com.example.notedemo.network.** { *; }如果你用了 Retrofit 和 Gson,还需要保留 Retrofit 的接口方法和泛型类型,否则运行时也会出问题。实际遇到情况非常容易辨别:APK 打出后,安装到手机上打开,走一遍“列表展示 → 点击详情 → 修改保存”三条主流程,哪个环节崩了,就去查 Logcat 里是哪个类找不到,把对应的包名加进 keep 规则。这个过程不会超过半小时,但它能让答辩现场少掉 90% 的意外。
5.3 答辩演示设计:录屏、飞行模式与两台手机的备份策略
功能写好了、包也打出来了,最后一步是设计演示流程。我见过太多人功能做得不错,但演示时翻车在操作细节上:现场投屏字体太小看不清、点错了页面找不到返回入口、网络中断导致数据加载不出来。这里有三条非常实用的预案,每条都是答辩现场的真实教训。
第一,提前录屏。安卓系统自带录屏功能,把完整的主流程、加分项功能录成视频存到相册。一旦现场设备出问题,直接切换到录屏播放,至少保证评委能看到你的功能展示。这里的细节是:录屏时把手机横过来,画面适配投影的比例,并且提前测试投影线材的兼容性。
第二,演示前打开飞行模式。如果你的应用核心功能是本地化的,演示时打开飞行模式,反而能证明一个加分点:这个应用在没有网络的情况下依然可用。同时它还能避免答辩现场突然弹出通知、来电打断流程。如果应用必须联网,也要先把服务器接口验证一遍,并在手机上准备好数据缓存,防止现场网络抖动。
第三,准备备用机。把打好的 APK 提前装到两台手机上,主备都装好。主演示机如果出现电池耗尽、系统崩溃,备用机直接顶上,前后切换只需要十几秒。这个细节看起来土,但实际效果极好,也是我最常对同学强调的建议。演示之前把手机“开发者选项”里的“保持唤醒状态”打开,屏幕就不会中途黑掉,这个小开关救过很多次场。
6. 进阶:用一条命令完成“交付前自检”,避免演示现场空手而归
答辩前最后一天,与其一遍遍打开界面手点测试,不如写一条自检脚本,把安装、启动、崩溃检查自动化。这个思路来自一个很朴素的习惯:人点按钮会漏,脚本不会。下面这段脚本是针对 release APK 的快速冒烟测试,适合所有使用 adb 调试的安卓项目:
#!/bin/bash APK_PATH="app/build/outputs/apk/release/app-release.apk" PACKAGE="com.example.notedemo" # 1. 覆盖安装 adb install -r "$APK_PATH" if [ $? -ne 0 ]; then echo "安装失败,检查 APK 路径或签名" exit 1 fi # 2. 启动主界面 adb shell am start -n "$PACKAGE/.MainActivity" sleep 2 # 3. 抓取崩溃日志并判断 adb logcat -d | grep -E "FATAL|AndroidRuntime" > crash_check.log if [ -s crash_check.log ]; then echo "检测到崩溃日志:" cat crash_check.log else echo "启动正常,无崩溃" fi脚本各部分的逻辑是这样的:adb install -r 表示覆盖安装,-r 参数允许重装同名应用,如果签名不一致,这里会直接报 INSTALL_FAILED_UPDATE_INCOMPATIBLE,这是排查签名问题的第一个信号。adb shell am start -n 用来拉起指定包名和 Activity,包名要替换成你自己项目的 applicationId,MainActivity 的路径要和 manifest 里声明的一致。最后的 logcat -d 是抓取当前已有日志,grep 过滤崩溃关键词,-s 判断文件是否为空,空表示没有崩溃。
除了启动检查,我还会顺手加一条验证数据落盘的命令,用 adb shell run-as 进入应用私有目录查看数据库文件是否存在,这能证明 Room 初始化成功:
adb shell run-as "$PACKAGE" ls -l databases/能看到 app_demo.db 文件且大小不为零,说明数据库建表成功。这个细节不仅用于自检,答辩时你现场执行这条命令,评委能看到你对“数据到底存哪”有真实理解,比嘴上说“用 Room 存储”有力得多。
你会注意到这套自检思路的核心是“黑匣子思维”——不管代码内部怎么写的,只看对外行为是否符合预期:装得上、拉得起、不崩溃、有数据文件。这也是我自己每次交付前必做的最后一件事。翻车这种事,准备多充分都不嫌多,但真正能把风险压到最低的,从来不是运气,而是提前把最坏的场景都走了一遍。希望帮到你。
本文还有配套的精品资源,点击获取