WPF Prism代码走读

Prism 8.1.97 模块化(Modularity)源码走读

源码版本:Prism 8.1.97


1. 先回答核心问题:它靠反射实现吗

是反射,但不是"扫描程序集自动发现模块"那种反射。

准确的说法是:元数据驱动 + 延迟反射 + 容器实例化。三层职责分离:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│ 第 1 层:登记(几乎无反射)                                       │
│   ConfigureModuleCatalog → catalog.AddModule<T>()                │
│   typeof(T).AssemblyQualifiedName 存成【字符串】                  │
│   → 此时程序集甚至可能还没被加载进内存                             │
├─────────────────────────────────────────────────────────────────┤
│ 第 2 层:加载(反射出现)                                         │
│   Type.GetType(moduleTypeString)   // 字符串 → Type              │
│   Assembly.LoadFrom(dll 路径)       // 外部 DLL 模块才会走        │
├─────────────────────────────────────────────────────────────────┤
│ 第 3 层:实例化(反射的最后一环,交给 DI 容器)                    │
│   container.Resolve(moduleType)     // 容器内部反射/表达式树构造  │
│   → module.RegisterTypes() / module.OnInitialized()             │
└─────────────────────────────────────────────────────────────────┘

对比一下"纯反射扫描"方案(比如 MEF、或者某些自研框架):它们会在启动时遍历 AppDomain.CurrentDomain.GetAssemblies(),对每个类型检查是否实现某接口、是否带某特性,然后缓存成注册表。

Prism 没有这么做,原因在第 13 章分析。


2. 设计总览

2.1 六个角色

角色 接口 实现 一句话职责
模块契约 IModule 业务侧自己写 暴露 RegisterTypes / OnInitialized 两个钩子
模块元数据 IModuleInfo ModuleInfo 存名字、类型全名、依赖、DLL 路径、状态
模块清单 IModuleCatalog ModuleCatalog 及 3 个子类 收集元数据 + 校验(重复/循环/缺失)
调度器 IModuleManager ModuleManager 决定"什么时候、按什么顺序"加载谁
类型加载器 IModuleTypeLoader FileModuleTypeLoader 把 DLL 弄进 AppDomain
初始化器 IModuleInitializer ModuleInitializer 类型 → 实例 → 调两个钩子

依赖方向是单向的:

复制代码
PrismApplication ──► IModuleManager ──► IModuleCatalog(读)
                           │
                           ├──► IModuleTypeLoader(必要时加载 DLL)
                           └──► IModuleInitializer ──► IContainerExtension

ModuleManager 不直接 new 任何模块,ModuleInitializer 不直接 new,一切走容器 ------ 这是 Prism 能替换容器的前提。

2.2 文件地图

契约与算法层(与 UI 框架无关,Prism.Core)

文件 内容
src/Prism.Core/Modularity/IModule.cs 模块契约
src/Prism.Core/Modularity/IModuleInfo.cs 元数据契约
src/Prism.Core/Modularity/ModuleState.cs 状态机(5 个状态)
src/Prism.Core/Modularity/IModuleCatalog.cs 清单契约
src/Prism.Core/Modularity/ModuleCatalogBase.cs 清单基类:校验 + 依赖展开 + 排序(核心算法都在这里)
src/Prism.Core/Modularity/ModuleDependencySolver.cs 拓扑排序
src/Prism.Core/Modularity/IModuleManager.cs 调度器契约
src/Prism.Core/Modularity/IModuleInitializer.cs 初始化器契约
src/Prism.Core/Modularity/IModuleManagerExtensions.cs ModuleExists<T> / IsModuleInitialized<T> / LoadModule<T>
src/Prism.Core/Modularity/IModuleCatalogCoreExtensions.cs Exists<T> / GetModuleState<T> / IsInitialized<T>
src/Prism.Core/Modularity/ModuleDependencyAttribute.cs [ModuleDependency("A")]

WPF 平台层(Prism.Wpf)

文件 内容
Modularity/ModuleManager.cs 调度主逻辑(走读重点)
Modularity/ModuleManager.Desktop.cs 默认只挂一个 FileModuleTypeLoader
Modularity/ModuleInitializer.cs 反射解析类型 + 调钩子
Modularity/ModuleInfo.cs 元数据实现 + 多个便捷构造函数
Modularity/ModuleCatalog.cs 默认清单(就是一个空集合)
Modularity/IModuleCatalogExtensions.cs AddModule<T>(...) 全套重载
Modularity/IModuleTypeLoader.cs 类型加载器契约
Modularity/FileModuleTypeLoader.Desktop.cs 按 file:// 前缀加载 DLL
Modularity/AssemblyResolver.Desktop.cs 用 AppDomain.AssemblyResolve 懒加载
Modularity/DirectoryModuleCatalog.netcore.cs 反射扫描目录(可选方案)
Modularity/XamlModuleCatalog.cs 从 XAML 建清单
Modularity/ConfigurationModuleCatalog.Desktop.cs 从 app.config 建清单
Modularity/ModuleAttribute.Desktop.cs [Module(ModuleName=..., OnDemand=...)]

3. 数据模型

3.1 IModule:只有两个方法的契约

src/Prism.Core/Modularity/IModule.cs(全文)

csharp 复制代码
public interface IModule
{
    // 注册类型到容器:View / ViewModel / Service
    void RegisterTypes(IContainerRegistry containerRegistry);

    // 模块已就绪:挂 Region、订阅事件、初始化数据
    void OnInitialized(IContainerProvider containerProvider);
}

注意它没有 Name、Version、Initialize()。名字和类型这类元信息全部外置到 ModuleInfo 里。这个设计让 IModule 几乎零负担 ------ 业务模块类可以只有 5 行代码(见 e2e/Wpf/Modules/HelloWorld.Modules.ModuleA/ModuleAModule.cs)。

同时它不继承任何基类、不实现 IDisposable,意味着模块生命周期 = AppDomain 生命周期,没有卸载能力。这是取舍:简单,但热插拔做不了。

3.2 ModuleInfo:为什么类型存成字符串

src/Wpf/Prism.Wpf/Modularity/ModuleInfo.cs(L80-L111)

csharp 复制代码
public string ModuleName { get; set; }
public string ModuleType { get; set; }      // ← AssemblyQualifiedName,不是 Type
public Collection<string> DependsOn { get; set; }
public InitializationMode InitializationMode { get; set; }
public string Ref { get; set; }             // ← DLL 路径,WPF 专用
public ModuleState State { get; set; }      // ← 运行时状态机

ModuleType 是 string 而不是 Type,这是整个延迟加载机制的地基:

  • 如果是 Type,取 Type 就必须先加载程序集,那"按需加载 DLL"就不可能了
  • 存字符串 → 可以先把模块清单建好、DLL 却还在磁盘上没被加载
  • 代价:每次用都要 Type.GetType 反解析(有字符串解析开销,但换来的是灵活性)

ModuleInfo 提供了一堆便捷构造函数(L26-L74),注意 ModuleInfo(Type moduleType, string moduleName, ...) 内部也是立刻转成 AssemblyQualifiedName 存字符串:

csharp 复制代码
public ModuleInfo(Type moduleType, string moduleName, InitializationMode initializationMode)
    : this(moduleName, moduleType.AssemblyQualifiedName)
{
    InitializationMode = initializationMode;
}

AssemblyQualifiedName 长这样,包含程序集全名(版本、公钥标记都有):

复制代码
HelloWorld.Modules.ModuleA.ModuleAModule, HelloWorld.Modules.ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

这也是为什么模块项目升级版本号后,手写在配置里的类型名可能失效 ------ 必须同步改版本号,或改用 AddModule<T> 走 typeof。

3.3 ModuleState:5 状态机

src/Prism.Core/Modularity/ModuleState.cs(L8-L38)

复制代码
                ┌──────────────────┐
                │    NotStarted    │  ← 清单刚建好
                └────────┬─────────┘
                         │ ModuleNeedsRetrieval() == true
                         ▼
                ┌──────────────────┐
                │   LoadingTypes   │  ← 正在 Assembly.LoadFrom
                └────────┬─────────┘
                         │ 加载完成回调
                         ▼
          ┌──────────────────────────────────┐
          │     ReadyForInitialization       │  ← 类型已可解析
          └───────────────┬──────────────────┘
                          │ 依赖都 Initialized 了
                          ▼
                ┌──────────────────┐
                │    Initializing  │  ← 正在跑 RegisterTypes / OnInitialized
                └────────┬─────────┘
                         ▼
                ┌──────────────────┐
                │   Initialized    │  ← 终态
                └──────────────────┘

若模块类型在内存中已存在(默认场景),会跳过 LoadingTypes 直接从 NotStarted → ReadyForInitialization(ModuleManager.cs L102-L112)。

状态是可变字段 ,ModuleManager 通过改状态来协作,没有锁。这就是为什么模块加载默认必须在 UI 线程上跑(见第 14 章"坑")。


4. 启动链路

4.1 App 侧:只需要覆写两个方法

e2e/Wpf/HelloWorld/App.xaml.cs(L24-L27)

csharp 复制代码
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    moduleCatalog.AddModule<ModuleAModule>();
}

4.2 框架侧:PrismApplicationBase.Initialize

src/Wpf/Prism.Wpf/PrismApplicationBase.cs(L58-L87)

csharp 复制代码
protected virtual void Initialize()
{
    ContainerLocator.SetContainerExtension(CreateContainerExtension);
    _containerExtension = ContainerLocator.Current;
    _moduleCatalog = CreateModuleCatalog();      // ① 建清单
    RegisterRequiredTypes(_containerExtension);  // ② 注册模块化服务
    RegisterTypes(_containerExtension);          // ③ 你的类型
    _containerExtension.FinalizeExtension();

    ConfigureModuleCatalog(_moduleCatalog);      // ④ 往清单里塞模块

    var regionAdapterMappings = _containerExtension.Resolve<RegionAdapterMappings>();
    ConfigureRegionAdapterMappings(regionAdapterMappings);
    var defaultRegionBehaviors = _containerExtension.Resolve<IRegionBehaviorFactory>();
    ConfigureDefaultRegionBehaviors(defaultRegionBehaviors);

    // ... 创建 Shell、设置 RegionManager ...

    InitializeModules();                          // ⑤ 启动模块加载
}

CreateModuleCatalog 有默认实现(L101-L104):

csharp 复制代码
protected virtual IModuleCatalog CreateModuleCatalog()
{
    return new ModuleCatalog();   // 空清单,等你 AddModule
}

ConfigureModuleCatalog 基类是空方法 (L173),这是理解 Prism 模块化的关键:框架不做任何自动发现。

InitializeModules 只有一行(L178-L181),委托给扩展方法。

4.3 RegisterRequiredTypes:模块化服务的注册点

src/Wpf/Prism.Wpf/PrismInitializationExtensions.cs(L32-L48)

csharp 复制代码
internal static void RegisterRequiredTypes(this IContainerRegistry containerRegistry, IModuleCatalog moduleCatalog)
{
    containerRegistry.RegisterInstance(moduleCatalog);                    // ① 清单进容器
    containerRegistry.RegisterSingleton<IDialogService, DialogService>();
    containerRegistry.RegisterSingleton<IModuleInitializer, ModuleInitializer>();  // ②
    containerRegistry.RegisterSingleton<IModuleManager, ModuleManager>();          // ③
    // ... Region / EventAggregator 等
}

注意顺序问题:moduleCatalog 是实例 注册,而 ConfigureModuleCatalog 是在 FinalizeExtension() 之后 才调用的。也就是说容器里拿到的就是同一个 ModuleCatalog 对象,后续 AddModule 修改对象内容,容器里的引用能看到 ------ 这是靠"引用共享"而不是"重新注册"实现的。

RunModuleManager(L69-L73):

csharp 复制代码
internal static void RunModuleManager(IContainerProvider containerProvider)
{
    IModuleManager manager = containerProvider.Resolve<IModuleManager>();
    manager.Run();
}

4.4 完整时序图

复制代码
App                        PrismApplicationBase        ModuleManager        Catalog        Initializer
 │                                  │                       │                 │               │
 │─ OnStartup ─────────────────────►│                       │                 │               │
 │                                  │─ CreateModuleCatalog ►│                 │               │
 │                                  │─ RegisterRequiredTypes│                 │               │
 │                                  │─ ConfigureModuleCatalog ─────────────► │ AddModule     │
 │                                  │─ CreateShell          │                 │               │
 │                                  │─ InitializeModules ──►│                 │               │
 │                                  │                       │─ Run() ────────►│               │
 │                                  │                       │                 │─ Load()       │
 │                                  │                       │                 │─ Validate()   │
 │                                  │                       │◄────────────────│               │
 │                                  │                       │ LoadModulesWhenAvailable()    │
 │                                  │                       │                 │               │
 │                                  │              ┌────────┴────────┐        │               │
 │                                  │              │ 每个 WhenAvailable 模块│        │               │
 │                                  │              │ ModuleNeedsRetrieval?  │        │               │
 │                                  │              └────────┬────────┘        │               │
 │                                  │                       │                 │               │
 │                                  │        ┌──────────────┴──────────────┐  │               │
 │                                  │        │ 需要取 DLL → BeginRetrieving │  │               │
 │                                  │        │ 不需要     → ReadyForInit    │  │               │
 │                                  │        └──────────────┬──────────────┘  │               │
 │                                  │                       │                 │               │
 │                                  │              ┌────────┴─────────┐       │               │
 │                                  │              │ LoadModulesThat  │       │               │
 │                                  │              │ AreReadyForLoad  │       │               │
 │                                  │              │ (while + break)  │       │               │
 │                                  │              └────────┬─────────┘       │               │
 │                                  │                       │────────────────────────────────►│
 │                                  │                       │                 │  Initialize() │
 │                                  │                       │                 │               │─ Type.GetType
 │                                  │                       │                 │               │─ Resolve(Type)
 │                                  │                       │                 │               │─ RegisterTypes
 │                                  │                       │                 │               │─ OnInitialized
 │                                  │                       │◄────────────────────────────────│
 │                                  │                       │ State = Initialized            │
 │◄─────────────────────────────────│◄──────────────────────│                 │               │

5. ModuleManager 逐段走读

文件:src/Wpf/Prism.Wpf/Modularity/ModuleManager.cs(316 行)

5.1 构造函数与字段

L14-L32

csharp 复制代码
private readonly IModuleInitializer moduleInitializer;
private IEnumerable<IModuleTypeLoader> typeLoaders;
private HashSet<IModuleTypeLoader> subscribedToModuleTypeLoaders = new HashSet<IModuleTypeLoader>();

public ModuleManager(IModuleInitializer moduleInitializer, IModuleCatalog moduleCatalog)
{
    this.moduleInitializer = moduleInitializer ?? throw new ArgumentNullException(nameof(moduleInitializer));
    ModuleCatalog = moduleCatalog ?? throw new ArgumentNullException(nameof(moduleCatalog));
}

subscribedToModuleTypeLoaders 这个 HashSet 解决的是一个经典 bug:typeLoaders 可能被多次枚举,如果每次都 += 事件订阅,同一实例会被订阅多次,导致回调重复触发。用 HashSet 去重(L185-L190)。

5.2 Run:启动入口

L67-L72

csharp 复制代码
public void Run()
{
    this.ModuleCatalog.Initialize();      // Load + Validate
    this.LoadModulesWhenAvailable();      // 加载 WhenAvailable 的
}

注意 Run() 不处理 OnDemand 模块,那些要等外部显式调用。

5.3 LoadModulesWhenAvailable:筛选入口

L118-L126

csharp 复制代码
private void LoadModulesWhenAvailable()
{
    var whenAvailableModules = this.ModuleCatalog.Modules
        .Where(m => m.InitializationMode == InitializationMode.WhenAvailable);

    // 关键:把依赖链一起带上,并排序
    var modulesToLoadTypes = this.ModuleCatalog.CompleteListWithDependencies(whenAvailableModules);

    if (modulesToLoadTypes != null)
        this.LoadModuleTypes(modulesToLoadTypes);
}

CompleteListWithDependencies 会把"被依赖的 OnDemand 模块"也拉进来(虽然 Validate 会阻止 WhenAvailable 依赖 OnDemand,但显式配置错误时这里会兜住)。

5.4 LoadModule:按需加载入口

L79-L90

csharp 复制代码
public void LoadModule(string moduleName)
{
    var module = this.ModuleCatalog.Modules.Where(m => m.ModuleName == moduleName);
    if (module == null || module.Count() != 1)
        throw new ModuleNotFoundException(moduleName, $"模块 {moduleName} 未找到");

    var modulesToLoad = this.ModuleCatalog.CompleteListWithDependencies(module);
    this.LoadModuleTypes(modulesToLoad);
}

注意 module == null 这个判断其实永远不成立 (LINQ 的 Where 不返回 null),真正的保护是 Count() != 1。这是个无害但存在的小瑕疵,读源码时值得留意 ------ 说明这段代码是从旧版本延续下来的。

5.5 ModuleNeedsRetrieval:反射点 ①

L97-L116

csharp 复制代码
protected virtual bool ModuleNeedsRetrieval(IModuleInfo moduleInfo)
{
    if (moduleInfo == null) throw new ArgumentNullException(nameof(moduleInfo));

    if (moduleInfo.State == ModuleState.NotStarted)
    {
        // 如果能实例化该类型,说明模块程序集已经在 AppDomain 里了,不需要再去取
        bool isAvailable = Type.GetType(moduleInfo.ModuleType) != null;
        if (isAvailable)
            moduleInfo.State = ModuleState.ReadyForInitialization;

        return !isAvailable;
    }
    return false;
}

这是第一个反射调用点 。Type.GetType(assemblyQualifiedName) 的行为:

  • 传入带程序集全名的字符串时,会尝试从已加载的程序集 里找;找不到返回 null(不会抛异常)
  • 它不会 主动去磁盘找 DLL ------ 这正是为什么需要 IModuleTypeLoader

返回 true = "需要去取 DLL",返回 false = "已经能用"。

5.6 LoadModuleTypes:两阶段推进

L128-L151

csharp 复制代码
private void LoadModuleTypes(IEnumerable<IModuleInfo> moduleInfos)
{
    if (moduleInfos == null) return;

    // 阶段一:把所有 NotStarted 的模块推进到 LoadingTypes 或 ReadyForInitialization
    foreach (var moduleInfo in moduleInfos)
    {
        if (moduleInfo.State == ModuleState.NotStarted)
        {
            if (this.ModuleNeedsRetrieval(moduleInfo))
                this.BeginRetrievingModule(moduleInfo);   // 异步/回调式
            else
                moduleInfo.State = ModuleState.ReadyForInitialization;
        }
    }

    // 阶段二:把"依赖已就绪"的模块实际初始化
    this.LoadModulesThatAreReadyForLoad();
}

为什么分两阶段?因为 BeginRetrievingModule 可能同步或异步 完成。同步完成时(FileModuleTypeLoader 是同步的)回调里已经跑过一次 LoadModulesThatAreReadyForLoad;异步完成时,阶段二先跑一遍处理那些本来就 ready 的,剩下的等回调。

5.7 BeginRetrievingModule

L177-L193

csharp 复制代码
private void BeginRetrievingModule(IModuleInfo moduleInfo)
{
    var moduleInfoToLoadType = moduleInfo;
    IModuleTypeLoader moduleTypeLoader = this.GetTypeLoaderForModule(moduleInfoToLoadType);
    moduleInfoToLoadType.State = ModuleState.LoadingTypes;

    // Delegate += 在不同平台行为不同,我们只想对每个实例订阅一次
    if (!this.subscribedToModuleTypeLoaders.Contains(moduleTypeLoader))
    {
        moduleTypeLoader.ModuleDownloadProgressChanged += this.IModuleTypeLoader_ModuleDownloadProgressChanged;
        moduleTypeLoader.LoadModuleCompleted += this.IModuleTypeLoader_LoadModuleCompleted;
        this.subscribedToModuleTypeLoaders.Add(moduleTypeLoader);
    }

    moduleTypeLoader.LoadModuleType(moduleInfo);
}

选择哪个 loader 靠 CanLoadModuleType 探测(L263-L274):

csharp 复制代码
private IModuleTypeLoader GetTypeLoaderForModule(IModuleInfo moduleInfo)
{
    foreach (IModuleTypeLoader typeLoader in this.ModuleTypeLoaders)
    {
        if (typeLoader.CanLoadModuleType(moduleInfo))
            return typeLoader;
    }
    throw new ModuleTypeLoaderNotFoundException(...);
}

默认只有一个(ModuleManager.Desktop.cs L17-L36):

csharp 复制代码
public virtual IEnumerable<IModuleTypeLoader> ModuleTypeLoaders
{
    get
    {
        if (this.typeLoaders == null)
        {
            this.typeLoaders = new List<IModuleTypeLoader> { new FileModuleTypeLoader() };
        }
        return this.typeLoaders;
    }
    set { this.typeLoaders = value; }
}

ModuleTypeLoaders 是 virtual 的,说明这是官方预留的扩展点:想从网络/压缩包加载模块,实现一个 IModuleTypeLoader 然后覆写这个属性即可。CanLoadModuleType 的惯例是检查 Ref 的前缀(file:// 对应文件加载器,http:// 对应 HTTP 下载器)。

5.8 回调:错误处理分叉

L200-L224

csharp 复制代码
private void IModuleTypeLoader_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
{
    if (e.Error == null)
    {
        if ((e.ModuleInfo.State != ModuleState.Initializing) && (e.ModuleInfo.State != ModuleState.Initialized))
            e.ModuleInfo.State = ModuleState.ReadyForInitialization;

        this.LoadModulesThatAreReadyForLoad();   // 继续推进
    }
    else
    {
        this.RaiseLoadModuleCompleted(e);

        if (!e.IsErrorHandled)                   // 订阅者可以"吃掉"错误
            this.HandleModuleTypeLoadingError(e.ModuleInfo, e.Error);
    }
}

IsErrorHandled 这个设计挺实用:UI 层订阅 LoadModuleCompleted 后可以弹个框、e.IsErrorHandled = true,就阻止框架继续抛异常。

5.9 LoadModulesThatAreReadyForLoad:核心调度循环

L156-L175,全文件最值得看的一段

csharp 复制代码
protected virtual void LoadModulesThatAreReadyForLoad()
{
    bool keepLoading = true;
    while (keepLoading)
    {
        keepLoading = false;
        var availableModules = this.ModuleCatalog.Modules
            .Where(m => m.State == ModuleState.ReadyForInitialization);

        foreach (var moduleInfo in availableModules)
        {
            if ((moduleInfo.State != ModuleState.Initialized) && (this.AreDependenciesLoaded(moduleInfo)))
            {
                moduleInfo.State = ModuleState.Initializing;
                this.InitializeModule(moduleInfo);
                keepLoading = true;
                break;              // ← 每轮只初始化一个,然后重新扫描
            }
        }
    }
}

三个细节:

  1. 每轮只处理一个模块就 break 。为什么?因为 InitializeModule 会执行模块的 RegisterTypes,可能注册新的服务、修改容器状态;而且模块初始化后,其他模块的"依赖是否就绪"判断结果会变。重新构建 availableModules 保证用的是最新状态。
  2. keepLoading 双重用途:既控制 while 是否继续,又作为"本轮是否找到活儿"的标志。
  3. 性能是 O(n²)。模块数通常 < 20,可以接受;这也说明 Prism 的设计假设是"模块数量不多"。

对应的依赖判断(L249-L261):

csharp 复制代码
private bool AreDependenciesLoaded(IModuleInfo moduleInfo)
{
    var requiredModules = this.ModuleCatalog.GetDependentModules(moduleInfo);
    if (requiredModules == null) return true;

    int notReadyRequiredModuleCount =
        requiredModules.Count(requiredModule => requiredModule.State != ModuleState.Initialized);

    return notReadyRequiredModuleCount == 0;
}

注意它用的是 != Initialized 而不是 == NotStarted。这意味着依赖模块初始化失败时,依赖它的模块会永远卡住 (状态停在 ReadyForInitialization,循环会一直转但不做事,最后 keepLoading=false 退出)。这是排查"某个模块死活不初始化"的第一线索。

5.10 InitializeModule

L276-L284

csharp 复制代码
private void InitializeModule(IModuleInfo moduleInfo)
{
    if (moduleInfo.State == ModuleState.Initializing)
    {
        this.moduleInitializer.Initialize(moduleInfo);   // 可能抛异常
        moduleInfo.State = ModuleState.Initialized;      // 只有成功才走到这里
        this.RaiseLoadModuleCompleted(moduleInfo, null);
    }
}

如果 Initialize 抛异常,State 会停在 Initializing ,且异常直接冒泡到 Run()/LoadModule() 调用方。这就是"模块初始化失败导致整个启动崩溃"的机制来源。


6. ModuleInitializer 走读

文件:src/Wpf/Prism.Wpf/Modularity/ModuleInitializer.cs(118 行)

6.1 Initialize:极小的方法体

L28-L50

csharp 复制代码
public void Initialize(IModuleInfo moduleInfo)
{
    if (moduleInfo == null) throw new ArgumentNullException(nameof(moduleInfo));

    IModule moduleInstance = null;
    try
    {
        moduleInstance = this.CreateModule(moduleInfo);
        if (moduleInstance != null)
        {
            moduleInstance.RegisterTypes(_containerExtension);
            moduleInstance.OnInitialized(_containerExtension);
        }
    }
    catch (Exception ex)
    {
        this.HandleModuleInitializationError(
            moduleInfo,
            moduleInstance?.GetType().Assembly.FullName,
            ex);
    }
}

关键观察:

  • RegisterTypes 和 OnInitialized 一定成对调用,中间没有别的逻辑
  • 两者接收的都是 _containerExtension(同时实现 IContainerRegistry 和 IContainerProvider),所以模块既能注册也能解析
  • catch 后重新抛出 (HandleModuleInitializationError 结尾是 throw moduleException),异常只是被包装成 ModuleInitializeException 并补上模块名/程序集名,便于定位

6.2 CreateModule:反射点 ②

L107-L116

csharp 复制代码
protected virtual IModule CreateModule(string typeName)
{
    Type moduleType = Type.GetType(typeName);
    if (moduleType == null)
        throw new ModuleInitializeException($"无法获取类型 {typeName}");

    return (IModule)_containerExtension.Resolve(moduleType);
}

两个 virtual(CreateModule(IModuleInfo) 和 CreateModule(string))意味着可以覆写来做 AOP(比如初始化计时、日志、模块隔离)。

这里 Type.GetType 能成功的前提是程序集已经加载 ------ 上面 ModuleNeedsRetrieval 已经保证过了。

(IModule) 这个强制转换是唯一的类型安全校验点。如果清单里配了个不实现 IModule 的类型,这里抛 InvalidCastException,被包装成 ModuleInitializeException。


7. ModuleCatalogBase 走读

文件:src/Prism.Core/Modularity/ModuleCatalogBase.cs(381 行)

这是与 UI 无关的纯逻辑类,依赖算法全在这里。

7.1 三个集合视图

L62-L87

csharp 复制代码
public Collection<IModuleCatalogItem> Items => _items;              // 原始集合(含 Group)
public virtual IEnumerable<IModuleInfo> Modules =>
    GrouplessModules.Union(Groups.SelectMany(g => g));              // 扁平化后的所有模块
public IEnumerable<IModuleInfoGroup> Groups => Items.OfType<IModuleInfoGroup>();
protected IEnumerable<IModuleInfo> GrouplessModules => Items.OfType<IModuleInfo>();

Items 里可以放 ModuleInfo 或 ModuleInfoGroup(一组共享同一个 Ref 的模块,用于"一个 DLL 里多个模块"的场景)。Modules 用 Union 去重 ------ 因为 ModuleInfoGroup 里的模块不应该在 GrouplessModules 里重复出现。

7.2 Initialize = Load + Validate

L92-L96、L180-L188

csharp 复制代码
public virtual void Load()
{
    _isLoaded = true;
    InnerLoad();          // ← 子类覆写,扫描/读配置/读 XAML
}

public virtual void Initialize()
{
    if (!_isLoaded) Load();
    Validate();
}

InnerLoad 基类是空实现(L253-L255)。所有"从外部来源构建清单"的逻辑都在 InnerLoad 的覆写里,这是模板方法模式的教科书用法。

7.3 Validate:四项检查

L155-L163

csharp 复制代码
public virtual void Validate()
{
    ValidateUniqueModules();                    // ① 名字不能重复
    ValidateDependencyGraph();                  // ② 不能有循环依赖
    ValidateCrossGroupDependencies();           // ③ 组内模块不能依赖别的组的模块
    ValidateDependenciesInitializationMode();   // ④ WhenAvailable 不能依赖 OnDemand

    Validated = true;
}

第 ④ 项容易被忽略但很重要(L318-L332):

csharp 复制代码
var moduleInfo = Modules.FirstOrDefault(m =>
    m.InitializationMode == InitializationMode.WhenAvailable &&
    GetDependentModulesInner(m).Any(dependency => dependency.InitializationMode == InitializationMode.OnDemand));

if (moduleInfo != null)
    throw new ModularityException(...);

原因:启动时 LoadModulesWhenAvailable 只处理 WhenAvailable 的模块。如果 A(WhenAvailable)依赖 B(OnDemand),B 永远不会被加载,A 就卡死。Prism 在启动阶段就把这个错误暴露出来,而不是让你运行时调半天。

另外注意 Validated 标志与 ItemsCollectionChanged(L355-L361):往 Items 里加东西后如果已经验证过,会重新验证 ------ 保证动态 AddModule 后清单依然合法。

7.4 CompleteListWithDependencies:依赖闭包

L122-L149

csharp 复制代码
public virtual IEnumerable<IModuleInfo> CompleteListWithDependencies(IEnumerable<IModuleInfo> modules)
{
    if (modules == null) throw new ArgumentNullException(nameof(modules));

    EnsureCatalogValidated();

    List<IModuleInfo> completeList = new List<IModuleInfo>();
    List<IModuleInfo> pendingList = modules.ToList();

    while (pendingList.Count > 0)
    {
        var moduleInfo = pendingList[0];

        foreach (var dependency in GetDependentModules(moduleInfo))
        {
            if (!completeList.Contains(dependency) && !pendingList.Contains(dependency))
                pendingList.Add(dependency);        // 依赖入队
        }

        pendingList.RemoveAt(0);
        completeList.Add(moduleInfo);
    }

    IEnumerable<IModuleInfo> sortedList = Sort(completeList);
    return sortedList;
}

这是一个广度优先的依赖闭包算法:

  • 从给定模块出发,把每个模块的依赖追加到待处理队列
  • completeList / pendingList 双重判重,防止菱形依赖重复入队
  • 最后交给 Sort 拓扑排序

注意 pendingList 用 List + RemoveAt(0) 而不是 Queue,这是 O(n) 的删除。模块数少所以无所谓,但读的时候知道这是"队列语义"就好。

7.5 Sort → SolveDependencies

L263-L269、L195-L221

csharp 复制代码
protected virtual IEnumerable<IModuleInfo> Sort(IEnumerable<IModuleInfo> modules)
{
    foreach (string moduleName in SolveDependencies(modules))
        yield return modules.First(m => m.ModuleName == moduleName);
}

protected static string[] SolveDependencies(IEnumerable<IModuleInfo> modules)
{
    ModuleDependencySolver solver = new ModuleDependencySolver();

    foreach (var data in modules)
    {
        solver.AddModule(data.ModuleName);
        if (data.DependsOn != null)
            foreach (string dependency in data.DependsOn)
                solver.AddDependency(data.ModuleName, dependency);   // ← 注意参数顺序
    }

    if (solver.ModuleCount > 0) return solver.Solve();
    return new string[0];
}

Solver 只认名字(string),不认 ModuleInfo,所以 Sort 需要再映射回去。这是"算法与数据结构解耦"的常见做法 ------ ModuleDependencySolver 完全可以单独拿去解决任何依赖排序问题。


8. ModuleDependencySolver 拓扑排序详解

文件:src/Prism.Core/Modularity/ModuleDependencySolver.cs(155 行)

8.1 数据结构

L15-L16

csharp 复制代码
private readonly ListDictionary<string, string> dependencyMatrix;  // key: 被依赖方, value: 依赖它的模块们
private readonly List<string> knownModules;

ListDictionary 是 .NET 里针对小集合优化的字典(内部是链表),避免为几个模块分配 HashTable。

8.2 AddDependency 的参数语义(易混淆)

L38-L51

csharp 复制代码
public void AddDependency(string dependingModule, string dependentModule)
{
    ...
    if (!knownModules.Contains(dependingModule))
        throw new ArgumentException(...);

    AddToDependencyMatrix(dependentModule);
    dependencyMatrix.Add(dependentModule, dependingModule);   // ← 反向存
}

参数名很拗口:dependingModule = "有依赖的那个"(A),dependentModule = "被依赖的那个"(B)。

调用 AddDependency("A", "B") 表示 A depends on B ,但矩阵里存成 matrix["B"] = ["A", ...] ------ 即 key 是被依赖方,value 是依赖者列表(反向边)。

if (!knownModules.Contains(dependingModule)) 这个检查只校验了 A 是否注册过,不校验 B ------ 因为 B 可能不在当前子集里(比如只加载部分模块时),AddToDependencyMatrix 会给它自动建空条目。

8.3 Solve:反复剥离"没人依赖的节点"

L76-L99

csharp 复制代码
public string[] Solve()
{
    List<string> skip = new List<string>();
    while (skip.Count < dependencyMatrix.Count)
    {
        List<string> leaves = this.FindLeaves(skip);
        if (leaves.Count == 0 && skip.Count < dependencyMatrix.Count)
            throw new CyclicDependencyFoundException(...);
        skip.AddRange(leaves);
    }
    skip.Reverse();                      // ← 结果反转

    if (skip.Count > knownModules.Count)  // ← 检测依赖到不存在的模块
    {
        string moduleNames = this.FindMissingModules(skip);
        throw new ModularityException(moduleNames, ...);
    }
    return skip.ToArray();
}

FindLeaves(L126-L152)找的是"当前没有任何未处理模块依赖它"的节点:

csharp 复制代码
foreach (string precedent in dependencyMatrix.Keys)     // precedent = 被依赖方
{
    if (skip.Contains(precedent)) continue;

    int count = 0;
    foreach (string dependent in dependencyMatrix[precedent])   // dependent = 依赖它的模块
    {
        if (skip.Contains(dependent)) continue;
        count++;
    }
    if (count == 0) result.Add(precedent);   // 没人依赖 → 是"叶子"
}

8.4 手工推演一遍

假设:A 依赖 B,B 依赖 C (配置里 AddDependency("A","B")、AddDependency("B","C"))

矩阵:

复制代码
matrix["A"] = []        // 没人依赖 A
matrix["B"] = ["A"]     // A 依赖 B
matrix["C"] = ["B"]     // B 依赖 C
轮次 skip FindLeaves 判定 leaves
1 [] A: 无人依赖 → 是;B: 被 A 依赖 → 否;C: 被 B 依赖 → 否 [A]
2 [A] A 已跳过;B: 唯一的依赖者 A 已跳过 → 是;C: 被 B 依赖 → 否 [B]
3 [A,B] C: 依赖者 B 已跳过 → 是 [C]

skip = [A, B, C] → Reverse() → [C, B, A]

结果正好是正确加载顺序:C 最先、A 最后。因为每轮剥离的是"最上层"(没人依赖它),所以最终顺序要反转。

8.5 循环依赖检测

leaves.Count == 0 且还有节点没处理 → 必然存在环。

例如 A→B→C→A:

  • 第 1 轮:A 被 C 依赖、B 被 A 依赖、C 被 B 依赖 → 没有叶子 → 抛 CyclicDependencyFoundException

这个检测同时被 ValidateDependencyGraph 复用(ModuleCatalogBase L293-L296),所以循环依赖在启动校验阶段就被拦下,不会拖到加载时才炸。


9. IModuleTypeLoader 与 FileModuleTypeLoader

9.1 契约

src/Wpf/Prism.Wpf/Modularity/IModuleTypeLoader.cs

csharp 复制代码
public interface IModuleTypeLoader
{
    bool CanLoadModuleType(IModuleInfo moduleInfo);      // 我能不能处理这个模块?
    void LoadModuleType(IModuleInfo moduleInfo);         // 去加载(通常异步回调)
    event EventHandler<ModuleDownloadProgressChangedEventArgs> ModuleDownloadProgressChanged;
    event EventHandler<LoadModuleCompletedEventArgs> LoadModuleCompleted;
}

注意它叫 Loader 但注释里说"download" ------ 因为 WPF 版历史上还支持过 Silverlight 的 XAP 下载,事件模型是从那继承来的。

9.2 FileModuleTypeLoader:判断 + 加载

src/Wpf/Prism.Wpf/Modularity/FileModuleTypeLoader.Desktop.cs

判断(L77-L85):

csharp 复制代码
public bool CanLoadModuleType(IModuleInfo moduleInfo)
{
    if (moduleInfo == null) throw new ArgumentNullException(nameof(moduleInfo));
    return moduleInfo.Ref != null && moduleInfo.Ref.StartsWith("file://", StringComparison.Ordinal);
}

加载(L93-L138,去掉了异常处理细节):

csharp 复制代码
public void LoadModuleType(IModuleInfo moduleInfo)
{
    try
    {
        Uri uri = new Uri(moduleInfo.Ref, UriKind.RelativeOrAbsolute);

        if (this.IsSuccessfullyDownloaded(uri))          // 幂等:已加载过直接回调
        {
            this.RaiseLoadModuleCompleted(moduleInfo, null);
        }
        else
        {
            string path = uri.LocalPath;
            long fileSize = File.Exists(path) ? new FileInfo(path).Length : -1L;

            this.RaiseModuleDownloadProgressChanged(moduleInfo, 0, fileSize);
            this.assemblyResolver.LoadAssemblyFrom(moduleInfo.Ref);   // ← 实际动作
            this.RaiseModuleDownloadProgressChanged(moduleInfo, fileSize, fileSize);

            this.RecordDownloadSuccess(uri);
            this.RaiseLoadModuleCompleted(moduleInfo, null);
        }
    }
    catch (Exception ex)
    {
        this.RaiseLoadModuleCompleted(moduleInfo, ex);   // 错误通过事件传递,不抛
    }
}

两个设计点:

  • 幂等 :用 HashSet<Uri> downloadedUris 记录,避免同一 DLL 重复加载(.NET 里重复 LoadFrom 同一路径会返回同一 Assembly,但事件会重复触发)
  • 异常不走 throw :错误包装进 LoadModuleCompletedEventArgs.Error,由 ModuleManager 决定是否抛出(见 5.8)

9.3 AssemblyResolver:懒加载 + AssemblyResolve 事件

这个类解决的是 .NET 的 "Load / LoadFrom 上下文" 老问题(L28-L58):

csharp 复制代码
public void LoadAssemblyFrom(string assemblyFilePath)
{
    if (!this.handlesAssemblyResolve)
    {
        AppDomain.CurrentDomain.AssemblyResolve += this.CurrentDomain_AssemblyResolve;
        this.handlesAssemblyResolve = true;
    }

    ...
    AssemblyName assemblyName = AssemblyName.GetAssemblyName(assemblyUri.LocalPath);   // 只读元数据,不加载
    if (registeredAssemblies.FirstOrDefault(a => assemblyName == a.AssemblyName) != null) return;

    registeredAssemblies.Add(new AssemblyInfo { AssemblyName = assemblyName, AssemblyUri = assemblyUri });
}

注意:LoadAssemblyFrom 并不真正加载程序集 ,它只是"登记"(用 AssemblyName.GetAssemblyName 读元数据,这个 API 不会把程序集载入进程)。真正的加载发生在 JIT 需要解析某类型时的 AssemblyResolve 回调里(L82-L99):

csharp 复制代码
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    AssemblyName assemblyName = new AssemblyName(args.Name);
    AssemblyInfo assemblyInfo = registeredAssemblies.FirstOrDefault(
        a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.AssemblyName));

    if (assemblyInfo != null)
    {
        if (assemblyInfo.Assembly == null)
            assemblyInfo.Assembly = Assembly.LoadFrom(assemblyInfo.AssemblyUri.LocalPath);   // ← 真正的加载点
        return assemblyInfo.Assembly;
    }
    return null;
}

所以"加载模块 DLL"这件事实际是三段式:

  1. FileModuleTypeLoader.CanLoadModuleType 判断
  2. AssemblyResolver.LoadAssemblyFrom 登记(注册事件 + 记路径)
  3. 后面的 Type.GetType 触发 CLR 解析 → AssemblyResolve → Assembly.LoadFrom

这是 .NET Core / .NET 5+ 上依然可用的做法,因为 AssemblyResolve 事件没被移除。


10. 四种 Catalog 对比(这才是"是否扫描"的分水岭)

类型 模块来源 是否反射扫描 典型场景
ModuleCatalog 代码 AddModule<T>() 否 默认,项目引用模块
DirectoryModuleCatalog 目录下 *.dll 是 (GetExportedTypes + 特性) 插件式部署、模块不进主项目引用
XamlModuleCatalog .xaml 资源文件 否(XamlReader.Load 反序列化) 清单可视化编辑
ConfigurationModuleCatalog app.config 的 <modules> 节 否(读 ConfigurationSection) 运维可改配置

10.1 ModuleCatalog(默认)

就是 ModuleCatalogBase 加一个 GetFileAbsoluteUri 工具方法,自身没有任何加载逻辑 。模块全部靠 AddModule 加进来。

10.2 DirectoryModuleCatalog:真正的反射扫描

src/Wpf/Prism.Wpf/Modularity/DirectoryModuleCatalog.netcore.cs

InnerLoad(L33-L73)流程:

csharp 复制代码
protected override void InnerLoad()
{
    if (string.IsNullOrEmpty(this.ModulePath)) throw new InvalidOperationException(...);
    if (!Directory.Exists(this.ModulePath))    throw new InvalidOperationException(...);

    AppDomain childDomain = AppDomain.CurrentDomain;       // ← 注释说新建子域,实际 .NET Core 不支持,就用当前域

    try
    {
        // 记下已加载的程序集路径(后面要排除)
        var assemblies = from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
                         where !(assembly is System.Reflection.Emit.AssemblyBuilder)
                            && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
                            && !String.IsNullOrEmpty(assembly.Location)
                         select assembly.Location;
        loadedAssemblies.AddRange(assemblies);

        Type loaderType = typeof(InnerModuleInfoLoader);
        if (loaderType.Assembly != null)
        {
            var loader = (InnerModuleInfoLoader)childDomain.CreateInstanceFrom(
                loaderType.Assembly.Location, loaderType.FullName).Unwrap();
            this.Items.AddRange(loader.GetModuleInfos(this.ModulePath));
        }
    }
    catch (Exception ex) { throw new Exception("There was an error loading assemblies.", ex); }
}

InnerModuleInfoLoader 继承 MarshalByRefObject ------ 这是为了旧 .NET Framework 的跨 AppDomain 调用。在 .NET Core 上 AppDomain.CreateInstanceFrom 只是当前域内反射创建对象,partial 编译(net45 / netcore 两个文件)保留了这套代码。

真正的扫描(L107-L124):

csharp 复制代码
foreach (FileInfo fileInfo in fileInfos)
{
    try
    {
        validAssemblies.Add(Assembly.LoadFrom(fileInfo.FullName));
    }
    catch (BadImageFormatException)
    {
        // 跳过非 .NET DLL(比如原生 C++ 依赖)
    }
}

return validAssemblies.SelectMany(assembly => assembly
            .GetExportedTypes()
            .Where(IModuleType.IsAssignableFrom)     // 实现 IModule
            .Where(t => t != IModuleType)
            .Where(t => !t.IsAbstract)
            .Select(type => CreateModuleInfo(type)));

读特性用 CustomAttributeData 而不是 GetCustomAttributes(L160-L207):

csharp 复制代码
private static ModuleInfo CreateModuleInfo(Type type)
{
    string moduleName = type.Name;                  // 默认用类型名
    List<string> dependsOn = new List<string>();
    bool onDemand = false;

    var moduleAttribute = CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(
        cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);

    if (moduleAttribute != null)
    {
        foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
        {
            switch (argument.MemberInfo.Name)
            {
                case "ModuleName":  moduleName = (string)argument.TypedValue.Value; break;
                case "OnDemand":    onDemand = (bool)argument.TypedValue.Value;     break;
                case "StartupLoaded": onDemand = !((bool)argument.TypedValue.Value); break;
            }
        }
    }

    var moduleDependencyAttributes = CustomAttributeData.GetCustomAttributes(type).Where(
        cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);

    foreach (CustomAttributeData cad in moduleDependencyAttributes)
        dependsOn.Add((string)cad.ConstructorArguments[0].Value);

    ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
    {
        InitializationMode = onDemand ? InitializationMode.OnDemand : InitializationMode.WhenAvailable,
        Ref = type.Assembly.EscapedCodeBase,
    };
    moduleInfo.DependsOn.AddRange(dependsOn);
    return moduleInfo;
}

为什么用 CustomAttributeData 而不是 type.GetCustomAttributes()?

  • GetCustomAttributes 会实例化特性对象,执行特性构造函数,还可能要加载特性类型所在程序集
  • CustomAttributeData 只读元数据表,不执行任何用户代码
  • 在"扫描未知 DLL"场景下,这是安全性和性能的双重考虑

这是整个仓库里最完整的"反射扫描"实现,也是回答"Prism 用反射吗"最好的反面证据 ------ 它只在你主动选择 DirectoryModuleCatalog 时才扫描。

注意 CreateModuleInfo 里 Ref = type.Assembly.EscapedCodeBase ------ 把 DLL 的路径记下来,后面 FileModuleTypeLoader 就能用 file:// 前缀匹配到它。但此时 Assembly.LoadFrom 已经把程序集加进内存了,所以 ModuleNeedsRetrieval 会返回 false,直接跳过加载阶段。

10.3 XamlModuleCatalog

InnerLoad(L38-L67)就是 XamlReader.Load 反序列化出一个 ModuleCatalog,然后把每个 ModuleInfo.Ref 从相对路径转成绝对 file:// URI:

csharp 复制代码
protected override void InnerLoad()
{
    var catalog = CreateFromXaml(_resourceUri);

    foreach (IModuleCatalogItem item in catalog.Items)
    {
        if (item is ModuleInfo mi)
        {
            if (!string.IsNullOrWhiteSpace(mi.Ref))
                mi.Ref = GetFileAbsoluteUri(mi.Ref);       // 相对 → file:/// 绝对
        }
        else if (item is ModuleInfoGroup mg) { /* 组内统一处理 */ }

        Items.Add(item);
    }
}

有个兼容性细节(L70-L83):同时接受 file:///path 和 file://path 两种老写法。

10.4 ConfigurationModuleCatalog

InnerLoad(L31-L68)从 ConfigurationStore 读 <modules> 配置节:

csharp 复制代码
foreach (ModuleConfigurationElement element in section.Modules)
{
    IList<string> dependencies = new List<string>();
    foreach (ModuleDependencyConfigurationElement dependency in element.Dependencies)
        dependencies.Add(dependency.ModuleName);

    ModuleInfo moduleInfo = new ModuleInfo(element.ModuleName, element.ModuleType)
    {
        Ref = GetFileAbsoluteUri(element.AssemblyFile),
        InitializationMode = element.StartupLoaded ? InitializationMode.WhenAvailable : InitializationMode.OnDemand
    };
    moduleInfo.DependsOn.AddRange(dependencies.ToArray());
    AddModule(moduleInfo);
}

注意 StartupLoaded = true 对应 WhenAvailable,是反向映射(配置项语义是"启动时加载吗",枚举语义是"可用时加载")。


11. 容器那一环:最后一段反射

ModuleInitializer.CreateModule 里的 _containerExtension.Resolve(moduleType) 走到容器实现。

src/Containers/Prism.DryIoc.Shared/DryIocContainerExtension.cs(L271-L300)

csharp 复制代码
public object Resolve(Type type) =>
    Resolve(type, Array.Empty<(Type, object)>());

public object Resolve(Type type, params (Type Type, object Instance)[] parameters)
{
    try
    {
        var container = _currentScope?.Resolver ?? Instance;
        return container.Resolve(type, args: parameters.Select(p => p.Instance).ToArray());
    }
    catch (Exception ex)
    {
        throw new ContainerResolutionException(type, ex);
    }
}

DryIoc 内部的 container.Resolve(Type) 会:

  1. 查注册表;模块类型通常没显式注册 → 走"未注册类型自动解析"规则
  2. 反射读取构造函数
  3. 生成/缓存一个编译表达式或 IL 委托 (DryIoc 的做法是 Expression.New 编译成委托),后续同类型解析直接调委托,不再反射

所以"反射创建对象"的代价是一次性的 。这也是为什么 Prism 不自己写 Activator.CreateInstance 而是坚持 Resolve(Type):

  • Activator.CreateInstance 每次都反射,慢
  • 没法注入依赖(模块构造函数里拿不到服务)
  • 换容器后行为一致

Unity 版(UnityContainerExtension.cs L269-L270)到 Resolve(type, params (Type, object)[]) 的委托链结构完全相同 ------ 说明 IContainerExtension 这个抽象层确实做到了容器无关。


12. 反射点总清单

# 位置 API 时机 作用
1 ModuleManager.ModuleNeedsRetrieval (L106) Type.GetType(string) 每次模块加载前 探测程序集是否已在内存
2 ModuleInitializer.CreateModule (L109) Type.GetType(string) 每次模块实例化前 字符串 → Type
3 ModuleInitializer.CreateModule (L115) container.Resolve(Type) 同上 容器反射构造实例
4 AssemblyResolver.CurrentDomain_AssemblyResolve (L92) Assembly.LoadFrom CLR 解析类型失败时 真正的 DLL 加载
5 DirectoryModuleCatalog (L111, L119-L124) Assembly.LoadFrom + GetExportedTypes InnerLoad(仅该 Catalog) 扫描目录找模块
6 DirectoryModuleCatalog.CreateModuleInfo (L166, L192) CustomAttributeData.GetCustomAttributes 同上 读 [Module] / [ModuleDependency]
7 AssemblyResolver.LoadAssemblyFrom (L48) AssemblyName.GetAssemblyName 登记时 只读元数据,不加载

默认路径只碰 #1、#2、#3。 #4-#7 是可选能力。

再强调一次:AddModule<T>() 里的 typeof(T).AssemblyQualifiedName 是编译期常量表达式,不是反射。


13. 设计取舍分析

13.1 为什么不用"启动时扫描所有程序集"

对比方案:

方案 优点 缺点
反射扫描全部程序集(MEF 风格) 零配置、放进去就能用 启动慢;无法控制加载顺序;程序集被意外加载;AOT/裁剪不友好
Prism 的元数据登记 启动快;顺序可控;可按需加载;可 AOT 需要写 ConfigureModuleCatalog

13.2 为什么 ModuleType 存字符串

  • 支持"程序集不在内存里也能建清单"→ 按需加载的前提
  • 支持配置文件/XAML 里写类型名 → 运维可改
  • 代价:字符串拼错只能运行时发现(Type.GetType 返回 null → ModuleInitializeException)

如果改成 Type,DirectoryModuleCatalog 和 ConfigurationModuleCatalog 都得改设计。这个决定是"可配置性"优先于"编译期安全"的典型权衡。

13.3 为什么用 while + break 而不是递归

LoadModulesThatAreReadyForLoad 的写法:

csharp 复制代码
while (keepLoading)
{
    keepLoading = false;
    foreach (...) { if (ready) { Initialize(); keepLoading = true; break; } }
}

如果改成递归调用,依赖链一长就爆栈(虽然实际不会那么长)。更重要的是:break 后重建 availableModules 可以保证集合不被 Initialize 过程中的修改影响(Initialize 里可能调用 AddModule 动态加模块)。

13.4 为什么模块加载是同步的

FileModuleTypeLoader.LoadModuleType 是同步的(尽管有 progress 事件)。ModuleManager 的注释(L209-L211)明确说:

This callback may call back on the UI thread, but we are not guaranteeing it. If you were to add a custom retriever that retrieved in the background, you would need to consider dispatching to the UI thread.

也就是说:框架假设你在 UI 线程调 Run(),并且加载器同步回调。 自定义异步加载器必须自己处理线程调度 ------ 而 ModuleInfo.State 是无锁的,异步场景下可能出现竞态。这是 Prism 模块化最需要注意的边界。


14. 实战:正确用法与常见坑

14.1 标准写法

csharp 复制代码
// 1. 模块项目引用 Prism.Wpf
public class OrderModule : IModule
{
    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterForNavigation<OrderView, OrderViewModel>();
        containerRegistry.RegisterSingleton<IOrderService, OrderService>();
    }

    public void OnInitialized(IContainerProvider containerProvider)
    {
        var regionManager = containerProvider.Resolve<IRegionManager>();
        regionManager.RegisterViewWithRegion("MainRegion", typeof(OrderView));
    }
}
csharp 复制代码
// 2. 主程序登记
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    moduleCatalog.AddModule<OrderModule>(InitializationMode.WhenAvailable);
    moduleCatalog.AddModule<ReportModule>(InitializationMode.OnDemand);   // 按需
}
csharp 复制代码
// 3. 按需触发(通常登录后)
_moduleManager.LoadModule("ReportModule");
// 或泛型版
_moduleManager.LoadModule<ReportModule>();

14.2 常见坑

现象 原因 处理
模块死活不初始化 依赖模块初始化失败,AreDependenciesLoaded 永远 false 看 LoadModuleCompleted 事件里的 Error
ModuleInitializeException: 无法获取类型 xxx ModuleType 字符串版本号不匹配 / DLL 没复制到输出目录 用 typeof 而非手写字符串;检查 MSBuild 复制
启动即抛 ModularityException(Startup module depends on an OnDemand module) WhenAvailable 依赖 OnDemand 把被依赖的也改成 WhenAvailable
模块 RegisterTypes 里的注册不生效 在 ConfigureModuleCatalog 之前就 Resolve 了该类型 模块注册晚于 RegisterTypes(container),需要延迟解析
同一事件被触发多次 自定义 IModuleTypeLoader 未去重订阅 框架已用 HashSet 处理,自定义 loader 要注意
Ref 路径写相对路径无效 CanLoadModuleType 要求以 file:// 开头 用 GetFileAbsoluteUri 转换,或直接给绝对 URI
调试时改了 DLL 但没生效 Assembly.LoadFrom 后程序集锁定,且不可卸载 重启调试;或改用项目引用方式

14.3 模块 vs 直接注册的边界判断

  • 模块不会 带来性能收益(Resolve 开销一样)
  • 模块的唯一实质价值是:加载时机可控 + 依赖关系声明 + 可选部署单元
  • 如果一个"模块"在启动时必定加载、且与主程序同生命周期,那它其实不需要做成模块 ------ 直接在 RegisterTypes 里注册更简单

15. 调试与练习建议

15.1 断点设置(按执行顺序)

顺序 文件:行 观察什么
1 PrismApplicationBase.cs:62 CreateModuleCatalog 返回什么类型
2 IModuleCatalogExtensions.cs:37 AssemblyQualifiedName 长什么样
3 ModuleManager.cs:69 Run() 被调用
4 ModuleCatalogBase.cs:157 Validate 四项检查逐个过
5 ModuleManager.cs:106 Type.GetType 返回 null 还是 Type
6 ModuleManager.cs:171 break 后重扫的循环次数
7 ModuleInitializer.cs:109 类型解析成功
8 ModuleInitializer.cs:115 容器返回的实例类型
9 ModuleInitializer.cs:39-40 模块自己的两个钩子

15.2 递进练习

  1. 最小验证 :把 e2e/Wpf/HelloWorld 跑起来,在第 5 章的各断点走一遍
  2. 加依赖 :加一个 ModuleB 依赖 ModuleA,观察 LoadModulesThatAreReadyForLoad 的循环次数变化
  3. 故意制造循环依赖 :A 依赖 B、B 依赖 A,看 CyclicDependencyFoundException 在哪抛出
  4. 故意制造 WhenAvailable→OnDemand:验证第 ④ 项校验,理解为什么禁止
  5. 改成 OnDemand :手动 LoadModule,观察状态机是否跳过 LoadingTypes
  6. 换 DirectoryModuleCatalog :按 e2e/Wpf/HelloWorld/App.xaml.cs L35-L42 的注释操作(注释掉 ConfigureModuleCatalog + 移除项目引用 + 后置生成任务),这是唯一会走反射扫描的路径
  7. 写自定义 Loader :实现一个 IModuleTypeLoader,只处理 http:// 前缀,覆写 ModuleTypeLoaders 属性
  8. 写自定义 Catalog :继承 ModuleCatalog 覆写 InnerLoad,从 JSON 读模块清单(注意 Ref 要转 file://)

15.3 值得单独研究的点

  • ModuleInfoGroup(src/Wpf/Prism.Wpf/Modularity/ModuleInfoGroup.cs,16KB):一个 DLL 多个模块、共享 Ref 的场景
  • 异常体系:ModuleInitializeException / ModuleTypeLoadingException / ModuleNotFoundException / DuplicateModuleException / CyclicDependencyFoundException 的分工
  • src/Forms/Prism.Forms/Modularity/ModuleManager.cs(5KB):MAUI 版精简实现,对比能看出"哪些是 WPF 特有的、哪些是核心的"
  • 分组校验:ModuleCatalogBase.ValidateCrossGroupDependencies 与 IModuleInfoGroup(src/Prism.Core/Modularity/IModuleInfoGroup.cs)的配合

16. 附:关键 API 速查

16.1 登记模块

csharp 复制代码
// 泛型(推荐)
moduleCatalog.AddModule<TModule>();
moduleCatalog.AddModule<TModule>(InitializationMode.OnDemand);
moduleCatalog.AddModule<TModule>("自定义名", InitializationMode.WhenAvailable, "依赖模块名1", "依赖模块名2");

// 非泛型
moduleCatalog.AddModule(new ModuleInfo(typeof(TModule), "名字", InitializationMode.WhenAvailable));

// 分组(共享同一个 DLL 的多个模块)
moduleCatalog.AddGroup(InitializationMode.WhenAvailable, "file:///path/to/Modules.dll",
    new ModuleInfo("A", "Ns.AModule, Assembly"),
    new ModuleInfo("B", "Ns.BModule, Assembly"));

16.2 查询状态

csharp 复制代码
moduleManager.ModuleExists<TModule>();
moduleManager.GetModuleState<TModule>();      // NotStarted / LoadingTypes / ReadyForInitialization / Initializing / Initialized
moduleManager.IsModuleInitialized<TModule>();
moduleCatalog.Exists<TModule>();
moduleCatalog.GetModuleState("模块名");

16.3 扩展点清单

扩展点 基类/接口 用途
自定义清单来源 继承 ModuleCatalog 覆写 InnerLoad() JSON / 数据库 / 远程接口
自定义加载器 实现 IModuleTypeLoader + 覆写 ModuleManager.ModuleTypeLoaders HTTP / 压缩包 / 加密 DLL
自定义初始化 继承 ModuleInitializer 覆写 CreateModule 日志、计时、AOP
自定义调度 继承 ModuleManager 覆写 LoadModulesThatAreReadyForLoad 并发加载、优先级
自定义校验 继承 ModuleCatalog 覆写 Validate() 业务级约束
拦截加载结果 订阅 IModuleManager.LoadModuleCompleted 失败兜底 UI

17. 一页总结

复制代码
Prism 模块化 = 一张清单(Catalog)+ 一个调度器(ModuleManager)+ 一个加载器(TypeLoader)+ 一个初始化器(Initializer)

清单里存的是字符串元数据,不是 Type
    ↓ 所以程序集可以先不加载
调度器按拓扑排序 + 依赖就绪判断,逐个推进
    ↓ 必要时才触发加载器去 Assembly.LoadFrom
初始化器把字符串变 Type、Type 变实例(走容器)
    ↓ 最后只做两件事:RegisterTypes / OnInitialized

反射只发生在边界上 :字符串 ←→ Type 的两次转换、以及容器构造实例。核心的依赖管理、状态机、调度循环,全是普通 C# 集合操作 + 图算法,没有一处反射。

这就是为什么 Prism 的模块化在 AOT / 裁剪场景下也能用 ------ 只要你不用 DirectoryModuleCatalog、不手写类型字符串。默认的 AddModule<T>() 路径是完全可静态分析的。


相关推荐
光泽雨3 天前
动画生命周期
c#·wpf
nap1174 天前
WPF 中常用的 LINQ 语句
wpf
做个文艺程序员7 天前
MQ第12篇:AI原生消息架构演进:从MQ到事件驱动的全链路设计
架构·消息队列·wpf·事件驱动架构·ai-native·cqrs
donk13149 天前
React Native for OpenHarmony 实战:三方库 react-native-circular-action-menu 的鸿蒙化适配指南
javascript·分布式·react native·react.js·wpf·harmonyos
donk131410 天前
React Native for OpenHarmony 实战:三方库 react-native-step-indicator 的鸿蒙化适配指南
分布式·react native·react.js·wpf·harmonyos
He BianGu10 天前
【笔记】WPF中RenderOptions类的详细功能
笔记·wpf
VidDown10 天前
上线三个月后的第一次事故:一个 Django 服务的日志、监控与故障复盘
数据库·python·django·wpf·音视频·视频编解码
He BianGu10 天前
【笔记】WPF中CachingHint的功能
笔记·wpf