1. 生命周期为什么值得系统学三个最常见的翻车现场做Android开发四五年后回头看我发现一个有点反直觉的事真正决定一个App质量上限的往往不是用了多新的架构、多酷的动画而是Activity和Fragment这套最基础的生命周期有没有吃透。为什么会这么说因为生命周期是一切UI组件的地基。地基不稳上层用再好的框架也是摇摇晃晃。我团队里来过一个工作经验两年的同学Kotlin协程、Jetpack Compose都聊得头头是道结果一遇到实际问题就卡壳。有一次他负责一个订单列表页用户下单成功后需要刷新列表他直接在onResume里写了重新拉数据的代码导致每次从后台切回来列表都会闪一下而且数据加载期间用户滑动的体验非常糟糕。后来排查了半天才发现问题根源不是网络请求慢而是onResume被调用得太频繁又没做任何防御性判断。那是我第一次深刻体会到生命周期不是面试题而是每个页面每次交互都在发生的真实事件流。具体来说生命周期没掌握好最常见的就是下面这三个翻车现场状态丢失用户在编辑框里输入了半天的内容转一下屏幕或者来一个电话回来发现内容全没了。安卓系统会在配置变化时销毁并重建Activity如果没做状态保存所有UI状态都会被重置。内存泄漏在Activity里发起一个异步网络请求回调里要更新UI但用户在请求回来之前关闭了页面。如果请求持有Activity的引用这个Activity就永远不会被回收内存泄漏就是这么来的。界面错乱Fragment在后台被重建时如果视图还没准备好就直接操作控件直接抛空指针。或者从后台回到App时Fragment的状态和当前显示的内容对不上明明应该停留在这个Tab结果跳到了另一个。这三个问题每一个都能在线上事故里找到对应案例。但它们的解法并不复杂核心就是搞懂生命周期回调的时机和规则知道每个方法里该做什么、不该做什么。这正是我这篇文章想带你系统过一遍的东西。这篇文章我会以实战视角把Activity和Fragment的生命周期、配置变化处理、任务栈与启动模式、运行时权限这几个核心主题完整梳理一遍。内容偏基础和进阶之间适合刚入门想打牢根基的同学也适合做了几年开发但总感觉这些概念模模糊糊的同行。2. Activity生命周期全景七个回调的职责边界与使用禁忌2.1 一张表看懂七个回调的调用时机安卓的Activity生命周期回调一共七个onCreate、onStart、onResume、onPause、onStop、onRestart、onDestroy。它们在不同的事件触发下会被系统依次调用整个流程可以用下表概括回调方法调用时机页面状态onCreateActivity第一次创建时不可见onStartActivity即将可见可见但不可交互onResumeActivity获得焦点可交互可见且可交互onPauseActivity即将失去焦点部分可见可见但不可交互onStopActivity完全不可见被覆盖或退到后台不可见onRestartActivity从停止状态重新回到前台可见但不可交互onDestroyActivity被销毁前不可见从表格可以很直观地看出一个规律onCreate和onDestroy是一对onStart和onStop是一对onResume和onPause是一对。系统通过这三对回调把一个页面从出生到死亡的全过程分成了几个可感知的阶段。但这里有个容易误判的地方一对回调之间的执行顺序不一定紧挨着。比如onPause之后系统可能会直接走onStop也可能会因为一个短暂的对话框遮挡后返回onResumeonStop不一定会被执行。新手写代码时如果假设onPause后必走onStop就可能在逻辑上埋下隐患。2.2 onCreate只做初始化别碰重活onCreate是开发者最熟悉的回调也是整个Activity生命周期里调用次数最少的一个——理论上每个Activity实例只调用一次。这里适合做的工作包括通过setContentView加载布局初始化View引用比如findViewById初始化ViewModel、仓库类等业务组件恢复之前保存的状态从Bundle参数中读取但onCreate里最忌讳的就是做耗时操作。很多新手会把网络请求、数据库读取直接放在onCreate里觉得反正页面需要这些数据。实际上onCreate执行完之后用户仍然看不到页面此时做耗时操作只会白白拖慢首帧渲染时间。正确的做法是数据加载可以放在onStart或onResume之后配合生命周期感知的加载机制或者用异步方式加载避免阻塞主线程。注意onCreate里的Bundle参数只有在Activity被异常销毁重建时才有值。如果是第一次正常启动这个参数是null。所以读取时需要判空。2.3 onStart与onResume可见不等于可交互onStart调用后页面已经进入可见状态用户能看到界面了但还不能操作。什么时候会处于这种能看到但点不了的状态比如一个半透明的Activity覆盖在上面或者正在从后台恢复的过程中。onResume调用后Activity才算真正获得了焦点用户既可以看见也可以交互。这里有个很重要的细节onResume里适合做重新开始的事情比如恢复动画、重新绑定传感器监听、检查是否需要刷新数据但同样不适合做耗时初始化因为此时页面已经显示出来了任何阻塞都会直接影响用户体验。一个实用的小技巧如果页面需要根据后台切回或跳转返回来刷新数据可以考虑在onResume里做轻量级刷新但一定要加防抖或去重判断避免频繁调用导致列表闪烁。2.4 onPause与onStop轻量保存别在这里做重操作当用户按下Home键打开另一个App、弹出来一个对话框、或者正在切换到下一个Activity时当前Activity会依次走onPause和onStop。onPause的含义是当前页面即将失去焦点但此时页面通常仍然是可见的比如被一个对话框部分遮挡。由于onPause之后系统随时可能把App进程放到后台在这里执行耗时操作是不安全的——因为系统不会等你的操作执行完再让你进入后台。我在实际项目里常用的做法是onPause里暂停掉正在播放的音频、视频、动画onPause里做轻量级状态保存比如把当前编辑框内容写进内存缓存或SharedPreferencesonStop里释放Camera、传感器等系统资源注销一些不必要的监听器这里要特别提醒不要在onStop里做UI更新因为此时页面已经完全不可见更新UI没有意义还白白消耗资源。另外从Android 7.0开始系统在onStop阶段就可以考虑杀死进程了所以持久化操作越早做越安全。2.5 onRestart与onDestroy两个容易忽略的边界onRestart比较好理解Activity从停止状态onStop之后被重新带回前台时会先走onRestart再走onStart、onResume。这意味着你在onStop里做的一些暂停操作需要在onRestart之后的onStart或onResume里恢复。比如我在onStop里暂停了一个轮播图自动播放那么onStart里就要恢复。onDestroy则是Activity被销毁前的最后一个回调适合做彻底清理取消未完成的异步任务注销注册过的BroadcastReceiver解绑Service关闭数据库连接、Cursor等但这里有个坑onDestroy不一定会被调用。当系统内存极度紧张时进程可能直接被杀死而不经过任何生命周期回调。所以真正关键的清理逻辑不能只依赖onDestroy还要结合onStop里的兜底方案。2.6 生命周期方法里的高频踩坑点踩坑点实在太多我挑几个真实项目里最常见的说出来重复setContentView在onCreate里被调用不止一次第二次会抛出异常或导致布局异常。监听器未注销在onCreate里注册了一个BroadcastReceiver忘记在onDestroy里注销页面销毁后receiver仍然活着轻则日志刷屏重则内存泄漏。异步回调操作已销毁的View网络请求回调里直接操作View控件但页面已经销毁此时控件的root view已经被detach操作可能抛异常或无效。finish()与Back键混淆点击返回键会先执行onPause→onStop→onDestroy但通过finish()方法销毁时可能跳过onStop。虽然这种情况不常见但代码里别对一定会执行某个回调持有绝对信心。3. Fragment生命周期挂在Activity身上的租客谁先谁后别搞反3.1 Fragment生命周期的11个方法与Activity的对应关系Fragment的生命周期方法比Activity更多一共11个。很多同学一看到这么多回调就头大但换个角度想Fragment本质上就是寄生在Activity身上的一个小Activity它的生命周期和Activity高度对应只是多出了几个与视图相关的回调。这11个回调分别是onAttachFragment与Activity建立关联onCreateFragment实例创建onCreateView创建Fragment的视图onViewCreated视图创建完成后回调onStartFragment可见onResumeFragment可交互onPauseFragment失去焦点onStopFragment不可见onDestroyViewFragment的视图被销毁onDestroyFragment实例被销毁onDetachFragment与Activity解除关联突出重点onCreateView和onViewCreated是Fragment独有的也是开发者最容易混淆的一对。onCreateView的职责比较简单返回一个View对象作为Fragment的根布局。很多人在这里做findViewById但其实更推荐在onViewCreated里做视图初始化因为此时视图树已经构建完成并且可靠地附加到了Activity上。如果在onCreateView里直接操作子控件虽然大多数时候没问题但偶尔会因为布局尚未measure完成而拿到不正确的宽高。3.2 Fragment与Activity生命周期的联动顺序理解了Fragment是寄生在Activity里的租客就不难理解生命周期联动规则Activity走到哪个阶段Fragment会跟着走到对应阶段。以页面启动为例完整顺序如下Fragment.onAttachActivity.onCreateFragment.onCreateFragment.onCreateViewFragment.onViewCreatedFragment.onStartActivity.onStartFragment.onResumeActivity.onResume等等这个顺序是不是和网上流传的不太一样网上有些文章说Fragment.onStart在Activity.onStart之前Fragment.onResume在Activity.onResume之后这个说法在某些版本下并不完全准确。从Android官方文档和实际调试来看Activity.onStart和Fragment.onStart的执行顺序取决于Fragment事务提交的时机。用supportFragmentManager的add/replace方式加入的Fragment在Activity.onStart之前执行onStart而使用FragmentTransaction的attach/detach方式时序会有细微差别。所以这里我不建议死记硬背一个绝对顺序而是记住一个更实用的原则Activity才是宿主Fragment的视图和状态依赖于Activity。在Fragment里回调Activity方法时要做好判空。3.3 onViewCreated到底解决了什么问题onViewCreated这个回调是API 28Android 9.0正式引入的。它解决的核心问题是回调时机稳定、视图完全可用。在实际开发中很多视图操作必须等到视图树构建完成才能做比如设置适配器给RecyclerView设置LayoutManager给按钮设置点击监听初始化动画这些工作放在onViewCreated里最稳妥。放在onCreateView里可能会有view尚未完全附加到窗口的问题放在onResume里又太晚。提示onViewCreated和onActivityCreated是两回事。onActivityCreated在API 28之后被标记为deprecated官方推荐用onViewCreated代替它做视图初始化。3.4 Fragment常见陷阱getActivity()为空、视图复用、hide与removeFragment的坑比Activity多得多我挑三个日常开发最常遇到的陷阱一getActivity()返回nullFragment.getActivity()在Fragment已经detach之后调用会返回null此时如果直接使用它操作Activity的控件会抛NullPointerException。解决办法是先用isAdded()判断Fragment是否已经附加到Activity上再做后续操作。陷阱二View复用导致的重复初始化如果你用show/hide方式管理Fragment在ViewPager里很常见Fragment的onCreateView每次都会重新调用但视图不会每次都重新创建——如果布局通过add方式添加同一个View可能被重复添加上去。这会导致界面重复、控件状态混乱。解决思路是在onCreateView里先判断root View是否已经存在存在就直接返回或者用replace方式代替add方式。陷阱三hide/show丢失交互状态用hide/show切换Fragment时Fragment的生命周期只走到onPause和onStop不会走onDestroyView。这意味着你的View还在但如果用户之前滚动到某个位置切回来时滚动位置通常还在可如果Fragment内部有定时器或动画切换时往往需要手动暂停。3.5 ViewPager Fragment场景下的生命周期变化规律ViewPager Fragment是Android开发中最经典也最痛的模式。默认情况下ViewPager会预加载当前页面的左右两页也就是说用户还没滑到的页面它的生命周期可能已经走到onResume了。这个特性带来两个问题问题一页面虽然不可见但onResume已经执行导致数据提前加载、埋点误报。问题二如果Fragment内部有轮播、音频播放等逻辑会提前启动。解决办法有两个方向。一是通过setOffscreenPageLimit(0)禁止预加载但这只是让Fragment少预加载一层不是完全取消二是结合FragmentTransaction的setMaxLifecycle方法把Fragment的状态限制在STARTED只有当前显示时才进入RESUMED。setMaxLifecycle是官方推荐方案也是处理ViewPager Fragment页面可见性的最佳实践。4. 配置变化屏幕旋转后你的界面状态去哪儿了4.1 配置变化如何触发Activity重建配置变化Configuration Change指的是系统级参数发生变化比如屏幕方向旋转、语言切换、字体大小改变、深色模式切换等。这些变化触发后系统默认行为是销毁当前Activity然后用新的配置重新创建它。这个默认行为有很多开发者不理解为什么我手机上旋转了一下屏幕Activity就被重新创建了因为不同的屏幕方向需要使用不同的资源文件比如layout-land文件夹里的布局系统只能通过重建Activity来加载正确的资源。配置变化导致的销毁属于异常销毁它和用户主动finish的区别在于系统给了一次保存状态的机会就是onSaveInstanceState回调。屏幕旋转的完整流程是这样的onSaveInstanceState被调用状态保存onPause被调用onStop被调用onDestroy被调用Activity实例销毁系统用新的屏幕配置重新创建Activity实例新Activity走onCreateBundle参数里带着之前保存的状态4.2 状态保存三件套onSaveInstanceState、ViewModel、rememberSaveable既然系统给了保存机会那我们就得接住。处理配置变化下的状态保存有三件套配合使用效果最好第一件onSaveInstanceState系统在Activity异常销毁前会调用这个方法让你把轻量级的UI状态存进Bundle。恢复时onCreate的Bundle参数或者onRestoreInstanceState回调里可以取出来。适合存的数据编辑框内容、复选框选中状态、当前滚动位置等。不适合存的数据大对象、Bitmap、复杂的业务数据。因为Bundle底层要序列化写入磁盘内容过多会导致丢帧甚至TransactionTooLargeException。第二件ViewModelViewModel是处理配置变化的最佳利器。它的生命周期和Activity实例解耦当Activity因配置变化重建时ViewModel实例会保留下来新的Activity拿到的是同一个ViewModel对象。ViewModel最大的优势是不仅保存生命周期短的UI状态还能保留业务数据、网络请求的LiveData结果不像Bundle需要序列化。复杂的列表数据、非序列化对象都可以放进ViewModel里。第三件rememberSaveableCompose场景如果你在用Jetpack Compose开发rememberSaveable是Compose状态保存的基础工具。它结合了remember和Bundle保存两种能力在重组后可以恢复状态在Activity重建后也能从Bundle中恢复。4.3 ViewModel跨配置保留的底层原理ViewModelStore与NonConfigurationInstances很多同学知道用ViewModel能避免屏幕旋转丢状态但不理解原理。这里我尽量用大白话解释。当Activity因为配置变化需要重建时系统会调用一个内部方法叫onRetainNonConfigurationInstance它会把Activity关联的ViewModelStore对象保存到一个NonConfigurationInstances里。重建后的Activity拿到这个NonConfigurationInstances时会检查里面有没有ViewModelStore如果有就直接取出来用。ViewModelStore本质上就是一个以字符串为key、ViewModel为value的HashMap。每个Activity都会关联一个自己的ViewModelStore实例。ViewModel本身不会持有Activity的引用而是持有Activity的application context所以配置变化时它不会因为Activity销毁而变成悬空引用。ViewModel的比较生命周期如下Activity第一次创建时ViewModelStore创建ViewModel通过getViewModelStore()方法获取然后new出来放进Store里。Activity因配置变化重建时ViewModelStore被保留Activity拿到之前的Store。Activity真正被finish销毁时ViewModelStore会清空此时会回调所有ViewModel的onCleared方法。明白原理之后有几个实用的推论ViewModel里不要持有Activity、View类型的引用否则配置变化时内存泄漏照样会发生。ViewModel的onCleared回调时机是Activity真正销毁时不是配置变化时。多个Fragment可以共享同一个Activity的ViewModel通过requireActivity().getViewModelStore()拿到同一个Store这是Fragment通信的好姿势。4.4 处理配置变化的新思路Lifecycle 2.2的显示/隐藏生命周期除了传统的手动保存状态方式谷歌在Lifecycle 2.2之后引入了新的生命周期方法onStart和onStop的替代——onShow和onHide用于LifecycleOwner和LifecycleRegistry的ensureActive状态但这里我主要想说的是一个更实用的变化Lifecycle 2.2开始支持Lifecycle只在主线程回调并且可以精确地控制重复生命周期事件。实际上处理配置变化最省心的方式之一是在Manifest里给Activity加上android:configChangesorientation|screenSize配置让系统不销毁Activity而是直接回调onConfigurationChanged方法。这种方式我建议在特定场景下用比如视频播放器全屏切换但不要全局滥用因为这意味着你自己要处理所有配置切换的资源加载逻辑工作量反而更大。另外一个更推荐的方向是用状态驱动UI。数据放在ViewModel里UI通过observable模式订阅状态配置变化后重组UI时数据自动恢复。这也是MVVM架构受欢迎的重要原因它天然就把配置变化导致UI重置的问题解决了一大半。4.5 配置变化实操建议基于我自己的项目经验给出几个实操建议在ViewModel里只放和UI不直接绑定的业务数据比如用户信息、列表数据、请求结果。UI状态输入框内容可以放SavedStateHandle里。SavedStateHandle是Google推荐的新方案它属于ViewModel的构造参数底层利用Bundle保存可以自动在配置变化和进程死亡后恢复数据。如果用了LiveData和DataBinding配置变化全程基本不用手动做状态保存ViewModel恢复数据后UI自动回调。5. 任务栈与启动模式四种模式在实际业务里到底怎么选5.1 任务栈的栈帧思维任务栈Task Stack是Android管理Activity的组织方式。系统把每个正在运行的Activity放进一个栈里栈顶是用户当前看到的页面。新Activity入栈push返回键出栈pop栈顶变化时系统会重新计算哪些Activity显示、哪些暂停、哪些停止。这个模型用一句话概括就是Activity永远只有一个栈顶的能活着RESUMED下面的要么STOPPED要么PAUSED。任务栈有几个容易忽略的特性一个App可以有多个任务栈通过taskAffinity属性指定。任务栈的栈底Activity往往是应用入口Activity比如MainActivity它从栈底被移除时整个任务栈就结束了。系统默认情况下任务栈的Activity会被记忆。用户按下Home键再点击图标回到App恢复的是之前的栈而不是重新创建栈底Activity。5.2 四种启动模式standard、singleTop、singleTask、singleInstanceActivity的启动模式通过Manifest的launchMode属性配置一共四种standard、singleTop、singleTask、singleInstance。这四种模式的核心区别在于已存在实例时如何处理新Intent。mode 1standard默认每次启动都创建一个新实例放入当前任务栈的栈顶。这是最简单也最常用的模式大多数页面都适合用它。普通场景用一个Activity实例就够了。mode 2singleTop如果栈顶已经有一个该Activity的实例那么不会创建新实例而是调用旧实例的onNewIntent方法。如果栈顶不是该Activity那就创建新实例。实际适用场景通知栏推送点击时如果用户正停留在消息详情页重复点击不应该再创建新的详情页。mode 3singleTask系统会查找任务栈中是否已经存在该Activity的实例如果存在就把它上面的所有Activity全部弹出让该实例回到栈顶并回调onNewIntent。如果不存在就创建新实例并放入栈中。实际适用场景主界面MainActivity、购物车页面、个人主页。这种页面一般是App的核心枢纽页不应该存在多个副本。mode 4singleInstance比singleTask更极端该Activity实例会单独放在一个独立的任务栈中这个栈里只有它一个Activity。其他Activity启动时与它不在同一个栈中。实际适用场景来电接听页面、闹钟响铃页面、视频通话页面。这种页面需要独占系统的返回键和最近任务列表用户按返回时直接退出到桌面而不是回到上一个页面。为了方便对比我把四种模式整理成一张表模式已存在实例时行为适用场景standard每次创建新实例普通跳转页面singleTop栈顶存在则复用并回调onNewIntent推送详情、搜索结果singleTask回栈顶清掉上面的Activity回调onNewIntent首页、主界面、购物车singleInstance独立任务栈实例全局唯一来电页、闹钟、视频通话5.3 Intent Flags代码里动态控制启动行为的另一套API除了Manifest里的launchModeIntent还可以携带Flags来临时指定启动模式。Flags的优先级高于Manifest配置。常用的几个FlagsFLAG_ACTIVITY_NEW_TASK在新的任务栈中启动Activity。如果已存在相同taskAffinity的栈则复用。FLAG_ACTIVITY_CLEAR_TOP如果Activity已在栈内清掉它上面的所有Activity让它回到栈顶。FLAG_ACTIVITY_SINGLE_TOP相当于singleTop效果与newTask组合使用可以实现类似singleTask的效果。FLAG_ACTIVITY_REORDER_TO_FRONT不清理其他Activity只是把已存在的Activity直接移到栈顶。FLAG_ACTIVITY_NO_HISTORYActivity退出后不保留在栈中。这里有一个大家容易迷糊的点FLAG_ACTIVITY_CLEAR_TOP FLAG_ACTIVITY_SINGLE_TOP 的组合。CLEAR_TOP默认会把目标Activity的旧实例destroy掉再创建一个新的但如果同时加上SINGLE_TOP就不会重新创建新实例而是复用旧实例直接回调onNewIntent。这个组合实际上等价于某几种场景下的singleTask。5.4 真实案例从通知栏点击消息如何避免打开多个Activity实例这是我接手过的一个实际需求App收到订单状态变更推送后用户点击通知应该跳到订单详情页。如果直接startActivity一个订单详情Activity假设用户已经在详情页看单号再点一次通知会出现两个详情页返回键要按两次才能退出体验非常糟糕。正确做法是用singleTop模式启动订单详情Activity或者给Intent加上FLAG_ACTIVITY_SINGLE_TOP然后通过onNewIntent方法处理新推送带来的数据更新逻辑。这样用户在详情页时点击通知只是在原页面上刷新数据不会创建新页面。另外一个常见场景是跳转到首页并清空全部页面比如用户点击退出登录后跳回登录页同时加FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_CLEAR_TASK就能实现。CLEAR_TASK会清空当前任务栈然后用新栈装载目标Activity。5.5 taskAffinity与进程管理taskAffinity是Activity的一个属性默认值是包名它决定了Activity所属的任务。对大多数App来说用默认值就够了但当你想让某个Activity和主任务分开时就可以设置不同的affinity。典型场景App里跳转到网页浏览器时浏览器自己的Activity affinity是浏览器包名所以它在新任务栈里打开如果你想让自己的Activity进入另一个已有的任务栈就需要让affinity匹配那个栈的affinity。还有一点和生命周期相关不同任务栈之间切换时非栈顶Activity都会进入STOPPED状态。比如你切到一个singleInstance的页面原来的任务栈整体就STOPPED了回来时再RESUMED。6. 运行时权限从安装时授权到用的时候再问的演进与实践6.1 targetSdkVersion决定了你的权限行为权限部分有一句老话值得放在开头Android 6.0API 23开始危险权限从安装时全部授权改成了运行时动态申请。但这句表述里隐藏了一个关键细节是否走动态权限取决于你的targetSdkVersion而不只是设备系统版本。如果targetSdkVersion小于23系统完全走旧逻辑安装时授权所有权限App安装后权限默认全部可用。如果targetSdkVersion大于等于23危险权限就必须在运行时动态申请。Google对新上架App的targetSdkVersion有最低要求目前是34所以到2025年还想着我不升级targetSdk躲开动态权限已经完全不现实了。而且从Android 10开始系统还会在权限管理界面强制展示哪些App没有实时请求权限躲是躲不掉的。所以正确的思路就是拥抱动态权限把权限申请流程打磨好同时适配好每一代的权限变化。6.2 正常权限与危险权限分类决定策略Android的权限分为两大型正常权限Normal Permission安装时自动授予不弹窗不需要运行时申请。比如INTERNET、ACCESS_NETWORK_STATE、VIBRATE等。危险权限Dangerous Permission涉及用户隐私或敏感数据必须在运行时申请。比如定位、相机、麦克风、联系人、存储、短信、电话等。危险权限被系统分成若干权限组Permission Group。同一组内的权限只要有一个被授权同组其他权限就自动获得授权。比如CAMERA权限授权后同组的RECORD_VIDEO就不需要单独申请了因为系统认为它们是一类。权限组的这个特性有陷阱分组逻辑在不同Android版本上并不完全一致。所以在代码里判断权限时不能假设有一个组内权限被授权其他权限一定可用更稳妥的做法是分别检查每个权限的grant状态。6.3 标准申请流程判断、解释、申请、处理回调动态权限申请的标准套路我在项目里沉淀成一个固定模板了第一步判断是否已授权if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) PackageManager.PERMISSION_GRANTED) { // 已经授权直接执行相机相关逻辑 openCamera() } else { // 未授权进入申请流程 }第二步是否需要解释如果用户之前拒绝过权限再次申请前应该弹出一个解释说明弹窗告诉用户为什么要用这个权限。判断逻辑是if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { // 用户拒绝过显示解释然后再次申请 showExplanationDialog { requestPermission() } } else { // 第一次申请直接调系统弹窗 requestPermission() }注意shouldShowRequestPermissionRationale在第一次申请前返回false拒绝过一次后返回true勾选不再询问后返回false。所以这个方法的语义并不完全等同于用户是否拒绝过它更适合理解为是否需要解释理由。第三步发起申请private val PERMISSIONS_REQ_CODE 100 private fun requestPermission() { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.CAMERA), PERMISSIONS_REQ_CODE ) }第四步处理回调override fun onRequestPermissionsResult( requestCode: Int, permissions: Arrayout String, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode PERMISSIONS_REQ_CODE) { if (grantResults.isNotEmpty() grantResults[0] PackageManager.PERMISSION_GRANTED) { openCamera() } else { // 用户拒绝跳权限设置页引导 showSettingsDialog() } } }这套流程写下来不难难的是把所有分支都处理到位。我建议把权限申请封装成一个工具类统一处理已授权、未授权、解释、拒绝、不再询问这五种状态避免在每个页面重复写逻辑。6.4 Android 11API 30的权限变化Android 11开始动态权限有了新的交互变化一次性授权One-time permission用户可以选择仅这一次App下次再访问时会被视为未授权。权限自动重置用户如果几个月没用某App系统会重置所有运行时权限取消之前的授权状态。多个权限申请合并系统可能会将多次申请合并显示用户批量处理。这些变化意味着不能假设用户授权一次就永久有效在每次要访问敏感数据前都需要重新检查权限状态做好被拒绝后的降级处理。比如定位权限被重置后App应该自动降级为粗略定位或者提示用户开启。6.5 Android 13API 33的权限演进Android 13带来了几个重要的权限变化通知权限独立化从Android 13开始通知权限POST_NOTIFICATIONS成为危险权限需要在运行时申请。以前通知权限不需要在Manifest里声明13开始如果不申请通知默认关闭用户收不到任何推送。这直接影响所有依赖推送的App。照片选择器Photo PickerAndroid 13引入系统级的照片选择器用户可以在不授予整个相册读取权限的情况下选择指定照片授权给App。这大大减少了READ_EXTERNAL_STORAGE权限的申请频率。针对通知权限我建议在App启动时主动申请或者在需要推送功能时申请不要等用户开启推送功能发现没权限后才去处理。针对照片选择器如果targetSdkVersion 33推荐直接用Photo Picker API替代READ_MEDIA_IMAGES等权限。6.6 权限申请实操建议最后分享几个从实际项目中总结出来的操作经验不要一股脑申请所有权限在首页弹一排权限弹窗的体验极其糟糕用户大概率全给拒绝了。尽量在用户真正需要使用某个功能时再申请对应权限。申请权限前先解释理由第一次申请不用解释但拒绝后再申请一定要先弹自定义解释弹窗。这一步能显著提升授权率。处理永久拒绝场景如果用户拒绝了两次以上shouldShowRequestPermissionRationale返回false此时系统弹窗不会出现必须跳转应用设置页。跳转代码Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse(package:$packageName))适配深色模式和配置变化权限申请弹窗属于系统对话框不会导致Activity重建但如果你在弹窗回调里操作了View要确保回调时Activity还活着。7. 写在最后一些个人体会把Activity和Fragment这套生命周期体系彻底搞清楚之后再回头看我刚入行时遇到的那些问题其实都不是复杂的技术难题只是对这套机制的理解不够。屏幕旋转丢状态、异步回调崩溃、任务栈错乱、权限被拒……这些问题的答案全都藏在这些基础概念里。我的一个建议是不要只是把它们当成面试知识点背一遍就完事。真正有效的学习方式是在自己的项目里刻意制造一些事故旋转屏幕观察生命周期日志往任务栈里疯狂跳转然后观察返回行为用adb shell dumpsys activity activities查看当前任务栈状态。这些操作能帮你把文档上的理论变成肌肉记忆。如果时间有限我在实际项目中最高频用到的技能就这三样ViewModel保存状态、启动模式里的singleTop和singleTask、权限申请的统一封装。先把这三样练熟再去啃更深的原理性价比最高。这篇文章没有覆盖到所有Android基础细节但如果你能把这几个主题的原理吃透日常开发里的绝大多数生命周期问题应该都难不倒你了。剩下的就在实战中慢慢补吧。