WPF Prism代码走读

本文深入剖析Prism 8.1.97模块化(Modularity)核心机制,聚焦ModuleManager、ModuleCatalogBase、ModuleDependencySolver等关键组件。重点揭示其元数据驱动+延迟反射+容器实例化的三层设计,澄清“是否依赖反射”的常见误解;详解拓扑排序依赖解析、OnDemand/WhenAvailable加载策略、字符串类型存储的设计取舍,以及FileModuleTypeLoader的扫描机制与安全考量。

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 两个钩子
模块元数据IModuleInfoModuleInfo存名字、类型全名、依赖、DLL 路径、状态
模块清单IModuleCatalogModuleCatalog 及 3 个子类收集元数据 + 校验(重复/循环/缺失)
调度器IModuleManagerModuleManager决定"什么时候、按什么顺序"加载谁
类型加载器IModuleTypeLoaderFileModuleTypeLoader把 DLL 弄进 AppDomain
初始化器IModuleInitializerModuleInitializer类型 → 实例 → 调两个钩子

依赖方向是单向的:

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.csModuleExists<T> / IsModuleInitialized<T> / LoadModule<T>
src/Prism.Core/Modularity/IModuleCatalogCoreExtensions.csExists<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.csAddModule<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(全文)

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)

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 存字符串:

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)

protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    moduleCatalog.AddModule<ModuleAModule>();
}

4.2 框架侧:PrismApplicationBase.Initialize

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

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):

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)

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):

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

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

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

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

5.3 LoadModulesWhenAvailable:筛选入口

L118-L126

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

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

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

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

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):

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):

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

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,全文件最值得看的一段

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):

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

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

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

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

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

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

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

    Validated = true;
}

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

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

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

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

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

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

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

L38-L51

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

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)找的是"当前没有任何未处理模块依赖它"的节点:

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
轮次skipFindLeaves 判定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

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):

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,去掉了异常处理细节):

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):

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):

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 反序列化)清单可视化编辑
ConfigurationModuleCatalogapp.config 的 <modules> 节否(读 ConfigurationSection)运维可改配置

10.1 ModuleCatalog(默认)

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

10.2 DirectoryModuleCatalog:真正的反射扫描

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

InnerLoad(L33-L73)流程:

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):

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):

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:

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> 配置节:

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)

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时机作用
1ModuleManager.ModuleNeedsRetrieval (L106)Type.GetType(string)每次模块加载前探测程序集是否已在内存
2ModuleInitializer.CreateModule (L109)Type.GetType(string)每次模块实例化前字符串 → Type
3ModuleInitializer.CreateModule (L115)container.Resolve(Type)同上容器反射构造实例
4AssemblyResolver.CurrentDomain_AssemblyResolve (L92)Assembly.LoadFromCLR 解析类型失败时真正的 DLL 加载
5DirectoryModuleCatalog (L111, L119-L124)Assembly.LoadFrom + GetExportedTypesInnerLoad(仅该 Catalog)扫描目录找模块
6DirectoryModuleCatalog.CreateModuleInfo (L166, L192)CustomAttributeData.GetCustomAttributes同上读 [Module] / [ModuleDependency]
7AssemblyResolver.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 的写法:

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 标准写法

// 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));
    }
}
// 2. 主程序登记
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    moduleCatalog.AddModule<OrderModule>(InitializationMode.WhenAvailable);
    moduleCatalog.AddModule<ReportModule>(InitializationMode.OnDemand);   // 按需
}
// 3. 按需触发(通常登录后)
_moduleManager.LoadModule("ReportModule");
// 或泛型版
_moduleManager.LoadModule<ReportModule>();

14.2 常见坑

现象原因处理
模块死活不初始化依赖模块初始化失败,AreDependenciesLoaded 永远 false看 LoadModuleCompleted 事件里的 Error
ModuleInitializeException: 无法获取类型 xxxModuleType 字符串版本号不匹配 / 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 断点设置(按执行顺序)

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

// 泛型(推荐)
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 查询状态

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.ModuleTypeLoadersHTTP / 压缩包 / 加密 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>() 路径是完全可静态分析的。


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值