资讯动态

Android高级进阶:架构设计、性能优化与源码实战指南

发布时间:2026/10/19 11:41:08 来源:尧图企业网站定制
你上一次因为一个线上卡顿被用户吐槽或者因为某段代码在三个月之后连自己都看不懂是什么时候做Android做到高级工程师这个阶段我越来越觉得真正的分水岭不在于你会不会用某个新框架而在于三件事架构上能不能想清楚性能上能不能做极致技术上有没有自己的沉淀。这篇分享我就围绕这三点展开把我在实际项目中踩过的坑、用过的方案、最终沉淀下来的方法都摊开讲希望对正在往高级方向走的朋友有参考价值。1. 架构之思先想清楚再动手1.1 从一次模块重构说起选型不是追新两年前我接手了一个某社交App的消息列表模块代码已经迭代了三年多。当时的现状是同一个页面有Activity、Fragment、自定义View三种写法数据请求有的放在Presenter里有的直接写在Adapter中还有一部分通过EventBus跨页面通信。每次需求变更改动至少要牵涉五六个文件而且经常出现改一处、崩两处的局面。后来我们决定做一次架构梳理。当时团队里对方案有争议一部分人主张直接上MVI觉得单向数据流能彻底解决状态混乱另一部分人认为先稳定用MVVM Repository不要一次性变动太大。讨论了很久最后选了MVVM Repository 轻量单向数据流原因很简单团队大部分人只熟悉MVP直接上MVI的学习成本和重构风险都太高。MVI的不可变状态和Intent化确实很好但把现有的网络、数据库、缓存逻辑全部改造成单向数据流绝不是一次小迭代能完成的。架构选型最忌讳的就是追新。你要先搞清楚团队的水平、业务的复杂度、交付时间的压力这三者决定了你能走多远。我们当时做的第一步不是写代码而是把所有页面和请求画在一张白板上标注出哪些数据会被多个页面共用哪些状态是易变的。画完之后发现真正的问题不是缺一个好的架构模式而是缺少统一的数据入口同样的用户信息每个页面都自己拉了一遍接口。于是我们把数据获取全部收口到Repository层页面只管拿到LiveData/Flow后刷新UI至于数据来自缓存还是网络页面根本不关心。这次重构最大的收获是我明白了一个道理架构不是画一堆漂亮的依赖关系图而是要给团队一条清晰的、不容易走错的路。你设计的分层越简单执行的人就越不会跑偏。我们后来把“页面只跟ViewModel通信、ViewModel只跟Repository通信、Repository管理数据来源”写成了团队规范并且靠着代码评审和Gradle依赖检查来保证这条规则不被破坏而不是靠自觉。1.2 模块化拆分的落地红线架构分层的下一步就是模块化。很多团队一提到模块化就想着把每个页面拆成独立Module然后做路由、做组件化最后发现编译速度没上去代码却更难维护了。我个人的经验是模块化拆分必须带着明确目的拆而不是为了拆而拆。你要先回答一个问题拆开之后谁受益如果答案是“某个业务团队可以独立交付”或者“核心基础库不需要被上层业务反复改动”那才有拆分的价值。我们当时的做法是先把Module依赖方向理顺。举个例子CommonLibrary模块绝对不能被BusinessA模块反向依赖如果出现这种情况说明你把太多通用逻辑塞进了业务模块里。这里我推荐一个非常实用的检查手段在根项目的Gradle文件里做依赖方向的静态检查。我们当时用了一条简单的规则如果发现BusinessA的build.gradle里出现了对某个不在白名单内的Module的依赖构建直接失败。这种方式比在代码评审时靠肉眼检查可靠得多。还有一点要注意就是Module之间的通信不要滥用路由。组件化里经常用ARouter或自研路由来解决跨模块跳转但路由是为了让模块之间不直接依赖而不是让所有数据传递都走路由。我们踩过一次坑为了图省事把两个模块之间的数据回调也通过路由传递结果导致问题很难追踪最后不得不重构。正确的做法是跨模块页面跳转用路由跨模块数据交互尽量通过接口下沉到公共层或者用模块间约定好的数据仓库。模块化拆分之后编译速度不一定立刻提升因为很多模块会被强制重新编译。真正让编译变快的是减少核心模块的变动频率和依赖体积。我们后来把公共UI组件、网络库、图片库这类基础能力沉淀到两个Module里上层业务模块各自独立编译效果才慢慢体现出来。架构调整从来不是一次性的动作它需要你持续观察依赖变化周期性清理那些越来越胖的模块。1.3 架构评审时我们到底在评什么我们团队后来养成了一个习惯每次技术方案评审不聊具体代码只聊四件事——数据流向、状态归属、异常处理、可测试性。很多人做架构评审时喜欢抠细节比如某个方法该不该加final某个类是不是应该用sealed这其实偏离了重点。数据流向决定了你将来排查问题要翻几个文件状态归属决定了页面刷新会不会乱异常处理决定了网络差的时候App是闪退还是降级可测试性决定了你以后敢不敢改这个模块。我印象很深的一次评审是针对订单详情页的重构方案。业务同学提了一个看似简单的需求在订单状态变化时页面要同时更新按钮区、商品列表和优惠信息三块内容。最初的方案是在Activity里写三个回调方法分别刷新三块UI。我们在评审时只问了一个问题如果在这三块UI刷新之间发生一次状态异常数据从哪来问题一提出来大家就知道现有的方案根本兜不住。最后我们把订单状态抽成了单一的StateFlow三块UI都只对这个State做响应不管状态怎么变页面的数据永远是一致的。这个方案从代码量上看并没有简化多少但心智模型简单了每个人都能想清楚这个页面只有一个状态来源。所以说架构评审最重要的产出不是完美的分层图而是让团队对“这个模块到底应该长成什么样”达成共识。如果评审完每个人心里的实现方式都不一样这份设计文档就是失败的。架构之思本质上是一种取舍的智慧你要时刻清楚自己在用复杂度换什么。2. 性能之巅从指标到优化的完整闭环2.1 启动耗时第一个被感知的性能卡点启动优化是性能优化里性价比最高的一环。用户打开App的前三秒决定了他会不会留下来。做启动优化之前你要先建立一套可量化的指标采集方案否则你连自己是快是慢都不知道。我们的做法分两步线上用自研的埋点统计冷启动时长从Application的attachBaseContext开始计时到首页首帧绘制完成结束线下用adb命令手动验证。adb shell am start -W 包名/启动Activity这个命令会输出TotalTime和WaitTime用来做单次验证很方便。但注意线上的真实统计必须从进程创建开始因为很多第三方SDK的初始化在Application的onCreate里只统计Activity启动时间是看不出来的。拿到数据之后重点排查三个入口Application.onCreate里的耗时操作、首帧之前的主线程任务、以及启动页的布局渲染。我们当时发现一个很典型的案例某个第三方统计SDK的初始化方法内部竟然做了网络请求直接拖慢了启动速度一百多毫秒。排查过程不复杂用TraceCompat包住Application的onCreate把每个方法调用耗时长按排序问题就浮出水面了。后来我们把所有SDK初始化分成了三类必须在主线程同步初始化的、可以异步初始化的、真正用的时候再懒加载的。这一轮优化下来冷启动时间从2.4秒降到了1.3秒效果立竿见影。启动优化还有一个容易忽略的点ContentProvider的初始化。很多第三方SDK通过ContentProvider自动初始化这会导致Application还没跑起来你的进程就得先加载一堆Provider。这种隐式初始化的耗时常被忽略。我们的做法是排查AndroidManifest里所有Provider能改成手动初始化的尽量改实在改不了的至少分析清楚它们各自的耗时。2.2 卡顿定位用工具找出主线程到底在忙什么卡顿优化的难点不是优化而是定位。用户感知到的卡顿往往是偶发的你自己滑半天却复现不出来。我现在的习惯是收到卡顿反馈后先区分类型是列表滑动掉帧还是页面切换卡顿还是动画过程中CPU突然飙高。不同的卡顿类型定位工具完全不同。列表滑动掉帧我一般优先看RecyclerView的onBindViewHolder耗时和嵌套布局复杂度如果掉帧是偶发性的我会怀疑是不是主线程被某个IO操作堵住了。这里安利一个工具链Perfetto CPU Profiler。Perfetto适合抓系统级的长时程trace能看到主线程在整个时间片内到底跑了哪些任务CPU Profiler适合看方法级别的CPU占用配合火焰图能很快定位到耗CPU的方法。有一次线上反馈首页轮播图切换时偶尔卡顿我们抓了好几次trace都没复现。后来通过系统CPU Profiler抓了很久的trace才发现是某个图片加载库在缓存未命中时图片解码直接放到了主线程而当时正好是弱网环境图片解析耗时暴涨。问题根因找到了解决办法也简单把图片解码放到子线程加载库的配置里设置正确的线程调度即可。还有一个实战技巧使用BlockCanary这类工具做长时监控。它通过在主线程Looper的dispatchMessage前后打点监控单次消息处理时长。我建议先设一个阈值比如单次消息执行超过300ms就记录标志收到卡顿反馈后再去匹配当时的堆栈。这种方式比用户反馈“有点卡”精准太多因为你能定位到具体是哪个方法、哪一行代码卡住主线程。2.3 内存与布局那些容易漏掉的隐形杀手内存优化是性能优化里最需要耐心的部分。直接看App的Memory Profiler往往只能看到总内存趋势真正的问题还是要靠工具和代码审查结合。我们曾经排查过一次内存泄漏LeakCanary直接指向了一个静态的Handler持有Activity的引用这个Handler在一个Fragment里创建因为要接收全局网络状态变化被写成了静态实例结果Activity销毁后依然被持有。代码审查时这种问题很容易漏掉因为Handler的创建和使用看不出任何异常。后来我们加了规则所有静态字段引用Activity、Fragment、View的都必须在代码评审中特别说明否则不允许合并。布局优化方面我强烈建议在开发阶段就开启系统的布局嵌套检查。工具里有一个“Layout Inspector”和“Hierarchy Viewer”的替代方案可以直接看到View树的深度。我见过一个极端的例子一个普通的列表Item因为多层嵌套的RelativeLayout和LinearLayoutView树深度达到了11层每一层的measure和layout都要遍历子View列表能不卡吗后来一次性改成ConstraintLayout单层布局滑动帧率立马上来了。还要提醒一点RecyclerView的Item复用。很多人在itemView里做了findViewById却没有注意ViewHolder的复用或者写了复杂的动画和Drawable导致频繁创建对象。内存抖动的根源往往是频繁分配小对象比如在onBind里创建了新的Paint、新数组一旦滑动起来GC压力巨大。通过Memory Profiler里的“Allocations”窗口可以看到对象的分配频率这类问题通常一眼就能找到。解决思路就一句话热路径上减少对象分配能复用的复用能用primitive的别装箱。3. 技术之魂深度与广度的平衡术3.1 怎么读源码才算有效输入很多人问我现在框架更新这么快源码到底该不该读怎么读我的回答是源码当然要读但读的目的是为了建立解决问题的思路而不是把每一行都背下来。带着问题去读效率比从头到尾通读高得多。我在研究启动优化的时候就专门读了一遍开源网络请求库的初始化逻辑想搞清楚为什么它初始化时会在主线程耗时。读完后我发现它的Dispatcher创建时启用了线程池某些内置拦截器的构造方法里做了很多准备工作这些逻辑如果能在业务侧避开启动速度就能快不少。读源码我推荐三步走。第一步先在纸上画一条主链路比如网络请求从接口调用到响应回调经过了哪些类和回调第二步找到关键扩展点比如拦截器就是网络库的核心扩展点理解它的责任链机制第三步尝试自己写一个小Demo去替换或者模拟一个内部类看看行为会不会变化。这三步走下来你对于这个框架的设计思考就会特别深。平时我会给自己立一个规矩每个月深入研究一个开源库或者系统源码的某一块输出一篇笔记。研究过的内容不一定马上用到业务里但它会在某个关键时刻成为你的灵感库。我记得有次做一个内存缓存优化正愁没有合适的淘汰算法就想起了之前研究过的LruCache源码里面关于线程安全、命中率统计的细节直接给了我启发。技术深度就是这样你今天读的每一行源码都可能成为明天的解决方案。3.2 把踩过的坑变成团队资产技术之魂并不只是个人研究更在于如何输出价值。我发现很多团队的经验沉淀都靠口口相传同样的坑被不同的人反复踩。后来我在团队里推动了一个“故障复盘文档”的机制每解决一个线上问题必须提交一次复盘包含现象、排查过程、根因、解决方案、预防措施五个部分。重点是写清楚排查过程而不是只写最终答案。最典型的一个案例是线上某功能偶发空指针崩溃崩溃堆栈指向一个自定义View的onDraw方法。单看堆栈很难看出问题后来复现到稳定场景发现是页面切到后台再回来时View的尺寸发生了变化而测量逻辑里用了默认值导致绘制时访问了一个尚未初始化的对象。我们在复盘文档里把这类问题归纳为“生命周期和View状态不同步”的常见陷阱并且在代码规范里加了一条自定义View的onMeasure和onDraw里所有涉及状态判断的字段必须有默认值保护。这个改动小但从此同类问题再也没发生过。技术沉淀最容易犯的毛病是只记结论不记过程。结论是“不要在onDraw里做复杂计算”但过程才是财富是怎么通过掉帧监控找到onDraw的是怎么排除其他嫌疑的这些思考路径对于后来者才是真正有用的。所以我在团队评审复盘文档时最看重的是“如果这个人重新碰到这个问题只看这份文档能不能在半小时内定位到根因”。3.3 自研与引入第三方库的技术决策模型技术深度到了一定程度你一定会面临一个问题某个功能是自研还是引第三方库我的决策模型很简单算总账。这个账目包括功能匹配度、维护成本、学习成本、兼容风险、团队掌控力五项每一项打一个1-5分最后加权比较。比如图片加载功能如果团队已经有很成熟的图片解码经验自研是一个选项但如果只是需要一个稳定的图片列表加载方案引入成熟库的成本远低于自研这时候不要因为“别人都在自研”就冲动。但引入第三方库也不是一劳永逸的。我遇到过的坑是某个开源网络库的版本更新后内部线程池的配置变了导致线上出现线程数暴增。排查到最后发现是我们对库内部的线程池行为理解不够依赖了它的一些隐式实现。从那以后我们在引入任何第三方库之前会强制做一次源码级调研重点关注它的线程模型、异常处理方式、生命周期管理。这层功课虽然费时间但能避免很多线上事故。技术决策最怕的其实不是选错而是不做决定。团队里经常有方案争论A说这个库好B说那个库好一讨论就是三五天。我的做法是会前先约定好评分维度会上直接给五项打分用数据和成本说话。一旦决策下来就坚决执行不允许中途偷偷换方案。4. 实战复盘线上问题排查的完整思路4.1 崩溃问题从堆栈到根因的推理链有一次线上崩溃率突然上升集中在某款机型的Android 12系统上崩溃堆栈指向了一个列表组件的onScroll事件。堆栈信息非常简短只是一个标准的空指针异常看不出是哪行业务代码导致的。我们下载了崩溃堆栈对应版本的反混淆映射文件先解析出行号又紧急联系到部分反馈用户拿到他们当时的操作路径发现都是列表快速滑动时崩溃。逐步推理下来问题指向了一个变量正在加载数据的列表项被回收之后异步回调尝试更新它绑定的位置信息而此时绑定的对象已经为空。这类问题的通用排查思路我总结为三步。第一步解析崩溃堆栈锁定线程和具体类第二步结合版本差异和操作路径缩小范围第三步找出异步回调与生命周期错配的地方。很多崩溃其实不是偶发的它们都隐藏着线程或者生命周期管理的缺陷。后来我们把这个列表组件的数据加载逻辑改成“先判断目标是否有效再执行更新”的模式彻底解决了问题。排查崩溃问题时急躁是最要不得的你要像一个侦探一样从一句堆栈里挖出完整的因果链。4.2 卡顿问题火焰图与onCreate顶层的较量另一个让我印象深刻的案例是某次版本更新后首页滑动出现明显掉帧。用Perfetto抓trace后发现主线程在列表滑动过程中持续存在一个耗时的测量操作但火焰图显示的方法签名却是Android系统内部的Layout。没有业务代码的痕脚印说明是布局本身的复杂度问题。我们进一步对比新旧版本布局文件发现新版在页面顶部多了一个渐变阴影效果实现方式是给根布局添加了一个自定义的Drawable而这个Drawable重写了draw方法里面的渐变创建使用了贝塞尔曲线每帧都会重新计算路径。优化方案并不复杂把多帧固定不变的路径计算结果缓存起来只在布局尺寸变化时重新计算。修改后掉帧现象消失了。这个案例给我最大的触动是很多性能问题不是业务逻辑写错了而是你对渲染链路里的每一个阶段理解不够深。你要知道一帧画面从View树的measure、layout到draw再到硬件加速的合成每一步都可能是瓶颈。遇到卡顿不要急着怀疑业务代码先站在系统渲染的角度想一遍整体过程。4.3 常见问题速查表我把这些年排查过的高频问题整理成一个速查表每次做性能优化和问题排查前都会先过一遍这个清单能节省大量时间。下面这张表可以直接抄作业现象常见根因推荐排查方向冷启动慢Application初始化耗时、ContentProvider初始化TraceCompat打点定位耗时方法梳理初始化分类列表滑动卡顿onBindViewHolder复杂计算、布局嵌套过深、频繁对象分配Layout Inspector看View树Memory Profiler查分配量偶发掉帧主线程IO操作、图片解码在主线程、动画频繁重建对象Perfetto抓trace、CPU Profiler看火焰图内存泄漏静态引用持有Activity、回调未注销、Handler持有外部类LeakCanary定位、代码评审增加静态引用审查网络慢导致白屏数据未缓存、加载态缺失、DNS解析慢检查网络层的缓存策略、异步请求失败回调崩溃堆栈看不出原因混淆后无法还原、缺少机型/系统信息、异步线程崩溃开启反混淆映射、采集更多崩溃上下文、Review线程切换排查时我还有一个习惯改动一行代码之前先想清楚这行代码会影响哪些状态、哪些线程。很多线上问题都是“本地没问题上线必炸”根源就在于没有充分评估改动的影响面。你可以在脑海里把整条链路过一遍调用方是谁被调用方需要什么前置条件中间有没有并发风险。这几秒钟的思考往往能避免后续好几个小时的线上排查。5. 写在最后的经验之谈说句实在话从普通开发到高级工程师我没有遇到过任何一次“顿悟时刻”。架构能力、性能优化功底、技术深度都是一年年、一个个坑喂出来的。我自己的体会是你可以没有太高的天分但一定要有愿意较真的习惯一个崩溃堆栈要追到根因一次卡顿要看到系统层的trace一份方案要主动去问“还有没有更好的做法”。这种较真会在漫长的职业生涯里慢慢变成你跟别人拉开差距的那道护城河。我很庆幸自己在早期就养成了一个习惯把所有解决过的问题都写下来不追求文笔只追求可复现。今天分享的这部分内容有很多就是当年那些记录拼接出来的。架构、性能、技术深度这三点会一直随着技术发展变化但沉淀方法论这件事永远不会过时。如果这篇文章里有一两个思路能帮到你少走弯路我就满足了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑