第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 / MVVM / MVI 组件化 模块通信 / 懒加载 依赖注入 Hilt / 组件树 核心性能问题 对象创建频率 Presenter/State对象 内存泄漏 View引用/观察者 UI更新开销 回调/状态变化 优化策略:最小化对象创建 → 严格生命周期管理 → 合并数据源 → 按需加载

这张图把咱们今天聊的内容串起来了。架构模式、组件化、依赖注入,这三者不是孤立的。你选MVP还是MVI,决定了你的对象创建频率;你做不做懒加载,决定了启动性能;你用不用Hilt,决定了内存开销。说白了,架构设计本身就是性能优化的一部分

最后说一句:没有完美的架构,只有适合的架构。别为了用MVI而用MVI,也别因为Hilt有开销就放弃依赖注入。性能优化的本质是权衡——在代码可维护性和运行时性能之间找到平衡点。


公众号:蓝海资料掘金营,微信deep3321