Skip to content

Commit b1402d3

Browse files
authored
优化下载页面图标缓存功能 (#5211)
1 parent 62f0d0a commit b1402d3

2 files changed

Lines changed: 125 additions & 69 deletions

File tree

HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java

Lines changed: 10 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -56,29 +56,20 @@
5656
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
5757
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
5858
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
59-
import org.jackhuang.hmcl.util.AggregatedObservableList;
60-
import org.jackhuang.hmcl.util.Holder;
61-
import org.jackhuang.hmcl.util.Lang;
62-
import org.jackhuang.hmcl.util.StringUtils;
59+
import org.jackhuang.hmcl.util.*;
6360
import org.jackhuang.hmcl.util.i18n.I18n;
64-
import org.jackhuang.hmcl.util.io.NetworkUtils;
6561
import org.jackhuang.hmcl.util.javafx.BindingMapping;
6662
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
6763
import org.jetbrains.annotations.NotNull;
6864

69-
import java.lang.ref.WeakReference;
7065
import java.net.URI;
7166
import java.util.*;
72-
import java.util.concurrent.CancellationException;
73-
import java.util.concurrent.CompletableFuture;
74-
import java.util.concurrent.CompletionException;
7567
import java.util.stream.Collectors;
7668

7769
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
7870
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
7971
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
8072
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
81-
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
8273

8374
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
8475
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
@@ -247,6 +238,12 @@ protected Skin<?> createDefaultSkin() {
247238

248239
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
249240
private final JFXListView<RemoteMod> listView = new JFXListView<>();
241+
private final RemoteImageLoader iconLoader = new RemoteImageLoader() {
242+
@Override
243+
protected @NotNull Task<Image> createLoadTask(@NotNull URI uri) {
244+
return FXUtils.getRemoteImageTask(uri, 80, 80, true, true);
245+
}
246+
};
250247

251248
protected ModDownloadListPageSkin(DownloadListPage control) {
252249
super(control);
@@ -377,6 +374,7 @@ protected ModDownloadListPageSkin(DownloadListPage control) {
377374
IntegerProperty filterID = new SimpleIntegerProperty(this, "Filter ID", 0);
378375
IntegerProperty currentFilterID = new SimpleIntegerProperty(this, "Current Filter ID", -1);
379376
EventHandler<ActionEvent> searchAction = e -> {
377+
iconLoader.clearInvalidCache();
380378
if (currentFilterID.get() != -1 && currentFilterID.get() != filterID.get()) {
381379
control.pageOffset.set(0);
382380
}
@@ -536,7 +534,7 @@ protected ModDownloadListPageSkin(DownloadListPage control) {
536534

537535
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
538536
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
539-
var iconCache = new WeakHashMap<String, WeakReference<CompletableFuture<Image>>>();
537+
540538
listView.setCellFactory(x -> new FloatListCell<>(listView) {
541539
private final TwoLineListItem content = new TwoLineListItem();
542540
private final ImageView imageView = new ImageView();
@@ -564,66 +562,9 @@ protected void updateControl(RemoteMod dataItem, boolean empty) {
564562
if (getSkinnable().shouldDisplayCategory(category))
565563
content.addTag(getSkinnable().getLocalizedCategory(category));
566564
}
567-
loadIcon(dataItem);
565+
iconLoader.load(imageView.imageProperty(), dataItem.getIconUrl());
568566
}
569567

570-
private void loadIcon(RemoteMod mod) {
571-
if (StringUtils.isBlank(mod.getIconUrl())) {
572-
imageView.setImage(null);
573-
return;
574-
}
575-
576-
WeakReference<CompletableFuture<Image>> cacheRef = iconCache.get(mod.getIconUrl());
577-
CompletableFuture<Image> cache;
578-
if (cacheRef != null && (cache = cacheRef.get()) != null) {
579-
loadIcon(cache, mod.getIconUrl());
580-
return;
581-
}
582-
583-
URI iconUrl = NetworkUtils.toURIOrNull(mod.getIconUrl());
584-
if (iconUrl == null) {
585-
imageView.setImage(null);
586-
return;
587-
}
588-
589-
CompletableFuture<Image> future = new CompletableFuture<>();
590-
WeakReference<CompletableFuture<Image>> futureRef = new WeakReference<>(future);
591-
iconCache.put(mod.getIconUrl(), futureRef);
592-
593-
FXUtils.getRemoteImageTask(iconUrl, 80, 80, true, true)
594-
.whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
595-
if (exception == null) {
596-
future.complete(result);
597-
} else {
598-
LOG.warning("Failed to load image from " + iconUrl, exception);
599-
future.completeExceptionally(exception);
600-
}
601-
}).start();
602-
loadIcon(future, mod.getIconUrl());
603-
}
604-
605-
private void loadIcon(@NotNull CompletableFuture<Image> future,
606-
@NotNull String iconUrl) {
607-
Image image;
608-
try {
609-
image = future.getNow(null);
610-
} catch (CancellationException | CompletionException ignored) {
611-
imageView.setImage(null);
612-
return;
613-
}
614-
615-
if (image != null) {
616-
imageView.setImage(image);
617-
} else {
618-
imageView.setImage(null);
619-
future.thenAcceptAsync(result -> {
620-
RemoteMod item = getItem();
621-
if (item != null && iconUrl.equals(item.getIconUrl())) {
622-
this.imageView.setImage(result);
623-
}
624-
}, Schedulers.javafx());
625-
}
626-
}
627568
});
628569
}
629570

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package org.jackhuang.hmcl.util;
19+
20+
import javafx.beans.value.WritableValue;
21+
import javafx.scene.image.Image;
22+
import org.jackhuang.hmcl.task.Schedulers;
23+
import org.jackhuang.hmcl.task.Task;
24+
import org.jackhuang.hmcl.util.io.NetworkUtils;
25+
import org.jetbrains.annotations.NotNull;
26+
import org.jetbrains.annotations.Nullable;
27+
28+
import java.lang.ref.WeakReference;
29+
import java.net.URI;
30+
import java.util.*;
31+
32+
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
33+
34+
/// @author Glavo
35+
public abstract class RemoteImageLoader {
36+
private final Map<URI, WeakReference<Image>> cache = new HashMap<>();
37+
private final Map<URI, List<WeakReference<WritableValue<Image>>>> pendingRequests = new HashMap<>();
38+
private final WeakHashMap<WritableValue<Image>, URI> reverseLookup = new WeakHashMap<>();
39+
40+
public RemoteImageLoader() {
41+
}
42+
43+
protected @Nullable Image getPlaceholder() {
44+
return null;
45+
}
46+
47+
protected abstract @NotNull Task<Image> createLoadTask(@NotNull URI uri);
48+
49+
@FXThread
50+
public void load(@NotNull WritableValue<Image> writableValue, String url) {
51+
URI uri = NetworkUtils.toURIOrNull(url);
52+
if (uri == null) {
53+
reverseLookup.remove(writableValue);
54+
writableValue.setValue(getPlaceholder());
55+
return;
56+
}
57+
58+
WeakReference<Image> reference = cache.get(uri);
59+
if (reference != null) {
60+
Image image = reference.get();
61+
if (image != null) {
62+
reverseLookup.remove(writableValue);
63+
writableValue.setValue(image);
64+
return;
65+
}
66+
cache.remove(uri);
67+
}
68+
69+
{
70+
List<WeakReference<WritableValue<Image>>> list = pendingRequests.get(uri);
71+
if (list != null) {
72+
list.add(new WeakReference<>(writableValue));
73+
reverseLookup.put(writableValue, uri);
74+
return;
75+
} else {
76+
list = new ArrayList<>(1);
77+
list.add(new WeakReference<>(writableValue));
78+
pendingRequests.put(uri, list);
79+
reverseLookup.put(writableValue, uri);
80+
}
81+
}
82+
83+
createLoadTask(uri).whenComplete(Schedulers.javafx(), (result, exception) -> {
84+
Image image;
85+
if (exception == null) {
86+
image = result;
87+
} else {
88+
LOG.warning("Failed to load image from " + uri, exception);
89+
image = getPlaceholder();
90+
}
91+
92+
cache.put(uri, new WeakReference<>(image));
93+
List<WeakReference<WritableValue<Image>>> list = pendingRequests.remove(uri);
94+
if (list != null) {
95+
for (WeakReference<WritableValue<Image>> ref : list) {
96+
WritableValue<Image> target = ref.get();
97+
if (target != null && uri.equals(reverseLookup.get(target))) {
98+
reverseLookup.remove(target);
99+
target.setValue(image);
100+
}
101+
}
102+
}
103+
}).start();
104+
}
105+
106+
@FXThread
107+
public void unload(@NotNull WritableValue<Image> writableValue) {
108+
reverseLookup.remove(writableValue);
109+
}
110+
111+
@FXThread
112+
public void clearInvalidCache() {
113+
cache.entrySet().removeIf(entry -> entry.getValue().get() == null);
114+
}
115+
}

0 commit comments

Comments
 (0)