第25章:性能优化与架构设计
架构设计这事儿,说白了就是给代码定规矩。规矩定得好,性能自然差不了。我在项目里见过太多“架构很漂亮但跑起来卡成PPT”的例子,也见过“代码乱成一锅粥但性能意外不错”的野路子。今天咱们就聊聊,MVP、MVVM、MVI这些架构模式,到底对性能有什么影响。
25.1 MVP/MVVM/MVI架构对性能的影响
先说说我个人的习惯。我评估一个架构的性能表现,主要看三点:对象创建频率、内存泄漏风险、以及UI更新的开销。你想想看,一个架构如果动不动就new一堆对象,或者让Activity泄漏得跟筛子似的,那性能肯定好不了。
25.1.1 MVP:最朴实的架构
MVP其实挺实在的。Presenter持有View的引用,View通过接口回调通知Presenter。逻辑清晰,但有个坑——Presenter的生命周期管理。
我曾经在一个电商项目里,因为忘记在onDestroy中释放Presenter,导致Activity泄漏。那会儿用户反馈说“逛着逛着就卡了”,一查内存快照,好家伙,十几个Activity实例挂在那。
MVP的性能开销主要来自:
- 每个页面至少多一个Presenter对象
- 接口回调的匿名内部类容易持有外部引用
- View和Presenter之间的双向引用需要手动管理
// 典型的MVP写法,注意泄漏风险
public class MainPresenter {
private WeakReference<MainView> viewRef;
public void attachView(MainView view) {
this.viewRef = new WeakReference<>(view);
}
public void detachView() {
if (viewRef != null) {
viewRef.clear();
viewRef = null;
}
}
}
嗯,这里要注意。用WeakReference确实能防泄漏,但别忘了,GC回收是有延迟的。如果频繁attach/detach,WeakReference的创建本身也有开销。
25.1.2 MVVM:数据驱动的优雅方案
MVVM用LiveData或StateFlow做数据驱动,View层只管观察,不用管数据怎么来的。这比MVP清爽多了。
但性能问题出在哪?观察者的数量。我在一个社交App里见过一个ViewModel绑了30多个LiveData,每个LiveData又绑了3-4个观察者。你想想看,一次数据变化,触发上百次回调,UI线程能不卡吗?
我建议:一个ViewModel的LiveData数量控制在10个以内。如果超过,考虑拆分ViewModel或者用合并数据源的方式。
// 合并多个数据源,减少观察者数量
public class UserViewModel extends ViewModel {
private final MediatorLiveData<UserState> userState = new MediatorLiveData<>();
public UserViewModel() {
userState.addSource(nameLiveData, name -> combine());
userState.addSource(avatarLiveData, url -> combine());
userState.addSource(levelLiveData, level -> combine());
}
private void combine() {
// 合并成一个状态对象,UI只观察这一个
UserState state = new UserState(
nameLiveData.getValue(),
avatarLiveData.getValue(),
levelLiveData.getValue()
);
userState.setValue(state);
}
}
25.1.3 MVI:单向数据流的极致
MVI是我最近比较喜欢的模式。它的核心是单向数据流:用户操作产生Intent,Reducer处理Intent生成新State,UI根据State渲染。说白了,就是状态机。
性能优势很明显:
- State是不可变的,避免了数据竞争
- UI渲染逻辑是纯函数,容易做diff优化
- 每个页面只有一个State对象,内存开销可控
但MVI也有代价。每次状态变化都要创建新对象。如果State对象很大(比如包含列表数据),频繁创建会导致GC压力。我在一个新闻客户端里试过MVI,列表滚动时每秒创建几十个State对象,GC次数直接翻倍。
避坑指南:MVI的State设计要遵循“最小化原则”。只包含UI需要的数据,不要一股脑全塞进去。列表数据用DiffUtil做局部更新,别每次都重建整个列表。
25.2 组件化与模块化中的性能考量
组件化这事儿,说白了就是“分而治之”。但分不好,性能反而会下降。
25.2.1 模块间的通信开销
模块化之后,模块之间怎么通信?最常见的方式是接口+路由。但每次跨模块调用,都意味着一次反射或动态代理。我在一个20+模块的项目里测过,一次路由跳转平均耗时3-5ms。如果页面跳转频繁,这个开销不可忽视。
| 通信方式 | 单次调用耗时 | 内存开销 | 适用场景 |
|---|---|---|---|
| 直接依赖 | <0.1ms | 低 | 同模块内 |
| 接口+路由 | 1-5ms | 中 | 跨模块调用 |
| 事件总线 | 0.5-2ms | 高(需注册/注销) | 一对多通知 |
| ContentProvider | 5-20ms | 高 | 跨进程通信 |
我个人习惯是:能直接依赖就别用路由。同模块内的调用,直接写接口就行。只有跨模块且需要解耦的场景,才用路由。
25.2.2 模块的懒加载
模块化最怕什么?启动时加载了所有模块。我见过一个项目,启动时初始化了15个模块,每个模块都要读SP、建数据库、拉配置。结果冷启动时间直奔5秒。
解决方案很简单:按需加载。只有用户真正用到某个功能时,才初始化对应的模块。
// 懒加载模块的典型实现
public class ModuleManager {
private final Map<String, Lazy<BaseModule>> moduleCache = new HashMap<>();
public <T extends BaseModule> T getModule(String name, Class<T> clazz) {
Lazy<BaseModule> lazy = moduleCache.get(name);
if (lazy == null) {
lazy = new Lazy<>(() -> createModule(clazz));
moduleCache.put(name, lazy);
}
return (T) lazy.get();
}
}
注意:懒加载虽然好,但别滥用。如果用户频繁切换模块,每次都要重新初始化,反而更慢。我建议对“高频使用”的模块做预加载,对“低频使用”的模块做懒加载。
25.3 依赖注入(Hilt)的内存开销
Hilt现在几乎是Android开发的标配了。但好用归好用,内存开销不能忽视。
25.3.1 Hilt的组件树
Hilt会为每个组件(Activity、Fragment、ViewModel等)生成对应的组件实例。每个组件实例都持有自己的对象图。如果一个App有50个Activity,每个Activity平均注入10个对象,那光组件实例就有500个。
我测过一个中型项目,Hilt在运行时占用的额外内存大约在5-10MB。对于旗舰机来说不算什么,但如果是低端机(比如2GB内存),这个开销就有点肉疼了。
25.3.2 单例 vs 非单例
Hilt里最常用的注解就是@Singleton。但很多人不知道,单例的生命周期和组件绑定。如果你在@ActivityScoped里标记了@Singleton,那这个对象其实只在Activity范围内是单例,换个Activity又得重新创建。
// 注意作用域
@Module
@InstallIn(SingletonComponent.class)
public class AppModule {
@Provides
@Singleton // 全局单例,整个App只有一个实例
public ApiService provideApiService() {
return new ApiService();
}
@Provides
@ActivityScoped // Activity级别单例,每个Activity一个实例
public UserManager provideUserManager() {
return new UserManager();
}
}
我建议:把“全局通用”的对象(如网络库、数据库)放在SingletonComponent里。把“页面相关”的对象(如ViewModel、Adapter)放在ActivityComponent或FragmentComponent里。别一股脑全用@Singleton。
25.3.3 Hilt的编译期开销
Hilt在编译期会生成大量代码。我算过,一个中等规模的项目,Hilt生成的类文件大约有200-500个。这些类文件虽然不会影响运行时性能,但会拖慢编译速度。
怎么优化?
- 减少不必要的注入点
- 用
@Binds代替@Provides(减少对象创建) - 避免在
@Module里写复杂的初始化逻辑
25.4 知识体系总览
说了这么多,咱们用一张图来总结一下架构设计与性能优化的关系。
这张图把咱们今天聊的内容串起来了。架构模式、组件化、依赖注入,这三者不是孤立的。你选MVP还是MVI,决定了你的对象创建频率;你做不做懒加载,决定了启动性能;你用不用Hilt,决定了内存开销。说白了,架构设计本身就是性能优化的一部分。
最后说一句:没有完美的架构,只有适合的架构。别为了用MVI而用MVI,也别因为Hilt有开销就放弃依赖注入。性能优化的本质是权衡——在代码可维护性和运行时性能之间找到平衡点。
公众号:蓝海资料掘金营,微信deep3321