并行 mod 发现

基本信息

属性 值
类 com.mitchej123.jarjar.discovery.ParallellModDiscoverer
父类 cpw.mods.fml.common.discovery.ModDiscoverer
线程池 DiscoveryPool(共享 ForkJoinPool)
任务类型 ModScanTask extends RecursiveTask<ModCandidateV2>
来源 Fabric ModDiscoverer (Apache 2.0 / LGPL v3)
取代 原生串行 ModDiscoverer

ParallellModDiscoverer 是 JarJar 提速 Mod 发现的核心:从 Fabric-loader 移植并适配 FML。把"扫文件 → 解析 manifest → 抽 Jar-in-Jar → ASM 扫描所有 .class"全部异步化,主线程只在 future.get() 处收割。

构造函数

public ParallellModDiscoverer(File modsDir, ModClassLoader modClassLoader) {
    this.mcDir = modsDir;
    candidateFinders.add(new ClasspathModCandidateFinder(modClassLoader));
    candidateFinders.add(new DirectoryModCandidateFinder(
        modsDir.toPath(),
        ModListHelper.additionalMods.values().toArray(new File[0]),
        true));
    File versionSpecificModsDir = new File(modsDir, FMLInjectionData.mccversion);
    if (versionSpecificModsDir.isDirectory()) {
        candidateFinders.add(new DirectoryModCandidateFinder(versionSpecificModsDir.toPath(), true));
    }
}

三类 finder:

  • ClasspathModCandidateFinder - Minecraft jar 与已加载库
  • DirectoryModCandidateFinder - <mc>/mods/ + ModListHelper.additionalMods
  • DirectoryModCandidateFinder - <mc>/<mccversion>/ (Forge 1.7.10 时代给特定 MC 版本提供的子目录)

discoverMods 流程

ForkJoinPool pool = DiscoveryPool.get();
Set<Path> processedPaths = new HashSet<>();   // 抑制同 path 重复入队
List<Future<ModCandidateV2>> futures = new ArrayList<>();

ModCandidateFinder.ModCandidateConsumer taskSubmitter = (paths, requiresRemap, isMinecraft, isClasspath) -> {
    List<Path> pendingPaths = new ArrayList<>(paths.size());
    for (Path path : paths) {
        assert path.equals(LoaderUtil.normalizeExistingPath(path));
        if (processedPaths.add(path)) pendingPaths.add(path);
    }
    if (!pendingPaths.isEmpty()) futures.add(pool.submit(new ModScanTask(pendingPaths, requiresRemap, isMinecraft, isClasspath)));
};

for (ModCandidateFinder finder : candidateFinders) {
    finder.findCandidates(taskSubmitter);
}

主线程遍历 finder,每个 finder 提交的 path 经 pathNormalizationCache 归一化(path.toRealPath())后入队。

收割

int timeout = Integer.getInteger(JARJAR_DEBUG_DISCOVERY_TIMEOUT, -1);
if (timeout <= 0) timeout = Integer.MAX_VALUE;

for (Future<ModCandidateV2> future : futures) {
    ModCandidateV2 candidate = future.get(timeout, TimeUnit.SECONDS);
    if (candidate != null) candidates.add(candidate);
}
// 然后处理嵌套 mod 的 future

超时抛 RuntimeException,包含 jarjar.debug.discoveryTimeout JVM 属性提示。

ModScanTask

每个 task 扫描单个 jar:

  1. try (JarFile jar = new JarFile(modFile))
  2. JarUtil.examineJarCandidate(jar, modFile, null, false, isMinecraft, isClasspath):读 manifest / mcmod.info / coremod / AT / nested jars
  3. 若 modCandidate.hasNestedJars():用 jijDedupMap 去重 fork;首个任务 localTask 不 fork,留本线程
  4. 枚举 jar.entries():跳过 __MACOSX / META-INF/versions/ / module-info.class / $.class
  5. 对每个 .class 用 ASMModParserV2 解析(new ASMModParserV2(classBytes, entryName))
  6. 通过 ModContainerFactoryV2.buildV2(...) 转 ModContainerWrapper,加入 wrappedModList + modList
  7. 把所有 ASMModParserV2 加入 asmDataCollection,等 sendToTable 时统一发送给 ASMDataTable

生命周期收尾

modCandidates.addAll(ret);
modCandidates.sort(Comparator.comparing(candidate -> candidate.getModContainer().getName()));
for (ModCandidateV2 c : modCandidates) {
    c.releaseNestedJars();    // 释放 NestedJar 内存引用
}
candidateFinders.clear();
jijDedupMap.clear();
nestedModInitDatas.clear();

LoaderV2.identifyMods 在 Discoverer.getModCandidates() 之后调用 releaseParsedData(),丢弃 ASM 字节缓存与 MetadataCollection 引用。

超时

throw new RuntimeException(String.format("Mod discovery took too long! ... The timeout can be changed with the system property %s (-D%<s=<desired timeout in seconds>).",
    timeout, JARJAR_DEBUG_DISCOVERY_TIMEOUT));

相关条目