fix(gui): add memory and time limits for decompile task (#1181)
This commit is contained in:
@@ -6,8 +6,9 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.SwingWorker;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@@ -15,6 +16,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.ProgressPanel;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
/**
|
||||
* Class for run tasks in background with progress bar indication.
|
||||
@@ -34,7 +36,7 @@ public class BackgroundExecutor {
|
||||
this.taskQueueExecutor = makeTaskQueueExecutor();
|
||||
}
|
||||
|
||||
public Future<Boolean> execute(IBackgroundTask task) {
|
||||
public Future<TaskStatus> execute(IBackgroundTask task) {
|
||||
TaskWorker taskWorker = new TaskWorker(task);
|
||||
taskQueueExecutor.execute(() -> {
|
||||
taskWorker.init();
|
||||
@@ -46,7 +48,8 @@ public class BackgroundExecutor {
|
||||
public void cancelAll() {
|
||||
try {
|
||||
taskQueueExecutor.shutdownNow();
|
||||
taskQueueExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
boolean complete = taskQueueExecutor.awaitTermination(2, TimeUnit.SECONDS);
|
||||
LOG.debug("Background task executor terminated with status: {}", complete ? "complete" : "interrupted");
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error terminating task executor", e);
|
||||
} finally {
|
||||
@@ -58,25 +61,18 @@ public class BackgroundExecutor {
|
||||
execute(new SimpleTask(title, backgroundJobs, onFinishUiRunnable));
|
||||
}
|
||||
|
||||
public void execute(String title, List<Runnable> backgroundJobs) {
|
||||
execute(new SimpleTask(title, backgroundJobs, null));
|
||||
}
|
||||
|
||||
public void execute(String title, Runnable backgroundRunnable, Runnable onFinishUiRunnable) {
|
||||
execute(new SimpleTask(title, backgroundRunnable, onFinishUiRunnable));
|
||||
}
|
||||
|
||||
public void execute(String title, Runnable backgroundRunnable) {
|
||||
execute(new SimpleTask(title, backgroundRunnable, null));
|
||||
}
|
||||
|
||||
private ThreadPoolExecutor makeTaskQueueExecutor() {
|
||||
return (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
|
||||
}
|
||||
|
||||
private final class TaskWorker extends SwingWorker<Boolean, Void> {
|
||||
private final class TaskWorker extends SwingWorker<TaskStatus, Void> {
|
||||
private final IBackgroundTask task;
|
||||
private long jobsCount;
|
||||
private TaskStatus status = TaskStatus.WAIT;
|
||||
|
||||
public TaskWorker(IBackgroundTask task) {
|
||||
this.task = task;
|
||||
@@ -88,39 +84,24 @@ public class BackgroundExecutor {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground() throws Exception {
|
||||
protected TaskStatus doInBackground() throws Exception {
|
||||
progressPane.changeLabel(this, task.getTitle() + "… ");
|
||||
progressPane.changeCancelBtnVisible(this, task.canBeCanceled());
|
||||
progressPane.changeVisibility(this, true);
|
||||
|
||||
if (runJobs()) {
|
||||
status = TaskStatus.COMPLETE;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private boolean runJobs() throws InterruptedException {
|
||||
List<Runnable> jobs = task.scheduleJobs();
|
||||
jobsCount = jobs.size();
|
||||
LOG.debug("Starting background task '{}', jobs count: {}", task.getTitle(), jobsCount);
|
||||
if (jobsCount == 1) {
|
||||
jobs.get(0).run();
|
||||
return true;
|
||||
}
|
||||
LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}",
|
||||
task.getTitle(), jobsCount, task.timeLimit(), task.checkMemoryUsage());
|
||||
status = TaskStatus.STARTED;
|
||||
int threadsCount = mainWindow.getSettings().getThreadsCount();
|
||||
if (threadsCount == 1) {
|
||||
return runInCurrentThread(jobs);
|
||||
}
|
||||
return runInExecutor(jobs, threadsCount);
|
||||
}
|
||||
|
||||
private boolean runInCurrentThread(List<Runnable> jobs) {
|
||||
int k = 0;
|
||||
for (Runnable job : jobs) {
|
||||
job.run();
|
||||
k++;
|
||||
setProgress(calcProgress(k));
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean runInExecutor(List<Runnable> jobs, int threadsCount) throws InterruptedException {
|
||||
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount);
|
||||
for (Runnable job : jobs) {
|
||||
executor.execute(job);
|
||||
@@ -129,22 +110,74 @@ public class BackgroundExecutor {
|
||||
return waitTermination(executor);
|
||||
}
|
||||
|
||||
@SuppressWarnings("BusyWait")
|
||||
private boolean waitTermination(ThreadPoolExecutor executor) throws InterruptedException {
|
||||
while (true) {
|
||||
if (executor.isTerminated()) {
|
||||
BooleanSupplier cancelCheck = buildCancelCheck();
|
||||
try {
|
||||
while (true) {
|
||||
if (executor.isTerminated()) {
|
||||
return true;
|
||||
}
|
||||
if (cancelCheck.getAsBoolean()) {
|
||||
performCancel(executor);
|
||||
return false;
|
||||
}
|
||||
setProgress(calcProgress(executor.getCompletedTaskCount()));
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.debug("Task wait interrupted");
|
||||
status = TaskStatus.CANCEL_BY_USER;
|
||||
performCancel(executor);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Task wait aborted by exception", e);
|
||||
performCancel(executor);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void performCancel(ThreadPoolExecutor executor) throws InterruptedException {
|
||||
progressPane.changeLabel(this, task.getTitle() + " (Canceling)… ");
|
||||
progressPane.changeIndeterminate(this, true);
|
||||
// force termination
|
||||
executor.shutdownNow();
|
||||
boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
LOG.debug("Task cancel complete: {}", complete);
|
||||
}
|
||||
|
||||
private boolean isSimpleTask() {
|
||||
return task.timeLimit() == 0 && !task.checkMemoryUsage();
|
||||
}
|
||||
|
||||
private boolean simpleCancelCheck() {
|
||||
if (isCancelled() || Thread.currentThread().isInterrupted()) {
|
||||
LOG.debug("Task '{}' canceled", task.getTitle());
|
||||
status = TaskStatus.CANCEL_BY_USER;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private BooleanSupplier buildCancelCheck() {
|
||||
if (isSimpleTask()) {
|
||||
return this::simpleCancelCheck;
|
||||
}
|
||||
long waitUntilTime = task.timeLimit() == 0 ? 0 : System.currentTimeMillis() + task.timeLimit();
|
||||
boolean checkMemoryUsage = task.checkMemoryUsage();
|
||||
return () -> {
|
||||
if (waitUntilTime != 0 && waitUntilTime < System.currentTimeMillis()) {
|
||||
LOG.debug("Task '{}' execution timeout, force cancel", task.getTitle());
|
||||
status = TaskStatus.CANCEL_BY_TIMEOUT;
|
||||
return true;
|
||||
}
|
||||
if (isCancelled()) {
|
||||
executor.shutdownNow();
|
||||
progressPane.changeLabel(this, task.getTitle() + " (Canceling)… ");
|
||||
progressPane.changeIndeterminate(this, true);
|
||||
// force termination
|
||||
executor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
return false;
|
||||
if (checkMemoryUsage && !UiUtils.isFreeMemoryAvailable()) {
|
||||
LOG.debug("Task '{}' memory limit reached, force cancel", task.getTitle());
|
||||
status = TaskStatus.CANCEL_BY_MEMORY;
|
||||
return true;
|
||||
}
|
||||
setProgress(calcProgress(executor.getCompletedTaskCount()));
|
||||
Thread.sleep(500);
|
||||
}
|
||||
return simpleCancelCheck();
|
||||
};
|
||||
}
|
||||
|
||||
private int calcProgress(long done) {
|
||||
@@ -154,7 +187,7 @@ public class BackgroundExecutor {
|
||||
@Override
|
||||
protected void done() {
|
||||
progressPane.setVisible(false);
|
||||
task.onFinish();
|
||||
task.onFinish(status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,12 +217,7 @@ public class BackgroundExecutor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBeCanceled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
public void onFinish(TaskStatus status) {
|
||||
if (onFinish != null) {
|
||||
onFinish.run();
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package jadx.gui.jobs;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.JadxWrapper;
|
||||
|
||||
public abstract class BackgroundJob {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BackgroundJob.class);
|
||||
|
||||
protected final JadxWrapper wrapper;
|
||||
private final ThreadPoolExecutor executor;
|
||||
private Future<Boolean> future;
|
||||
|
||||
public BackgroundJob(JadxWrapper wrapper, int threadsCount) {
|
||||
this.wrapper = wrapper;
|
||||
this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount);
|
||||
}
|
||||
|
||||
public synchronized Future<Boolean> process() {
|
||||
if (future != null) {
|
||||
return future;
|
||||
}
|
||||
ExecutorService shutdownExecutor = Executors.newSingleThreadExecutor();
|
||||
FutureTask<Boolean> task = new ShutdownTask();
|
||||
shutdownExecutor.execute(task);
|
||||
shutdownExecutor.shutdown();
|
||||
future = task;
|
||||
return future;
|
||||
}
|
||||
|
||||
private class ShutdownTask extends FutureTask<Boolean> {
|
||||
public ShutdownTask() {
|
||||
super(new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
runJob();
|
||||
executor.shutdown();
|
||||
return executor.awaitTermination(5, TimeUnit.DAYS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel(boolean mayInterruptIfRunning) {
|
||||
executor.shutdownNow();
|
||||
return super.cancel(mayInterruptIfRunning);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void runJob();
|
||||
|
||||
public abstract String getInfoString();
|
||||
|
||||
protected void addTask(Runnable runnable) {
|
||||
executor.execute(runnable);
|
||||
}
|
||||
|
||||
public void processAndWait() {
|
||||
try {
|
||||
process().get();
|
||||
} catch (Exception e) {
|
||||
LOG.error("BackgroundJob.processAndWait failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isComplete() {
|
||||
try {
|
||||
return future != null && future.isDone();
|
||||
} catch (Exception e) {
|
||||
LOG.error("BackgroundJob.isComplete failed", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return (int) (executor.getCompletedTaskCount() * 100 / (double) executor.getTaskCount());
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package jadx.gui.jobs;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.SwingWorker;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.ui.ProgressPanel;
|
||||
import jadx.gui.utils.CacheObject;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.search.TextSearchIndex;
|
||||
|
||||
/**
|
||||
* Deprecated. Use {@link BackgroundExecutor} instead.
|
||||
*/
|
||||
public class BackgroundWorker extends SwingWorker<Void, Void> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BackgroundWorker.class);
|
||||
|
||||
private final CacheObject cache;
|
||||
private final ProgressPanel progressPane;
|
||||
|
||||
public BackgroundWorker(CacheObject cacheObject, ProgressPanel progressPane) {
|
||||
this.cache = cacheObject;
|
||||
this.progressPane = progressPane;
|
||||
}
|
||||
|
||||
public void exec() {
|
||||
if (isDone()) {
|
||||
return;
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> progressPane.setVisible(true));
|
||||
addPropertyChangeListener(progressPane);
|
||||
execute();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (isDone()) {
|
||||
return;
|
||||
}
|
||||
LOG.debug("Canceling background jobs ...");
|
||||
cancel(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
try {
|
||||
System.gc();
|
||||
LOG.debug("Memory usage: Before decompile: {}", UiUtils.memoryInfo());
|
||||
runJob(cache.getDecompileJob());
|
||||
LOG.debug("Memory usage: After decompile: {}", UiUtils.memoryInfo());
|
||||
|
||||
LOG.debug("Memory usage: Before index: {}", UiUtils.memoryInfo());
|
||||
runJob(cache.getIndexJob());
|
||||
LOG.debug("Memory usage: After index: {}", UiUtils.memoryInfo());
|
||||
|
||||
System.gc();
|
||||
LOG.debug("Memory usage: After gc: {}", UiUtils.memoryInfo());
|
||||
|
||||
TextSearchIndex searchIndex = cache.getTextIndex();
|
||||
if (searchIndex != null && searchIndex.getSkippedCount() > 0) {
|
||||
LOG.warn("Indexing of some classes skipped, count: {}, low memory: {}",
|
||||
searchIndex.getSkippedCount(), UiUtils.memoryInfo());
|
||||
String msg = NLS.str("message.indexingClassesSkipped", searchIndex.getSkippedCount());
|
||||
JOptionPane.showMessageDialog(null, msg);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Exception in background worker", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void runJob(BackgroundJob job) {
|
||||
if (isCancelled() || job == null) {
|
||||
return;
|
||||
}
|
||||
progressPane.changeLabel(this, job.getInfoString());
|
||||
Future<Boolean> future = job.process();
|
||||
while (!future.isDone()) {
|
||||
try {
|
||||
setProgress(job.getProgress());
|
||||
if (isCancelled()) {
|
||||
future.cancel(false);
|
||||
}
|
||||
Thread.sleep(500);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Background worker error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
progressPane.setVisible(false);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package jadx.gui.jobs;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.utils.NLS;
|
||||
|
||||
public class DecompileJob extends BackgroundJob {
|
||||
|
||||
public DecompileJob(JadxWrapper wrapper, int threadsCount) {
|
||||
super(wrapper, threadsCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void runJob() {
|
||||
for (final JavaClass cls : wrapper.getIncludedClasses()) {
|
||||
addTask(cls::decompile);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInfoString() {
|
||||
return NLS.str("progress.decompile") + "… ";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package jadx.gui.jobs;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
public class DecompileTask implements IBackgroundTask {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DecompileTask.class);
|
||||
|
||||
private static final int CLS_LIMIT = Integer.parseInt(UiUtils.getEnvVar("JADX_CLS_PROCESS_LIMIT", "50"));
|
||||
|
||||
private final MainWindow mainWindow;
|
||||
private final JadxWrapper wrapper;
|
||||
private final AtomicInteger complete = new AtomicInteger(0);
|
||||
private int expectedCompleteCount;
|
||||
private long startTime;
|
||||
|
||||
public DecompileTask(MainWindow mainWindow, JadxWrapper wrapper) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.wrapper = wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return NLS.str("progress.decompile");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> scheduleJobs() {
|
||||
List<JavaClass> classes = wrapper.getIncludedClasses();
|
||||
expectedCompleteCount = classes.size();
|
||||
|
||||
IndexService indexService = mainWindow.getCacheObject().getIndexService();
|
||||
indexService.setComplete(false);
|
||||
complete.set(0);
|
||||
|
||||
List<Runnable> jobs = new ArrayList<>(expectedCompleteCount + 1);
|
||||
for (JavaClass cls : classes) {
|
||||
jobs.add(() -> {
|
||||
cls.decompile();
|
||||
indexService.indexCls(cls);
|
||||
complete.incrementAndGet();
|
||||
});
|
||||
}
|
||||
jobs.add(indexService::indexResources);
|
||||
startTime = System.currentTimeMillis();
|
||||
return jobs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(TaskStatus status) {
|
||||
long taskTime = System.currentTimeMillis() - startTime;
|
||||
long avgPerCls = taskTime / expectedCompleteCount;
|
||||
LOG.info("Decompile task complete in {} ms (avg {} ms per class), classes: {},"
|
||||
+ " time limit:{ total: {}ms, per cls: {}ms }, status: {}",
|
||||
taskTime, avgPerCls, expectedCompleteCount, timeLimit(), CLS_LIMIT, status);
|
||||
|
||||
IndexService indexService = mainWindow.getCacheObject().getIndexService();
|
||||
indexService.setComplete(true);
|
||||
|
||||
int complete = this.complete.get();
|
||||
int skipped = expectedCompleteCount - complete;
|
||||
if (skipped == 0) {
|
||||
return;
|
||||
}
|
||||
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skipped, status);
|
||||
switch (status) {
|
||||
case CANCEL_BY_USER: {
|
||||
String reason = NLS.str("message.userCancelTask");
|
||||
String message = NLS.str("message.indexIncomplete", reason, skipped);
|
||||
JOptionPane.showMessageDialog(mainWindow, message);
|
||||
break;
|
||||
}
|
||||
case CANCEL_BY_TIMEOUT: {
|
||||
String reason = NLS.str("message.taskTimeout", timeLimit());
|
||||
String message = NLS.str("message.indexIncomplete", reason, skipped);
|
||||
JOptionPane.showMessageDialog(mainWindow, message);
|
||||
break;
|
||||
}
|
||||
case CANCEL_BY_MEMORY: {
|
||||
mainWindow.showHeapUsageBar();
|
||||
JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skipped));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBeCanceled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int timeLimit() {
|
||||
return expectedCompleteCount * CLS_LIMIT + 5000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkMemoryUsage() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,23 @@ public interface IBackgroundTask {
|
||||
|
||||
List<Runnable> scheduleJobs();
|
||||
|
||||
void onFinish();
|
||||
void onFinish(TaskStatus status);
|
||||
|
||||
boolean canBeCanceled();
|
||||
default boolean canBeCanceled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global (for all jobs) time limit in milliseconds (0 - to disable).
|
||||
*/
|
||||
default int timeLimit() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executor will check memory usage on every tick and cancel job if no free memory available.
|
||||
*/
|
||||
default boolean checkMemoryUsage() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-26
@@ -8,36 +8,25 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ICodeWriter;
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.utils.CacheObject;
|
||||
import jadx.gui.utils.CodeLinesInfo;
|
||||
import jadx.gui.utils.CodeUsageInfo;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.search.StringRef;
|
||||
import jadx.gui.utils.search.TextSearchIndex;
|
||||
|
||||
public class IndexJob extends BackgroundJob {
|
||||
public class IndexService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IndexJob.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IndexService.class);
|
||||
|
||||
private final CacheObject cache;
|
||||
private boolean indexComplete;
|
||||
|
||||
public IndexJob(JadxWrapper wrapper, CacheObject cache, int threadsCount) {
|
||||
super(wrapper, threadsCount);
|
||||
public IndexService(CacheObject cache) {
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void runJob() {
|
||||
TextSearchIndex index = cache.getTextIndex();
|
||||
addTask(index::indexResource);
|
||||
for (final JavaClass cls : wrapper.getIncludedClasses()) {
|
||||
addTask(() -> indexCls(cache, cls));
|
||||
}
|
||||
}
|
||||
|
||||
private static void indexCls(CacheObject cache, JavaClass cls) {
|
||||
public void indexCls(JavaClass cls) {
|
||||
try {
|
||||
TextSearchIndex index = cache.getTextIndex();
|
||||
CodeUsageInfo usageInfo = cache.getUsageInfo();
|
||||
@@ -51,17 +40,18 @@ public class IndexJob extends BackgroundJob {
|
||||
List<StringRef> lines = splitLines(cls);
|
||||
|
||||
usageInfo.processClass(cls, linesInfo, lines);
|
||||
if (UiUtils.isFreeMemoryAvailable()) {
|
||||
index.indexCode(cls, linesInfo, lines);
|
||||
} else {
|
||||
index.classCodeIndexSkipped(cls);
|
||||
}
|
||||
index.indexCode(cls, linesInfo, lines);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Index error in class: {}", cls.getFullName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void refreshIndex(CacheObject cache, JavaClass cls) {
|
||||
public void indexResources() {
|
||||
TextSearchIndex index = cache.getTextIndex();
|
||||
index.indexResource();
|
||||
}
|
||||
|
||||
public void refreshIndex(JavaClass cls) {
|
||||
TextSearchIndex index = cache.getTextIndex();
|
||||
CodeUsageInfo usageInfo = cache.getUsageInfo();
|
||||
if (index == null || usageInfo == null) {
|
||||
@@ -69,7 +59,9 @@ public class IndexJob extends BackgroundJob {
|
||||
}
|
||||
index.remove(cls);
|
||||
usageInfo.remove(cls);
|
||||
indexCls(cache, cls);
|
||||
if (UiUtils.isFreeMemoryAvailable()) {
|
||||
indexCls(cls);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -82,8 +74,11 @@ public class IndexJob extends BackgroundJob {
|
||||
return lines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInfoString() {
|
||||
return NLS.str("progress.index") + "… ";
|
||||
public boolean isComplete() {
|
||||
return indexComplete;
|
||||
}
|
||||
|
||||
public void setComplete(boolean indexComplete) {
|
||||
this.indexComplete = indexComplete;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package jadx.gui.jobs;
|
||||
|
||||
public enum TaskStatus {
|
||||
WAIT,
|
||||
STARTED,
|
||||
COMPLETE,
|
||||
CANCEL_BY_USER,
|
||||
CANCEL_BY_TIMEOUT,
|
||||
CANCEL_BY_MEMORY
|
||||
}
|
||||
@@ -46,9 +46,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.jobs.BackgroundJob;
|
||||
import jadx.gui.jobs.BackgroundWorker;
|
||||
import jadx.gui.jobs.DecompileJob;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.JResSearchNode;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
@@ -101,7 +98,7 @@ public abstract class CommonSearchDialog extends JDialog {
|
||||
}
|
||||
|
||||
public void prepare() {
|
||||
if (cache.getIndexJob().isComplete()) {
|
||||
if (cache.getIndexService().isComplete()) {
|
||||
loadFinishedCommon();
|
||||
loadFinished();
|
||||
return;
|
||||
@@ -532,19 +529,8 @@ public abstract class CommonSearchDialog extends JDialog {
|
||||
@Override
|
||||
public Void doInBackground() {
|
||||
try {
|
||||
BackgroundWorker backgroundWorker = mainWindow.getBackgroundWorker();
|
||||
if (backgroundWorker == null) {
|
||||
return null;
|
||||
}
|
||||
backgroundWorker.exec();
|
||||
|
||||
DecompileJob decompileJob = cache.getDecompileJob();
|
||||
progressPane.changeLabel(this, decompileJob.getInfoString());
|
||||
decompileJob.processAndWait();
|
||||
|
||||
BackgroundJob indexJob = cache.getIndexJob();
|
||||
progressPane.changeLabel(this, indexJob.getInfoString());
|
||||
indexJob.processAndWait();
|
||||
progressPane.changeLabel(this, NLS.str("progress.decompile") + ": ");
|
||||
mainWindow.waitDecompileTask();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Waiting background tasks failed", e);
|
||||
}
|
||||
@@ -553,11 +539,6 @@ public abstract class CommonSearchDialog extends JDialog {
|
||||
|
||||
@Override
|
||||
public void done() {
|
||||
try {
|
||||
get();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Load task failed", e);
|
||||
}
|
||||
loadFinishedCommon();
|
||||
loadFinished();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
@@ -87,9 +90,9 @@ import jadx.core.utils.files.FileUtils;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.device.debugger.BreakpointManager;
|
||||
import jadx.gui.jobs.BackgroundExecutor;
|
||||
import jadx.gui.jobs.BackgroundWorker;
|
||||
import jadx.gui.jobs.DecompileJob;
|
||||
import jadx.gui.jobs.IndexJob;
|
||||
import jadx.gui.jobs.DecompileTask;
|
||||
import jadx.gui.jobs.IndexService;
|
||||
import jadx.gui.jobs.TaskStatus;
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.settings.JadxSettingsWindow;
|
||||
@@ -154,6 +157,7 @@ public class MainWindow extends JFrame {
|
||||
private final transient JadxWrapper wrapper;
|
||||
private final transient JadxSettings settings;
|
||||
private final transient CacheObject cacheObject;
|
||||
private final transient BackgroundExecutor backgroundExecutor;
|
||||
private transient JadxProject project;
|
||||
private transient Action newProjectAction;
|
||||
private transient Action saveProjectAction;
|
||||
@@ -177,8 +181,6 @@ public class MainWindow extends JFrame {
|
||||
|
||||
private transient Link updateLink;
|
||||
private transient ProgressPanel progressPane;
|
||||
private transient BackgroundWorker backgroundWorker;
|
||||
private transient BackgroundExecutor backgroundExecutor;
|
||||
private transient Theme editorTheme;
|
||||
|
||||
private JDebuggerPanel debuggerPanel;
|
||||
@@ -196,10 +198,11 @@ public class MainWindow extends JFrame {
|
||||
registerMouseNavigationButtons();
|
||||
UiUtils.setWindowIcons(this);
|
||||
loadSettings();
|
||||
checkForUpdate();
|
||||
newProject();
|
||||
|
||||
this.backgroundExecutor = new BackgroundExecutor(this);
|
||||
|
||||
checkForUpdate();
|
||||
newProject();
|
||||
}
|
||||
|
||||
public void init() {
|
||||
@@ -393,7 +396,7 @@ public class MainWindow extends JFrame {
|
||||
deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
|
||||
initTree();
|
||||
update();
|
||||
runBackgroundJobs();
|
||||
runInitialBackgroundJobs();
|
||||
BreakpointManager.init(paths.get(0).getParent());
|
||||
onFinish.run();
|
||||
});
|
||||
@@ -469,37 +472,42 @@ public class MainWindow extends JFrame {
|
||||
cacheObject.setJRoot(treeRoot);
|
||||
cacheObject.setJadxSettings(settings);
|
||||
|
||||
int threadsCount = settings.getThreadsCount();
|
||||
cacheObject.setDecompileJob(new DecompileJob(wrapper, threadsCount));
|
||||
cacheObject.setIndexJob(new IndexJob(wrapper, cacheObject, threadsCount));
|
||||
cacheObject.setIndexService(new IndexService(cacheObject));
|
||||
cacheObject.setUsageInfo(new CodeUsageInfo(cacheObject.getNodeCache()));
|
||||
cacheObject.setTextIndex(new TextSearchIndex(this));
|
||||
}
|
||||
|
||||
synchronized void runBackgroundJobs() {
|
||||
cancelBackgroundJobs();
|
||||
backgroundWorker = new BackgroundWorker(cacheObject, progressPane);
|
||||
synchronized void runInitialBackgroundJobs() {
|
||||
if (settings.isAutoStartJobs()) {
|
||||
new Timer().schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
backgroundWorker.exec();
|
||||
waitDecompileTask();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void cancelBackgroundJobs() {
|
||||
if (backgroundExecutor != null) {
|
||||
backgroundExecutor.cancelAll();
|
||||
}
|
||||
if (backgroundWorker != null) {
|
||||
backgroundWorker.stop();
|
||||
backgroundWorker = new BackgroundWorker(cacheObject, progressPane);
|
||||
resetCache();
|
||||
private static final Object DECOMPILER_TASK_SYNC = new Object();
|
||||
|
||||
public void waitDecompileTask() {
|
||||
synchronized (DECOMPILER_TASK_SYNC) {
|
||||
try {
|
||||
DecompileTask decompileTask = new DecompileTask(this, wrapper);
|
||||
Future<TaskStatus> task = backgroundExecutor.execute(decompileTask);
|
||||
task.get();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Decompile task execution failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelBackgroundJobs() {
|
||||
ExecutorService worker = Executors.newSingleThreadExecutor();
|
||||
worker.execute(backgroundExecutor::cancelAll);
|
||||
worker.shutdown();
|
||||
}
|
||||
|
||||
public void reOpenFile() {
|
||||
List<Path> openedFile = wrapper.getOpenPaths();
|
||||
Map<String, Integer> openTabs = storeOpenTabs();
|
||||
@@ -673,7 +681,10 @@ public class MainWindow extends JFrame {
|
||||
} else if (obj instanceof QuarkReport) {
|
||||
tabbedPane.showSimpleNode((JNode) obj);
|
||||
} else if (obj instanceof JNode) {
|
||||
tabbedPane.codeJump(new JumpPosition((JNode) obj));
|
||||
JNode node = (JNode) obj;
|
||||
if (node.getRootClass() != null) {
|
||||
tabbedPane.codeJump(new JumpPosition(node));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Content loading error", e);
|
||||
@@ -1283,10 +1294,6 @@ public class MainWindow extends JFrame {
|
||||
return cacheObject;
|
||||
}
|
||||
|
||||
public BackgroundWorker getBackgroundWorker() {
|
||||
return backgroundWorker;
|
||||
}
|
||||
|
||||
public BackgroundExecutor getBackgroundExecutor() {
|
||||
return backgroundExecutor;
|
||||
}
|
||||
@@ -1314,6 +1321,11 @@ public class MainWindow extends JFrame {
|
||||
debuggerPanel = null;
|
||||
}
|
||||
|
||||
public void showHeapUsageBar() {
|
||||
settings.setShowHeapUsageBar(true);
|
||||
heapUsageBar.setVisible(true);
|
||||
}
|
||||
|
||||
private void initDebuggerPanel() {
|
||||
if (debuggerPanel == null) {
|
||||
debuggerPanel = new JDebuggerPanel(this);
|
||||
|
||||
@@ -41,7 +41,6 @@ import jadx.core.dex.nodes.VariableNode;
|
||||
import jadx.core.dex.visitors.RenameVisitor;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.jobs.IndexJob;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JField;
|
||||
@@ -244,7 +243,7 @@ public class RenameDialog extends JDialog {
|
||||
private void refreshJClass(JClass cls) {
|
||||
try {
|
||||
cls.reload();
|
||||
IndexJob.refreshIndex(cache, cls.getCls());
|
||||
cache.getIndexService().refreshIndex(cls.getCls());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to reload class: {}", cls.getFullName(), e);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
}
|
||||
|
||||
private TextSearchIndex checkIndex() {
|
||||
if (!cache.getIndexJob().isComplete()) {
|
||||
if (!cache.getIndexService().isComplete()) {
|
||||
if (isFullIndexNeeded()) {
|
||||
prepare();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.api.CodePosition;
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.JavaNode;
|
||||
import jadx.gui.jobs.IndexJob;
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
@@ -222,7 +221,7 @@ public final class CodeArea extends AbstractCodeArea {
|
||||
caretFix.save();
|
||||
|
||||
cls.reload();
|
||||
IndexJob.refreshIndex(getMainWindow().getCacheObject(), cls.getCls());
|
||||
getMainWindow().getCacheObject().getIndexService().refreshIndex(cls.getCls());
|
||||
|
||||
ClassCodeContentPanel codeContentPanel = (ClassCodeContentPanel) this.contentPanel;
|
||||
codeContentPanel.getTabbedPane().refresh(cls);
|
||||
|
||||
@@ -6,8 +6,7 @@ import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.jobs.DecompileJob;
|
||||
import jadx.gui.jobs.IndexJob;
|
||||
import jadx.gui.jobs.IndexService;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.JRoot;
|
||||
import jadx.gui.ui.SearchDialog;
|
||||
@@ -16,8 +15,7 @@ import jadx.gui.utils.search.TextSearchIndex;
|
||||
|
||||
public class CacheObject {
|
||||
|
||||
private DecompileJob decompileJob;
|
||||
private IndexJob indexJob;
|
||||
private IndexService indexService;
|
||||
|
||||
private TextSearchIndex textIndex;
|
||||
private CodeUsageInfo usageInfo;
|
||||
@@ -36,8 +34,7 @@ public class CacheObject {
|
||||
public void reset() {
|
||||
jRoot = null;
|
||||
settings = null;
|
||||
decompileJob = null;
|
||||
indexJob = null;
|
||||
indexService = null;
|
||||
textIndex = null;
|
||||
lastSearch = null;
|
||||
jNodeCache = new JNodeCache();
|
||||
@@ -45,14 +42,6 @@ public class CacheObject {
|
||||
lastSearchOptions = new HashMap<>();
|
||||
}
|
||||
|
||||
public DecompileJob getDecompileJob() {
|
||||
return decompileJob;
|
||||
}
|
||||
|
||||
public void setDecompileJob(DecompileJob decompileJob) {
|
||||
this.decompileJob = decompileJob;
|
||||
}
|
||||
|
||||
public TextSearchIndex getTextIndex() {
|
||||
return textIndex;
|
||||
}
|
||||
@@ -87,12 +76,12 @@ public class CacheObject {
|
||||
this.commentsIndex = commentsIndex;
|
||||
}
|
||||
|
||||
public IndexJob getIndexJob() {
|
||||
return indexJob;
|
||||
public IndexService getIndexService() {
|
||||
return indexService;
|
||||
}
|
||||
|
||||
public void setIndexJob(IndexJob indexJob) {
|
||||
this.indexJob = indexJob;
|
||||
public void setIndexService(IndexService indexService) {
|
||||
this.indexService = indexService;
|
||||
}
|
||||
|
||||
public JNodeCache getNodeCache() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package jadx.gui.utils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import jadx.api.CodePosition;
|
||||
import jadx.api.JavaNode;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
@@ -10,11 +12,11 @@ public class JumpPosition {
|
||||
private int pos;
|
||||
|
||||
public JumpPosition(JNode jumpNode) {
|
||||
this(jumpNode.getRootClass(), jumpNode.getLine(), jumpNode.getPos());
|
||||
this(Objects.requireNonNull(jumpNode.getRootClass()), jumpNode.getLine(), jumpNode.getPos());
|
||||
}
|
||||
|
||||
public JumpPosition(JNode jumpNode, CodePosition codePos) {
|
||||
this(jumpNode.getRootClass(), codePos.getLine(), codePos.getPos());
|
||||
this(Objects.requireNonNull(jumpNode.getRootClass()), codePos.getLine(), codePos.getPos());
|
||||
}
|
||||
|
||||
public JumpPosition(JNode node, int line, int pos) {
|
||||
|
||||
@@ -250,4 +250,12 @@ public class UiUtils {
|
||||
SwingUtilities.convertPointFromScreen(pos, comp);
|
||||
return pos;
|
||||
}
|
||||
|
||||
public static String getEnvVar(String varName, String defValue) {
|
||||
String envVal = System.getenv(varName);
|
||||
if (envVal == null) {
|
||||
return defValue;
|
||||
}
|
||||
return envVal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ tree.loading=Laden…
|
||||
|
||||
progress.load=Laden
|
||||
progress.decompile=Dekompilieren
|
||||
progress.index=Indizieren
|
||||
#progress.index=Indizieren
|
||||
|
||||
error_dialog.title=Fehler
|
||||
|
||||
@@ -58,6 +58,9 @@ tabs.smali=Smali
|
||||
nav.back=Zurück
|
||||
nav.forward=Vorwärts
|
||||
|
||||
#message.taskTimeout=Task exceeded time limit of %d ms.
|
||||
#message.userCancelTask=Task was canceled by user.
|
||||
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
|
||||
message.indexingClassesSkipped=<html>Jadx hat nur noch wenig Speicherplatz. Daher wurden %d Klassen nicht indiziert.<br>Wenn Sie möchten, dass alle Klassen indiziert werden, Jadx mit erhöhter maximaler Heap-Größe neustarten.</html>
|
||||
|
||||
heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB
|
||||
|
||||
@@ -36,7 +36,7 @@ tree.loading=Loading...
|
||||
|
||||
progress.load=Loading
|
||||
progress.decompile=Decompiling
|
||||
progress.index=Indexing
|
||||
#progress.index=Indexing
|
||||
|
||||
error_dialog.title=Error
|
||||
|
||||
@@ -58,6 +58,9 @@ tabs.smali=Smali
|
||||
nav.back=Back
|
||||
nav.forward=Forward
|
||||
|
||||
message.taskTimeout=Task exceeded time limit of %d ms.
|
||||
message.userCancelTask=Task was canceled by user.
|
||||
message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
|
||||
message.indexingClassesSkipped=<html>Jadx is running low on memory. Therefore %d classes were not indexed.<br>If you want all classes to be indexed restart Jadx with increased maximum heap size.</html>
|
||||
|
||||
heapUsage.text=JADX memory usage: %.2f GB of %.2f GB
|
||||
|
||||
@@ -36,7 +36,7 @@ tree.loading=Cargando...
|
||||
|
||||
progress.load=Cargando
|
||||
progress.decompile=Decompiling
|
||||
progress.index=Indexing
|
||||
#progress.index=Indexing
|
||||
|
||||
#error_dialog.title=
|
||||
|
||||
@@ -58,6 +58,9 @@ tabs.closeAll=Cerrar todo
|
||||
nav.back=Atrás
|
||||
nav.forward=Adelante
|
||||
|
||||
#message.taskTimeout=Task exceeded time limit of %d ms.
|
||||
#message.userCancelTask=Task was canceled by user.
|
||||
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
|
||||
#message.indexingClassesSkipped=
|
||||
|
||||
#heapUsage.text=
|
||||
|
||||
@@ -36,7 +36,7 @@ tree.loading=로딩중...
|
||||
|
||||
progress.load=로딩중
|
||||
progress.decompile=디컴파일 중
|
||||
progress.index=인덱싱 중
|
||||
#progress.index=인덱싱 중
|
||||
|
||||
error_dialog.title=오류
|
||||
|
||||
@@ -58,6 +58,9 @@ tabs.smali=Smali
|
||||
nav.back=뒤로
|
||||
nav.forward=앞으로
|
||||
|
||||
#message.taskTimeout=Task exceeded time limit of %d ms.
|
||||
#message.userCancelTask=Task was canceled by user.
|
||||
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
|
||||
message.indexingClassesSkipped=<html>Jadx의 메모리가 부족합니다. 따라서 %d 개의 클래스가 인덱싱되지 않았습니다. <br> 모든 클래스를 인덱싱하려면 최대 힙 크기를 늘린 상태로 Jadx를 다시 시작하십시오.</html>
|
||||
|
||||
heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB
|
||||
|
||||
@@ -36,7 +36,7 @@ tree.loading=稍等...
|
||||
|
||||
progress.load=稍等
|
||||
progress.decompile=Decompiling
|
||||
progress.index=Indexing
|
||||
#progress.index=Indexing
|
||||
|
||||
error_dialog.title=错误
|
||||
|
||||
@@ -58,6 +58,9 @@ tabs.code=代码
|
||||
nav.back=后退
|
||||
nav.forward=前进
|
||||
|
||||
#message.taskTimeout=Task exceeded time limit of %d ms.
|
||||
#message.userCancelTask=Task was canceled by user.
|
||||
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
|
||||
message.indexingClassesSkipped=<html>Jadx 的内存不足。因此,%d 类没有编入索引。<br>如果要将所有类编入索引,请使用增加的最大堆大小重新启动 Jadx。</html>
|
||||
|
||||
heapUsage.text=JADX 内存使用率:%.2f GB 共 %.2f GB
|
||||
|
||||
Reference in New Issue
Block a user