☰
Android Activity 功能代码实战:生命周期、跳转传参与启动模式避坑指南
2026/9/28 22:49:02 网站建设 项目流程

简介:这份资源面向BPM流程开发人员与Activiti/Camunda学习者,聚焦流程运行期的动态控制能力,解决部署、加签、流程变量与指定节点审批人等实际开发问题。压缩包共200个文件,约1.57MB,以63个java源码、62个class编译文件、56个bpmn流程定义为主,辅以13个xml配置、2个properties、2个sql及日志与工程文件,覆盖从流程建模到引擎调用的完整链路。已有818人学习下载,适合需要理解动态加签与审批人指派实现细节的中级开发者。通过研读源码与流程定义,读者可掌握流程部署解析、运行时动态添加审批节点、流程变量驱动流转、setAssignee与监听器指定审批人等核心写法,并借助act6子目录中的示例快速对照调试,形成可复用的流程扩展思路。

1. Activity 功能代码到底在写什么:从一次冷启动白屏说起

做过 Android 的人大概都遇到过这种场面:点开应用图标,先白屏一两秒,然后首页才慢悠悠出来。产品经理拿着手机问“为什么这么慢”,你打开日志一看,冷启动耗时 1800ms,其中 Activity 的 onCreate 里塞了三个网络请求、两个 SharedPreferences 读取和一个第三方 SDK 初始化。这就是 Activity 功能代码最真实的战场——它不是一个抽象概念,而是承载界面生命周期、状态恢复、跳转传参、动画过渡的那一层具体实现。

Activity 功能代码,说白了就是在 Activity 这个组件里,把“界面怎么显示、数据怎么加载、状态怎么保存、页面怎么跳”这几件事用代码落地。它涉及生命周期回调的编排、Intent 传参的序列化、启动模式的选择、转场动画的配置,以及和 Fragment、ViewModel、Compose 之间的协作边界。适合谁看?正在写业务页面但总觉得代码越写越乱的中级开发者,以及需要排查启动慢、状态丢失、跳转异常这类问题的维护者。下面从最小可运行单元开始,一层层把这块代码拆开。

2. 生命周期与状态恢复:Activity 功能代码的骨架

2.1 七个回调不是模板,是状态机的七个入口

很多人写 Activity 功能代码时,把 onCreate 当成 main 函数用,所有初始化都往里塞。但 Activity 的生命周期回调本质上是一个状态机的入口,每个回调对应系统在不同阶段给你的“插话机会”。理解这一点,代码组织方式会完全不同。

常见做法是按下表分配职责:

回调该做什么不该做什么
onCreate设置布局、绑定 ViewModel、恢复 savedInstanceState耗时 IO、网络请求
onStart注册需要可见时才生效的监听重量级初始化
onResume启动动画、恢复传感器、刷新实时数据阻塞主线程的操作
onPause暂停动画、提交轻量数据耗时保存、弹对话框
onStop释放相机、注销广播保存关键状态(可能不被调用)
onDestroy解绑监听、取消订阅假设一定会被调用
onRestart重新进入可见前的准备重复 onCreate 的逻辑

关键点在于:onStop 之后系统可能直接杀进程,onDestroy 不保证执行。所以任何“必须保存”的状态,要放在 onPause 或 ViewModel 里,而不是赌 onDestroy 会跑。

2.2 用代码把状态恢复写对

下面是一个最小但完整的状态恢复示例,覆盖了配置变更(旋转屏幕)和进程被杀两种场景:

class CounterActivity : AppCompatActivity() { private var count = 0 private lateinit var tvCount: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_counter) tvCount = findViewById(R.id.tv_count) // 从 savedInstanceState 恢复,进程被杀后重建也能拿到 count = savedInstanceState?.getInt(KEY_COUNT) ?: 0 render() findViewById<Button>(R.id.btn_add).setOnClickListener { count++ render() } } // 配置变更时保存;进程被杀时系统也会调用 onSaveInstanceState override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putInt(KEY_COUNT, count) } private fun render() { tvCount.text = "当前计数:$count" } companion object { private const val KEY_COUNT = "key_count" } }

逻辑说明:onSaveInstanceState 会在 Activity 即将被系统回收前调用,把 count 写进 Bundle。重建时 onCreate 的 savedInstanceState 参数不为 null,从中读回。参数说明:KEY_COUNT 用常量而非硬编码字符串,避免拼写错误导致恢复失败——这是血泪经验,字符串写错不会报错,只会静默丢失状态。

注意:onSaveInstanceState 不适合存大对象。Bundle 有大小限制(通常 1MB 左右),存 Bitmap 或长列表会触发 TransactionTooLargeException。

2.3 ViewModel 与 onSaveInstanceState 的分工

如果状态只在配置变更时需要保留,用 ViewModel 更干净;如果需要跨进程死亡恢复,才用 onSaveInstanceState。两者可以叠加:ViewModel 持有内存态,onSaveInstanceState 存最小必要标识。

class CounterViewModel : ViewModel() { // 配置变更时 ViewModel 存活,count 不丢 var count = 0 } // Activity 中 private val vm: CounterViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 进程被杀后 ViewModel 重建,从 Bundle 恢复 vm.count = savedInstanceState?.getInt(KEY_COUNT) ?: vm.count }

这样分工的好处是:旋转屏幕不触发序列化,性能更好;进程被杀时又有兜底。参数上,ViewModel 的 scope 默认跟随 Activity,如果页面里有 Fragment,要用 activityViewModels() 共享同一个实例,否则各拿各的,状态对不上。

3. 跳转传参与启动模式:Intent 和 launchMode 的配合

3.1 Intent 传参的三种方式和各自边界

Activity 功能代码里,页面跳转传参是最频繁的操作。常见三种方式:Intent extras、Bundle、以及通过 ViewModel 或单例做数据共享。前两种本质一样,都是往 Intent 里塞 Bundle。

// 方式一:直接 putExtra val intent = Intent(this, DetailActivity::class.java).apply { putExtra("id", 1001) putExtra("title", "商品详情") putExtra("price", 99.9) } startActivity(intent) // 方式二:封装成静态工厂,调用方不用记 key companion object { private const val EXTRA_ID = "extra_id" fun newIntent(context: Context, id: Int): Intent { return Intent(context, DetailActivity::class.java).apply { putExtra(EXTRA_ID, id) } } }

逻辑说明:方式二把 key 私有化,调用方只传业务参数,减少拼写错误。参数说明:putExtra 支持基本类型、String、Parcelable、Serializable。Serializable 用反射,性能差,大数据量时优先 Parcelable 或直接用 ID 传参、目标页再查。

注意:Intent 传 Bitmap 或大数组容易触发 TransactionTooLargeException。正确做法是传 URI 或 ID,目标页自己加载。

3.2 launchMode 四个值的实际效果

launchMode 决定了 Activity 实例在任务栈里怎么复用。很多人背了标准答案但没在实际场景里验证过,下面用表格把行为和典型用途对齐:

launchMode行为典型场景
standard每次跳转都新建实例普通详情页
singleTop栈顶已有则不新建,回调 onNewIntent通知点击、搜索页
singleTask栈中已有则清空其上方的 Activity 并复用首页、主入口
singleInstance独占一个任务栈来电界面、闹钟

singleTask 最容易翻车的地方是:它会把该 Activity 之上的页面全部清掉。如果首页是 singleTask,从首页跳到详情页再跳回首页,详情页会被销毁。这不是 bug,是设计行为,但很多人没意识到。

<activity android:name=".MainActivity" android:launchMode="singleTask" android:exported="true" />

配合 onNewIntent 处理复用时的参数更新:

override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) // 必须调用,否则 getIntent 拿到的还是旧数据 handleIntent(intent) }

参数说明:setIntent 这行如果漏了,后续 getIntent() 返回的是第一次启动的 Intent,新参数读不到。这是排查“为什么第二次跳转参数没更新”时最常见的根因。

3.3 显式跳转与隐式跳转的选择

显式 Intent 指定类名,用于应用内部跳转,安全且编译期可检查。隐式 Intent 通过 action 和 category 匹配,用于跨应用调用,比如打开浏览器、分享内容。隐式跳转必须做 resolveActivity 检查,否则没有匹配应用时会抛 ActivityNotFoundException。

val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com")) if (intent.resolveActivity(packageManager) != null) { startActivity(intent) } else { Toast.makeText(this, "没有可用的浏览器", Toast.LENGTH_SHORT).show() }

参数说明:resolveActivity 返回最佳匹配,返回 null 表示无匹配。Android 11 之后包可见性收紧,需要在 manifest 里声明 queries 才能查到其他应用,否则 resolveActivity 可能返回 null 但实际能跳转。

4. 转场动画与视觉过渡:让 Activity 切换不突兀

4.1 系统内置转场动画的配置方式

Activity 之间的切换默认有淡入淡出,但很多人想自定义却不知道从哪下手。常见做法是在主题里定义 windowAnimationStyle,或者在 startActivity 后用 overridePendingTransition。

<!-- res/values/styles.xml --> <style name="AppTheme" parent="Theme.AppCompat.Light"> <item name="android:windowAnimationStyle">@style/ActivityAnim</item> </style> <style name="ActivityAnim"> <item name="android:activityOpenEnterAnimation">@anim/slide_in_right</item> <item name="android:activityOpenExitAnimation">@anim/slide_out_left</item> <item name="android:activityCloseEnterAnimation">@anim/slide_in_left</item> <item name="android:activityCloseExitAnimation">@anim/slide_out_right</item> </style>

参数说明:activityOpenEnterAnimation 是新页面进入动画,activityOpenExitAnimation 是旧页面退出动画,Close 系列对应返回时的动画。四个要配套写,只写两个会导致返回时动画缺失。

4.2 共享元素动画的实现要点

共享元素动画能让两个 Activity 之间的某个 View 平滑过渡,视觉效果好但配置项多。核心是给两个页面的对应 View 设置相同的 transitionName。

// 页面 A:启动时指定共享元素 val intent = Intent(this, DetailActivity::class.java) val options = ActivityOptionsCompat.makeSceneTransitionAnimation( this, imageView to "shared_image" ) startActivity(intent, options.toBundle()) // 页面 B:在 onCreate 里声明过渡 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.sharedElementEnterTransition = TransitionSet().apply { addTransition(ChangeImageTransform()) addTransition(ChangeBounds()) duration = 300 } setContentView(R.layout.activity_detail) // 目标 View 的 transitionName 必须和 A 页面一致 detailImage.transitionName = "shared_image" }

逻辑说明:makeSceneTransitionAnimation 把源 View 和 transitionName 绑定,目标页通过相同名字找到对应 View 做过渡。参数说明:duration 建议 200-400ms,太短看不出效果,太长显得拖沓。ChangeBounds 处理位置和大小变化,ChangeImageTransform 处理 ImageView 的 scaleType 差异,两个一起加才不会有跳变。

注意:共享元素动画在低端机上可能掉帧。如果目标页需要网络加载图片,先显示占位图,等图片加载完再触发过渡,否则动画会闪。

4.3 动画与启动速度的取舍

转场动画虽然好看,但会延长用户感知的等待时间。如果目标页本身加载就慢,动画反而放大焦虑。我的习惯是:冷启动路径上的第一个 Activity 不加复杂动画,用系统默认或极简淡入;二级页面才用共享元素或滑动。另外,动画资源用 XML 定义比代码创建更省内存,因为系统可以缓存。

5. 避坑与排查:Activity 功能代码里最容易翻车的五个点

5.1 旋转屏幕后数据丢失

现象:用户填了一半的表单,旋转屏幕后内容清空。原因:Activity 重建,成员变量归零,没有走 onSaveInstanceState 或 ViewModel。解决:把表单数据放 ViewModel,或者重写 onSaveInstanceState 存关键字段。如果不想重建,可以在 manifest 里给该 Activity 加 android:configChanges="orientation|screenSize",但这样会绕过重建,要自己处理布局切换。

5.2 启动模式导致返回栈异常

现象:从 A 跳到 B,再跳到 C,按返回键直接回到桌面而不是 A。原因:B 或 C 设置了 singleTask 或 singleInstance,清空了任务栈。解决:检查 manifest 里的 launchMode,只有主入口和特殊页面才用 singleTask。普通页面一律 standard 或 singleTop。

5.3 onActivityResult 回调不触发

现象:跳转到另一个 Activity 拿结果,返回时 onActivityResult 没执行。原因:启动时用了 startActivity 而不是 startActivityForResult,或者 requestCode 为负数,或者目标 Activity 没有 setResult。解决:确认用 startActivityForResult 启动,requestCode 用正数常量,目标页在 finish 前调用 setResult(RESULT_OK, data)。AndroidX 项目建议迁移到 ActivityResultLauncher,避免 requestCode 冲突。

5.4 内存泄漏:匿名内部类持有 Activity

现象:Activity 销毁后,Handler 或 Runnable 还在跑,导致内存泄漏。原因:匿名内部类隐式持有外部 Activity 引用,延迟任务未取消。解决:用静态内部类加 WeakReference,或者在 onDestroy 里移除所有 Handler 回调和 Runnable。

private val handler = Handler(Looper.getMainLooper()) private val updateTask = object : Runnable { override fun run() { // 更新 UI handler.postDelayed(this, 1000) } } override fun onDestroy() { super.onDestroy() handler.removeCallbacks(updateTask) // 必须移除 }

5.5 主题设置错误导致崩溃

现象:Activity 启动瞬间崩溃,日志提示“You need to use a Theme.AppCompat theme”。原因:Activity 继承 AppCompatActivity,但 manifest 里指定的主题不是 AppCompat 系列。解决:在 manifest 的 activity 标签或 application 标签上指定 Theme.AppCompat 或 Theme.MaterialComponents 派生主题。如果用了 MaterialToolbar,主题还要带 NoActionBar。

6. 进阶技巧:用 ActivityResultLauncher 替代 onActivityResult

onActivityResult 这套 API 从 Android 早期沿用至今,但它的设计有明显缺陷:requestCode 要手动管理、容易冲突、回调集中在一个方法里用 switch 分发、类型不安全。AndroidX 推出的 ActivityResultLauncher 把“启动”和“接收结果”绑定在一起,代码更清晰,也不容易出错。

下面是一个完整的迁移示例,覆盖拍照和选图两个常见场景:

class PhotoActivity : AppCompatActivity() { // 注册拍照 launcher private val takePicture = registerForActivityResult( ActivityResultContracts.TakePicture() ) { success: Boolean -> if (success) { // 图片已保存到指定 URI loadImage(imageUri) } } // 注册选图 launcher private val pickImage = registerForActivityResult( ActivityResultContracts.GetContent() ) { uri: Uri? -> uri?.let { loadImage(it) } } private lateinit var imageUri: Uri override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_photo) findViewById<Button>(R.id.btn_camera).setOnClickListener { imageUri = createImageUri() takePicture.launch(imageUri) } findViewById<Button>(R.id.btn_gallery).setOnClickListener { pickImage.launch("image/*") } } private fun createImageUri(): Uri { // 在 FileProvider 配置的路径下创建临时文件 val file = File(getExternalFilesDir("images"), "photo_${System.currentTimeMillis()}.jpg") return FileProvider.getUriForFile(this, "$packageName.fileprovider", file) } }

逻辑说明:registerForActivityResult 必须在 Activity 创建阶段调用(onCreate 之前或之中),不能在点击事件里注册。launch 方法触发跳转,回调在 lambda 里处理结果。参数说明:TakePicture 合约需要一个 Uri 参数指定图片保存位置,GetContent 合约传 MIME 类型如 "image/*"。FileProvider 需要在 manifest 里声明,并配置 file_paths.xml。

对比 onActivityResult 的优势:每个 launcher 独立回调,不用 switch;类型由合约保证,编译器帮你检查;requestCode 由框架管理,不会冲突。迁移成本也不高,一个页面通常两三个 launcher 就覆盖了。

注意:registerForActivityResult 如果在 onCreate 之后调用会抛 IllegalStateException。如果需要在 Fragment 里用,要在 onAttach 之后、onCreate 之前注册,或者用 Fragment 自己的 registerForActivityResult。

我自己的习惯是:新项目一律用 ActivityResultLauncher,老项目遇到 onActivityResult 相关 bug 时顺手迁移。踩过最深的坑是在 Fragment 的 onCreateView 里注册 launcher,结果生命周期不对导致回调丢失。后来统一改到 Fragment 的字段初始化位置,再没出过问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询