
这个标题第一眼看上去像某本爽文的章节名但懂行的人会会心一笑——“斗破ADK Java苍穹”这种命名本质上是把 Android 开发者的能力成长拆成了一个个“修炼境界”。“第6境斗王”对应的是 Session 与会话状态管理“化翼藏忆”则是说怎么把用户的状态安全地存下来、在恰当的时机恢复。说白了这一境练的就是两件事会话怎么管内存怎么省。很多开发者写了两三年界面对 Activity 生命周期、列表加载倒背如流但一遇到“为什么用户杀进程回来以后登录态丢了”“为什么内存看着不大却总是 OOM”这类问题就抓瞎。Session 和 Memory 这两个词听起来基础实际上恰恰是中级开发者和高级开发者的分水岭。这篇就按我自己带项目、做性能优化时沉淀下来的思路把这一境拆开讲透。1. 斗王之境为什么要单独把 Session 与 Memory 拎出来练1.1 这套进阶脉络背后的真实需求你去看现在市面上的技术课程、进阶路线几乎都会把“数据持久化”“内存优化”放到中高级阶段。原因很简单初级开发只需要让功能“能跑”而到了斗王这个层级你要让 App“能扛”。“能扛”包含两层意思扛得住用户的各种异常使用路径切后台、杀进程、断网、覆盖安装、系统重启。扛得住长时间运行后的资源消耗不泄漏、不膨胀、不莫名崩溃。Session 管的是“用户从哪来、现在是谁、要往哪去”。Memory 管的是“这一切都得在有限的内存里完成”。这两者还互相纠缠——你存 Session 要占内存内存紧张时系统可能连你的进程一起杀掉杀完你又得靠 Session 恢复现场。所以把它们放在同一境里练不是巧合是实战需求。1.2 “藏忆”到底藏的是什么标题里的“化翼藏忆”四个字对应到技术上就是两件事记忆状态数据。包括登录令牌、用户信息、页面临时输入、滚动位置、表单草稿。藏持久化与恢复。不仅要把数据写到磁盘还要能在正确的时机读回来。很多人只做到前半截登录成功了把 token 往 SharedPreferences 里一扔就算完事。但真到了进程被系统回收、用户从最近任务列表划回来的时候你才会发现恢复的逻辑远比存储的逻辑复杂。要判断 token 有没有过期、要不要刷新、页面栈要恢复到哪一层、之前填了一半的表单还在不在。这一节我把“藏”拆成三个层次内存态、持久态、恢复态。理解了这三层Session 管理才算入门。1.3 这一境适合谁来跨如果你已经能熟练开发完整功能模块但对以下问题心里没底那这一境就是为你准备的用户登录后杀掉 App再次打开要求重新登录体验割裂。列表页滑到一半App 切到后台再回来位置丢了。用 Profiler 一看内存曲线一路向上找不到是谁在作怪。多进程或者多 Module 之间共享登录态各玩各的数据不一致。这阶段不需要你像系统工程师一样去抠内核但需要你建立一套完整的“状态管理观”。下面我从 Session 和 Memory 两个大方向分别讲。2. Session 管理一场会话在 App 里到底怎么流转2.1 会话状态分三层别混在一起存我见过太多项目把所有状态塞进同一个全局类里Activity 也读、网络层也读、数据库也读最后改一个字段牵一发动全身。Session 管理的第一步是先给状态分层。按我的习惯会分成三层UI 状态层页面级数据比如当前输入的文本、列表滚动位置、选中项。这层生命周期很短通常只活到 Activity 销毁。会话状态层一次“登录会话”内的数据比如当前用户信息、会话令牌、权限掩码。这层跟着登录态走登出即清。持久身份层设备级身份数据比如设备唯一标识、上次登录账号、加密后的刷新令牌。这层基本不随会话消失。这三层的数据存储方式完全不同。UI 状态适合放在 ViewModel 或者 onSaveInstanceState 里会话状态适合放在内存缓存持久化副本持久身份层必须放到加密存储或系统 AccountManager 一类的机制中。下面这张表是我实际项目里常用的选型参考状态类型存储方案生命周期典型数据UI 临时状态ViewModel / SavedStateHandleActivity 重建即恢复输入框内容、筛选条件会话级状态内存单例 SharedPreferences/DataStore登录期间有效登出清除用户信息、访问令牌持久身份加密存储 / Keystore跨会话保留刷新令牌、设备 ID很多新手容易犯的错是把刷新令牌和用户昵称存在同一个 XML 里既不加密也不分类。等到要清理会话时要么一删全删要么删不干净。分层之后每层状态有独立的存取入口出了问题也容易定位。2.2 登录态与令牌安全是第一要务Session 的核心是身份凭证。移动端最常见的凭证是 Token分访问令牌和刷新令牌两层。访问令牌有效期短刷新令牌有效期长。这里有一个很多项目都会踩的坑把 Token 直接明文保存在 SharedPreferences 里。虽然 SharedPreferences 在应用私有目录下普通用户看不到但只要设备被 root或者开启了备份数据就可能被导出。更稳妥的做法是访问令牌可以放在内存里App 启动时从持久层读取运行期不落盘。刷新令牌必须加密存储推荐使用 Android Keystore 生成密钥再用 AES 加密后写入本地文件。涉及多设备登录态同步时不要把用户密码存在任何本地存储里。我在一个项目里排查过一例“用户反馈重新安装后还是旧账号”的问题最后发现就是刷新令牌存了明文备份恢复时被一起还原了。换成 Keystore 加密后再配合服务端令牌吊销这个问题才彻底解决。2.3 进程被回收后的现场恢复Session 管理最难的一环是“恢复”。Android 系统回收进程是常态尤其是低内存设备。你的 Activity 还在返回栈里但进程已经没了。用户从最近任务切回来时系统会重建 Activity如果什么都不做用户看到的就是冷启动首页之前的操作全丢了。恢复策略我总结成三步在onSaveInstanceState里保存页面必需的最小状态集。别试图保存整个列表数据那是 ViewModel 数据层的职责。用SavedStateHandle配合 ViewModel把“界面能立即重建”和“数据从缓存加载”两件事拆开。恢复时先展示骨架或缓存数据再在新数据到达后刷新。有一个容易被忽略的恢复点是“登录态的静默刷新”。如果 App 启动时发现访问令牌过期但刷新令牌还有效就要在用户无感知的前提下完成刷新再恢复原页面。这要求在启动流程的最前端就加一个“会话初始化”环节避免页面已经加载了却拿不到用户身份。我习惯在应用入口处维护一个会话启动器统一处理读取本地会话→判断有效性→必要时刷新→分发到目标页面。这样业务代码里不需要到处判断登录态Session 的管理也就收敛到了单一模块。3. Memory 管理Java 对象在 Android 上的生存法则3.1 先看懂 ART 与 Java 堆的关系Android 从 5.0 开始全面使用 ART 运行时App 跑在虚拟机之上但内存最终由系统统一调度。对于 Java 开发者来说你 new 出来的对象都在堆里GC 负责回收不可达对象。但是 Android 的内存压力比普通 Java 服务端大得多原因有这么几条每个 App 有系统分配的内存上限不同设备差异很大。我见过最低的只有 64MB高端的可以到 512MB。UI 需要响应快GC 抖动会导致掉帧。移动设备内存带宽有限大量对象分配会让 GC 频繁触发。所以 Android 内存优化的核心不是“尽量减少内存占用”而是“让内存分配和回收都变得可预测”。我经常用生活里的场景来类比Java 堆就像你家里的储物间GC 是定期上门收废品的大爷。你不断往储物间塞东西大爷就得频繁来收。但大爷每次来你都得停下手里的事等他——这就是 GC 造成的卡顿。所以高手会怎么做少产生垃圾让大爷一天只来一次每次来都能满载而归。3.2 内存泄漏的四种典型隐患内存泄漏是“斗王”路上的头号大敌。对象明明已经不需要了但还被某个根引用挂着GC 死活收不掉。日积月累OOM 就会找上门。按我的排查经验Android 里最常见的是下面四种第一种Activity 被静态引用持有。比如在单例里存了一个 Context或者用 static 变量指向了 Activity。Activity 本身生命周期短被 static 一hold住整个视图树全泄漏。第二种Handler 和 Runnable 匿名内部类。看下面这段代码// 危险写法 Handler handler new Handler() { Override public void handleMessage(Message msg) { // 更新 UI } };这段代码里内部的 Handler 对象隐式持有外部 Activity 的引用。如果消息延迟执行而 Activity 已经销毁Activity 就泄漏了。解决方案是使用静态内部类加弱引用或者在onDestroy里removeCallbacksAndMessages(null)。第三种注册了监听器但没反注册。比如系统服务、广播接收器、观察者模式注册了就要在对应生命周期里注销。这个是最冤的泄漏代码明明写了 register却忘了 unregister排查时还不容易发现。第四种流和游标没关闭。虽然大多数流内部有 finalize 兜底但等到 GC 处理时可能已经拖垮了内存和文件句柄。记得所有 IO 在 finally 里关闭或者直接用 try-with-resources。这里给出一个简单的自查清单检查单例类里有没有 Context 或 View 字段。检查内部类前有没有加 static。检查页面销毁时是否移除了回调。检查长时间运行的线程里是否持有了外层引用。3.3 引用类型选不好等于给自己埋雷Java 有四种引用类型强引用、软引用、弱引用、虚引用。Android 开发中最常用到的是前三种。我认为最实用的原则是强引用默认选择绝大多数场景都用它。软引用适合“有比没有好但内存紧张时可以丢掉”的缓存数据。比如图片压缩后的缩略图。弱引用适合“生命周期不在自己控制范围内”的关联对象。比如 Handler 持有 Activity用弱引用可以避免阻碍 GC 回收。但我要提醒一句软引用和弱引用不是万能的。如果为了省内存到处用弱引用反而会带来空指针风险——对象可能在你还没用完时就被回收了。更好的思路是该持有的对象用强引用不需要时主动置空、移除监听、退出线程。我自己处理图片缓存时经历过从 LruCache 到软引用再到 LruCache 的反复。最后结论是LruCache 这种“有上限的强引用缓存”比软引用更可控。因为软引用在内存紧张时会被立刻回收导致缓存命中率极低图片频繁重新解码反而更卡。4. 实操演示把 Session 和内存检测串成一个模块4.1 设计一套可持久化的会话数据结构理论说再多都不如直接上代码。这一节我演示一个最小可用的会话模块设计。先定义会话数据模型public class SessionData { public String userId; public String displayName; public String accessToken; public long accessTokenExpireTime; public String refreshToken; public long lastLoginTime; }这个模型对应的是会话状态层。需要注意accessToken 是访问令牌过期时间必须一起保存否则恢复时根本不知道要不要刷新。refreshToken 是刷新令牌真正落盘时要加密。4.2 缓存与序列化的落地策略会话数据要同时维护内存副本和磁盘副本。内存副本用单例保证整个进程内读写一致public class SessionManager { private static volatile SessionManager instance; private SessionData currentSession; private SessionManager() {} public static SessionManager get() { if (instance null) { synchronized (SessionManager.class) { if (instance null) { instance new SessionManager(); } } } return instance; } public synchronized void updateSession(SessionData data) { this.currentSession data; persistSession(data); } public synchronized SessionData getSession() { return currentSession; } private void persistSession(SessionData data) { // 加密后写入本地具体见下方说明 } }磁盘序列化我推荐用 DataStore JSON。相比 SharedPreferencesDataStore 基于协程和 Flow支持异步读取不会阻塞主线程。下面是一个简化的写入流程// 使用 DataStore 保存会话 JSON context.dataStore.edit { settings - val json gson.toJson(sessionData) settings[SESSION_KEY] json }读取时先判断内存缓存是否有值如果有直接返回没有再从 DataStore 异步读取反序列化后回填内存缓存。注意整个模块要处理“正在刷新令牌”的并发情况。用户快速切换页面时可能同时触发多次令牌刷新需要在刷新期间加锁或者用单飞模式避免重复请求。序列化方面有一个细节容易踩坑如果用 Gson 或 Moshi对象里的时间字段建议存 long 而不是 String。否则解析时碰到时区问题过期时间会算错导致用户明明登录着却被判定为未登录。4.3 内存分析用 Profiler 打一场硬仗写完 Session 模块下一步是验证它没有内存问题。我用 Android Studio 自带的 Memory Profiler 比较多流程很固定打开 Profiler选择 Memory 面板。手动触发各种场景登录、切后台、杀进程恢复、反复横跳页面。抓取 Java Heap然后分析 Heap Dump。按 Retained Size 排序找到大头对象。对可疑对象右键查看引用链。在分析 Heap Dump 时我重点会看三处Activity 实例数量有没有异常堆积。正常情况下销毁后的 Activity 不应该出现在 Heap 里如果连续进入退出同一个页面三次Heap 里躺着三个相同 Activity那就肯定泄漏了。大对象数量。比如 Bitmap 底层像素数据占用的字节数组如果数量持续增长不回落的要警惕图片缓存失控。Session 单例。确认 currentSession 只被预期的地方引用没有被其他长生命周期对象额外持有。我还习惯在做内存分析时把设备“开发者选项”里的“不保留活动”打开模拟最极端的后台回收场景。这时候再测 Session 恢复才能暴露真实问题。5. 排查经验多进程、低内存、后台清理这些硬仗怎么打5.1 多进程共享会话问题比想象的复杂现在很多 App 为了隔离风险会把推送、播放、或某个重量级业务拆到独立进程。进程一多Session 共享就成了大麻烦。内存单例只在当前进程有效。你在主进程写了一个 token推送进程读不到。有人会立刻想到用 SharedPreferences 共享但 SharedPreferences 本身不保证多进程同步跨进程读写会出现脏读。我给出两种可靠方案方案一把会话数据放到 ContentProvider 里通过 Provider 做跨进程读写。这是系统级支持的进程间通信方案。方案二用文件锁 序列化文件自己维护一致性。实现简单但容错要慎重。更省心的做法是只在主进程维护会话状态其他进程需要身份时通过 AIDL 或者绑定服务向主进程查询。这样会话只在一个进程内写入和更新不会出现并发写冲突。5.2 低内存驱逐时的数据保全Android 系统会在内存不足时发送onTrimMemory回调这是系统给你最后的提示音。收到这个回调说明系统已经在回收缓存进程了下一秒可能就是你的进程。在这个回调里别做耗时操作但可以快速把关键状态同步落盘。比如Override public void onTrimMemory(int level) { if (level TRIM_MEMORY_RUNNING_CRITICAL) { // 保存当前正在编辑的表单草稿 // 把会话的访问令牌刷新情况落盘 } }注意这里说的落盘是“快速写入”不要做数据库事务、不要做网络请求。最好的形态是几行代码把内存中的关键字段写进 DataStore 或者文件。因为系统随时可能杀掉进程你只有极短的时间窗口。5.3 实战问题记录与速查表最后放一张我这几年遇到的高频问题速查表每一条都是真金白银的教训现象直接原因解决办法杀进程后登录态丢失访问令牌和刷新令牌都只存内存刷新令牌加密落盘启动时静默刷新内存持续上涨但 GC 后下降缓存对象堆积用有上限的 LruCache设置合理的 maxSize页面关闭后 Activity 仍存活静态变量持有引用代码审查 LeakCanary 持续检测多进程间登录态不同步单例只存在于单一进程ContentProvider 或主进程查询恢复页面时闪一下白屏数据在恢复后才加载先读缓存渲染再异步刷新我个人在实际操作中的体会是Session 和 Memory 这两个话题是 Android 开发里少数几个“越早想明白越受益”的方向。它们不像新框架那样有新鲜感也不像炫酷动画那样有即时正反馈但几乎所有线上问题、崩溃、卡顿最后都能追溯到这两块没有做好。如果你正在准备跨过这一境我建议不要急着去看各种性能优化的偏方先把自己项目里的登录流程完整走一遍杀掉进程再打开看会话能不能无缝恢复再用 Profiler 打一遍自己的页面看有没有躺着不走的 Activity。能答住这两个问题你就已经具备斗王的资格了。