diff --git a/jadx-core/src/main/java/jadx/api/IDecompileScheduler.java b/jadx-core/src/main/java/jadx/api/IDecompileScheduler.java new file mode 100644 index 000000000..9cb8a48a3 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/IDecompileScheduler.java @@ -0,0 +1,7 @@ +package jadx.api; + +import java.util.List; + +public interface IDecompileScheduler { + List> buildBatches(List classes); +} diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 6f909ea76..7ee80dfe4 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,6 +39,7 @@ import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.SaveCode; import jadx.core.export.ExportGradleProject; +import jadx.core.utils.DecompilerScheduler; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; @@ -91,6 +93,8 @@ public final class JadxDecompiler implements Closeable { private final Map methodsMap = new ConcurrentHashMap<>(); private final Map fieldsMap = new ConcurrentHashMap<>(); + private final IDecompileScheduler decompileScheduler = new DecompilerScheduler(this); + public JadxDecompiler() { this(new JadxArgs()); } @@ -281,19 +285,26 @@ public final class JadxDecompiler implements Closeable { private void appendSourcesSave(List tasks, File outDir) { Predicate classFilter = args.getClassFilter(); - for (JavaClass cls : getClasses()) { + List classes = getClasses(); + List processQueue = new ArrayList<>(classes.size()); + for (JavaClass cls : classes) { if (cls.getClassNode().contains(AFlag.DONT_GENERATE)) { continue; } if (classFilter != null && !classFilter.test(cls.getFullName())) { continue; } + processQueue.add(cls); + } + for (List decompileBatch : decompileScheduler.buildBatches(processQueue)) { tasks.add(() -> { - try { - ICodeInfo code = cls.getCodeInfo(); - SaveCode.save(outDir, cls.getClassNode(), code); - } catch (Exception e) { - LOG.error("Error saving class: {}", cls.getFullName(), e); + for (JavaClass cls : decompileBatch) { + try { + ICodeInfo code = cls.getCodeInfo(); + SaveCode.save(outDir, cls.getClassNode(), code); + } catch (Exception e) { + LOG.error("Error saving class: {}", cls, e); + } } }); } @@ -405,7 +416,8 @@ public final class JadxDecompiler implements Closeable { } @Nullable("For not generated classes") - private JavaClass getJavaClassByNode(ClassNode cls) { + @ApiStatus.Internal + public JavaClass getJavaClassByNode(ClassNode cls) { JavaClass javaClass = classesMap.get(cls); if (javaClass != null) { return javaClass; @@ -554,6 +566,23 @@ public final class JadxDecompiler implements Closeable { throw new JadxRuntimeException("Unexpected node type: " + obj); } + // TODO: make interface for all nodes in code annotations and add common method instead this + Object getInternalNode(JavaNode javaNode) { + if (javaNode instanceof JavaClass) { + return ((JavaClass) javaNode).getClassNode(); + } + if (javaNode instanceof JavaMethod) { + return ((JavaMethod) javaNode).getMethodNode(); + } + if (javaNode instanceof JavaField) { + return ((JavaField) javaNode).getFieldNode(); + } + if (javaNode instanceof JavaVariable) { + return ((JavaVariable) javaNode).getVarRef(); + } + throw new JadxRuntimeException("Unexpected node type: " + javaNode); + } + List convertNodes(Collection nodesList) { return nodesList.stream() .map(this::convertNode) @@ -597,6 +626,10 @@ public final class JadxDecompiler implements Closeable { return pluginManager; } + public IDecompileScheduler getDecompileScheduler() { + return decompileScheduler; + } + @Override public String toString() { return "jadx decompiler " + getVersion(); diff --git a/jadx-core/src/main/java/jadx/api/JavaClass.java b/jadx-core/src/main/java/jadx/api/JavaClass.java index d1b41c371..b73faa13c 100644 --- a/jadx-core/src/main/java/jadx/api/JavaClass.java +++ b/jadx-core/src/main/java/jadx/api/JavaClass.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; @@ -69,6 +70,7 @@ public final class JavaClass implements JavaNode { /** * Internal API. Not Stable! */ + @ApiStatus.Internal public ClassNode getClassNode() { return cls; } @@ -155,6 +157,23 @@ public final class JavaClass implements JavaNode { return resultMap; } + public List getUsageFor(JavaNode javaNode) { + Map map = getCodeAnnotations(); + if (map.isEmpty() || decompiler == null) { + return Collections.emptyList(); + } + Object internalNode = getRootDecompiler().getInternalNode(javaNode); + List result = new ArrayList<>(); + for (Map.Entry entry : map.entrySet()) { + CodePosition codePosition = entry.getKey(); + Object obj = entry.getValue(); + if (internalNode.equals(obj)) { + result.add(codePosition); + } + } + return result; + } + @Override public List getUseIn() { return getRootDecompiler().convertNodes(cls.getUseIn()); diff --git a/jadx-core/src/main/java/jadx/api/JavaField.java b/jadx-core/src/main/java/jadx/api/JavaField.java index 12a440b5d..f8bfa8e75 100644 --- a/jadx-core/src/main/java/jadx/api/JavaField.java +++ b/jadx-core/src/main/java/jadx/api/JavaField.java @@ -2,6 +2,8 @@ package jadx.api; import java.util.List; +import org.jetbrains.annotations.ApiStatus; + import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.FieldNode; @@ -67,6 +69,7 @@ public final class JavaField implements JavaNode { /** * Internal API. Not Stable! */ + @ApiStatus.Internal public FieldNode getFieldNode() { return field; } diff --git a/jadx-core/src/main/java/jadx/api/JavaMethod.java b/jadx-core/src/main/java/jadx/api/JavaMethod.java index 277a8889a..2f0dce9cb 100644 --- a/jadx-core/src/main/java/jadx/api/JavaMethod.java +++ b/jadx-core/src/main/java/jadx/api/JavaMethod.java @@ -4,6 +4,8 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.jetbrains.annotations.ApiStatus; + import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.info.AccessInfo; @@ -101,6 +103,7 @@ public final class JavaMethod implements JavaNode { /** * Internal API. Not Stable! */ + @ApiStatus.Internal public MethodNode getMethodNode() { return mth; } diff --git a/jadx-core/src/main/java/jadx/api/JavaVariable.java b/jadx-core/src/main/java/jadx/api/JavaVariable.java index d757fac0d..0cd296569 100644 --- a/jadx-core/src/main/java/jadx/api/JavaVariable.java +++ b/jadx-core/src/main/java/jadx/api/JavaVariable.java @@ -3,6 +3,8 @@ package jadx.api; import java.util.Collections; import java.util.List; +import org.jetbrains.annotations.ApiStatus; + import jadx.api.data.annotations.VarDeclareRef; import jadx.api.data.annotations.VarRef; @@ -32,6 +34,11 @@ public class JavaVariable implements JavaNode { return varRef.getName(); } + @ApiStatus.Internal + public VarRef getVarRef() { + return varRef; + } + @Override public String getFullName() { return varRef.getType() + " " + varRef.getName() + " (r" + varRef.getReg() + "v" + varRef.getSsa() + ")"; diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java index d6e3ee797..3e07f1325 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java @@ -8,9 +8,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.WeakHashMap; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,7 +28,7 @@ public class ClspGraph { private static final Logger LOG = LoggerFactory.getLogger(ClspGraph.class); private final RootNode root; - private final Map> superTypesCache = Collections.synchronizedMap(new WeakHashMap<>()); + private Map> superTypesCache; private Map nameMap; private final Set missingClasses = new HashSet<>(); @@ -159,9 +157,12 @@ public class ClspGraph { } public Set getSuperTypes(String clsName) { - Set fromCache = superTypesCache.get(clsName); - if (fromCache != null) { - return fromCache; + if (superTypesCache != null) { + Set result = superTypesCache.get(clsName); + if (result == null) { + return Collections.emptySet(); + } + return result; } ClspClass cls = nameMap.get(clsName); if (cls == null) { @@ -170,18 +171,25 @@ public class ClspGraph { } Set result = new HashSet<>(); addSuperTypes(cls, result); - return putInSuperTypesCache(clsName, result); + return result; } - @NotNull - private Set putInSuperTypesCache(String clsName, Set result) { - if (result.isEmpty()) { - Set empty = Collections.emptySet(); - superTypesCache.put(clsName, empty); - return empty; + public synchronized void fillSuperTypesCache() { + Map> map = new HashMap<>(nameMap.size()); + Set tmpSet = new HashSet<>(); + for (Map.Entry entry : nameMap.entrySet()) { + ClspClass cls = entry.getValue(); + tmpSet.clear(); + addSuperTypes(cls, tmpSet); + Set result; + if (tmpSet.isEmpty()) { + result = Collections.emptySet(); + } else { + result = new HashSet<>(tmpSet); + } + map.put(cls.getName(), result); } - superTypesCache.put(clsName, result); - return result; + superTypesCache = map; } private void addSuperTypes(ClspClass cls, Set result) { @@ -203,9 +211,7 @@ public class ClspGraph { private ClspClass getClspClass(ArgType clsType) { ClspClass clspClass = nameMap.get(clsType.getObject()); if (clspClass == null) { - if (LOG.isDebugEnabled()) { - LOG.debug("External class not found: {}", clsType.getObject()); - } + missingClasses.add(clsType.getObject()); } return clspClass; } diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index aaac35659..d481c3acc 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -682,7 +682,12 @@ public class InsnGen { code.add("this"); } else { code.add("new "); - code.attachAnnotation(callMth); + if (callMth == null || callMth.contains(AFlag.DONT_GENERATE)) { + // use class reference if constructor method is missing (default constructor) + code.attachAnnotation(mth.root().resolveClass(insn.getCallMth().getDeclClass())); + } else { + code.attachAnnotation(callMth); + } mgen.getClassGen().addClsName(code, insn.getClassType()); GenericInfoAttr genericInfoAttr = insn.get(AType.GENERIC_INFO); if (genericInfoAttr != null) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java index becb7e225..ecadf5742 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java @@ -43,6 +43,11 @@ import jadx.core.utils.exceptions.JadxException; ) public class OverrideMethodVisitor extends AbstractVisitor { + @Override + public void init(RootNode root) throws JadxException { + root.getClsp().fillSuperTypesCache(); + } + @Override public boolean visit(ClassNode cls) throws JadxException { processCls(cls); diff --git a/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java b/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java new file mode 100644 index 000000000..146c17c4e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java @@ -0,0 +1,147 @@ +package jadx.core.utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.IDecompileScheduler; +import jadx.api.JadxDecompiler; +import jadx.api.JavaClass; +import jadx.core.dex.nodes.ClassNode; + +public class DecompilerScheduler implements IDecompileScheduler { + private static final Logger LOG = LoggerFactory.getLogger(DecompilerScheduler.class); + + private static final int MERGED_BATCH_SIZE = 16; + private static final boolean DUMP_STATS = false; + + private final JadxDecompiler decompiler; + + public DecompilerScheduler(JadxDecompiler decompiler) { + this.decompiler = decompiler; + } + + @Override + public List> buildBatches(List classes) { + long start = System.currentTimeMillis(); + List> batches = internalBatches(Utils.collectionMap(classes, JavaClass::getClassNode)); + List> result = Utils.collectionMap(batches, l -> Utils.collectionMapNoNull(l, decompiler::getJavaClassByNode)); + if (LOG.isDebugEnabled()) { + LOG.debug("Build decompilation batches in {}ms", System.currentTimeMillis() - start); + } + return result; + } + + /** + * Put classes with many dependencies at the end. + * Build batches for dependencies of single class to avoid locking from another thread. + */ + public List> internalBatches(List classes) { + Map depsMap = new HashMap<>(classes.size()); + Set visited = new HashSet<>(); + for (ClassNode classNode : classes) { + visited.clear(); + sumDeps(classNode, depsMap, visited); + } + List deps = new ArrayList<>(depsMap.values()); + Collections.sort(deps); + + Set added = new HashSet<>(classes.size()); + Comparator cmpDepSize = Comparator.comparingInt(c -> c.getDependencies().size()); + List> result = new ArrayList<>(); + List mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE); + for (DepInfo depInfo : deps) { + ClassNode cls = depInfo.getCls(); + int depsSize = cls.getDependencies().size(); + if (depsSize == 0) { + // add classes without dependencies in merged batch + mergedBatch.add(cls); + added.add(cls); + if (mergedBatch.size() >= MERGED_BATCH_SIZE) { + result.add(mergedBatch); + mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE); + } + } else { + List batch = new ArrayList<>(depsSize + 1); + for (ClassNode dep : cls.getDependencies()) { + ClassNode topDep = dep.getTopParentClass(); + if (!added.contains(topDep)) { + batch.add(topDep); + } + } + batch.sort(cmpDepSize); + batch.add(cls); + added.addAll(batch); + result.add(batch); + } + } + if (mergedBatch.size() > 0) { + result.add(mergedBatch); + } + if (DUMP_STATS) { + dumpBatchesStats(classes, result, deps); + } + return result; + } + + public int sumDeps(ClassNode cls, Map depsMap, Set visited) { + visited.add(cls); + DepInfo depInfo = depsMap.get(cls); + if (depInfo != null) { + return depInfo.getDepsCount(); + } + List deps = cls.getDependencies(); + int count = deps.size(); + for (ClassNode dep : deps) { + if (!visited.contains(dep)) { + count += sumDeps(dep, depsMap, visited); + } + } + depsMap.put(cls, new DepInfo(cls, count)); + return count; + } + + private static final class DepInfo implements Comparable { + private final ClassNode cls; + private final int depsCount; + + private DepInfo(ClassNode cls, int depsCount) { + this.cls = cls; + this.depsCount = depsCount; + } + + public ClassNode getCls() { + return cls; + } + + public int getDepsCount() { + return depsCount; + } + + @Override + public int compareTo(@NotNull DecompilerScheduler.DepInfo o) { + return Integer.compare(depsCount, o.depsCount); + } + } + + private void dumpBatchesStats(List classes, List> result, List deps) { + double avg = result.stream().mapToInt(List::size).average().orElse(-1); + int maxSingleDeps = classes.stream().mapToInt(c -> c.getDependencies().size()).max().orElse(-1); + int maxRecursiveDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1); + LOG.info("Batches stats:" + + "\n input classes: " + classes.size() + + ",\n batches: " + result.size() + + ",\n average batch size: " + avg + + ",\n max single deps count: " + maxSingleDeps + + ",\n max recursive deps count: " + maxRecursiveDeps); + } +} diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index fd651b252..3aa5141f4 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -207,6 +207,20 @@ public class Utils { return result; } + public static List collectionMapNoNull(Collection list, Function mapFunc) { + if (list == null || list.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(list.size()); + for (T t : list) { + R r = mapFunc.apply(t); + if (r != null) { + result.add(r); + } + } + return result; + } + public static boolean containsInListByRef(List list, T element) { if (isEmpty(list)) { return false; diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index d9d701186..75069e0b2 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -88,6 +88,10 @@ public class JadxWrapper { }).collect(Collectors.toList()); } + public List> buildDecompileBatches(List classes) { + return decompiler.getDecompileScheduler().buildBatches(classes); + } + // TODO: move to CLI and filter classes in JadxDecompiler public List getExcludedPackages() { String excludedPackages = settings.getExcludedPackages().trim(); diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java index dbadbdcad..ab0d8ac89 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -119,6 +119,7 @@ public class BackgroundExecutor { private TaskStatus waitTermination(ThreadPoolExecutor executor) throws InterruptedException { Supplier cancelCheck = buildCancelCheck(); try { + int k = 0; while (true) { if (executor.isTerminated()) { return TaskStatus.COMPLETE; @@ -129,7 +130,8 @@ public class BackgroundExecutor { return cancelStatus; } setProgress(calcProgress(executor.getCompletedTaskCount())); - Thread.sleep(300); + k++; + Thread.sleep(k < 20 ? 100 : 1000); // faster update for short tasks } } catch (InterruptedException e) { LOG.debug("Task wait interrupted"); diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java index b691bc39e..aa3c02d18 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java @@ -54,14 +54,30 @@ public class DecompileTask implements IBackgroundTask { complete.set(0); List jobs = new ArrayList<>(expectedCompleteCount + 1); - for (JavaClass cls : classesForIndex) { + jobs.add(indexService::indexResources); + for (List batch : wrapper.buildDecompileBatches(classesForIndex)) { jobs.add(() -> { - cls.decompile(); - indexService.indexCls(cls); - complete.incrementAndGet(); + for (JavaClass cls : batch) { + try { + cls.decompile(); + } catch (Throwable e) { + LOG.error("Failed to decompile class: {}", cls, e); + } finally { + complete.incrementAndGet(); + } + } }); } - jobs.add(indexService::indexResources); + jobs.add(() -> { + for (JavaClass cls : classesForIndex) { + try { + // TODO: a lot of synchronizations to index object, not effective for parallel usage + indexService.indexCls(cls); + } catch (Throwable e) { + LOG.error("Failed to index class: {}", cls, e); + } + } + }); startTime = System.currentTimeMillis(); return jobs; } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java b/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java index 62adac0af..2975b3e62 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java @@ -12,7 +12,6 @@ import jadx.api.ICodeWriter; import jadx.api.JavaClass; import jadx.gui.utils.CacheObject; import jadx.gui.utils.CodeLinesInfo; -import jadx.gui.utils.CodeUsageInfo; import jadx.gui.utils.search.StringRef; import jadx.gui.utils.search.TextSearchIndex; @@ -28,21 +27,19 @@ public class IndexService { this.cache = cache; } + /** + * Warning! Not ready for parallel execution. Use only in a single thread. + */ public void indexCls(JavaClass cls) { try { TextSearchIndex index = cache.getTextIndex(); - CodeUsageInfo usageInfo = cache.getUsageInfo(); - if (index == null || usageInfo == null) { + if (index == null) { return; } - - index.indexNames(cls); - - CodeLinesInfo linesInfo = new CodeLinesInfo(cls); List lines = splitLines(cls); - - usageInfo.processClass(cls, linesInfo, lines); + CodeLinesInfo linesInfo = new CodeLinesInfo(cls); index.indexCode(cls, linesInfo, lines); + index.indexNames(cls); indexSet.add(cls); } catch (Exception e) { LOG.error("Index error in class: {}", cls.getFullName(), e); @@ -54,15 +51,13 @@ public class IndexService { index.indexResource(); } - public void refreshIndex(JavaClass cls) { + public synchronized void refreshIndex(JavaClass cls) { TextSearchIndex index = cache.getTextIndex(); - CodeUsageInfo usageInfo = cache.getUsageInfo(); - if (index == null || usageInfo == null) { + if (index == null) { return; } indexSet.remove(cls); index.remove(cls); - usageInfo.remove(cls); indexCls(cls); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 6d3a6f53d..ce1b49024 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -122,7 +122,6 @@ import jadx.gui.update.JadxUpdate; import jadx.gui.update.JadxUpdate.IUpdateCallback; import jadx.gui.update.data.Release; import jadx.gui.utils.CacheObject; -import jadx.gui.utils.CodeUsageInfo; import jadx.gui.utils.FontUtils; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.LafManager; @@ -525,7 +524,6 @@ public class MainWindow extends JFrame { cacheObject.setJadxSettings(settings); cacheObject.setIndexService(new IndexService(cacheObject)); - cacheObject.setUsageInfo(new CodeUsageInfo(cacheObject.getNodeCache())); cacheObject.setTextIndex(new TextSearchIndex(this)); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java index d3baaee13..5d783a965 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java @@ -53,6 +53,7 @@ import jadx.gui.ui.TabbedPane; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.utils.CacheObject; +import jadx.gui.utils.JNodeCache; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; @@ -564,4 +565,8 @@ public abstract class CommonSearchDialog extends JDialog { warnLabel.setVisible(true); } } + + protected JNodeCache getNodeCache() { + return mainWindow.getCacheObject().getNodeCache(); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java index 8704e85fa..23e3ef8a8 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java @@ -3,10 +3,10 @@ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Container; import java.awt.FlowLayout; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; import javax.swing.BorderFactory; import javax.swing.JLabel; @@ -14,23 +14,26 @@ import javax.swing.JPanel; import javax.swing.SwingConstants; import javax.swing.WindowConstants; +import jadx.api.CodePosition; +import jadx.api.ICodeWriter; import jadx.api.JavaClass; import jadx.api.JavaNode; -import jadx.gui.jobs.IndexService; import jadx.gui.jobs.TaskStatus; import jadx.gui.treemodel.CodeNode; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; -import jadx.gui.utils.CodeUsageInfo; +import jadx.gui.utils.CodeLinesInfo; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.search.StringRef; public class UsageDialog extends CommonSearchDialog { - private static final long serialVersionUID = -5105405789969134105L; private final transient JNode node; + private transient List usageList; + public UsageDialog(MainWindow mainWindow, JNode node) { super(mainWindow); this.node = node; @@ -42,32 +45,9 @@ public class UsageDialog extends CommonSearchDialog { @Override protected void openInit() { - IndexService indexService = mainWindow.getCacheObject().getIndexService(); - if (indexService.isComplete()) { - loadFinishedCommon(); - loadFinished(); - return; - } - List useIn = node.getJavaNode().getUseIn(); - List usageTopClsForIndex = useIn - .stream() - .map(JavaNode::getTopParentClass) - .filter(indexService::isIndexNeeded) - .distinct() - .sorted(Comparator.comparing(JavaClass::getFullName)) - .collect(Collectors.toList()); - if (usageTopClsForIndex.isEmpty()) { - loadFinishedCommon(); - loadFinished(); - return; - } + usageList = new ArrayList<>(); mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), - () -> { - for (JavaClass cls : usageTopClsForIndex) { - cls.decompile(); - indexService.indexCls(cls); - } - }, + this::collectUsageData, (status) -> { if (status == TaskStatus.CANCEL_BY_MEMORY) { mainWindow.showHeapUsageBar(); @@ -78,6 +58,45 @@ public class UsageDialog extends CommonSearchDialog { }); } + private void collectUsageData() { + node.getJavaNode().getUseIn() + .stream() + .map(JavaNode::getTopParentClass) + .distinct() + .sorted(Comparator.comparing(JavaClass::getFullName)) + .forEach(this::processUsageClass); + } + + private void processUsageClass(JavaClass cls) { + String code = cls.getCodeInfo().getCodeStr(); + CodeLinesInfo linesInfo = new CodeLinesInfo(cls); + JavaNode javaNode = node.getJavaNode(); + List usage = cls.getUsageFor(javaNode); + for (CodePosition pos : usage) { + if (javaNode.getTopParentClass().equals(cls) && pos.getPos() == javaNode.getDefPos()) { + // skip declaration + continue; + } + StringRef line = getLineStrAt(code, pos.getPos()); + if (line.startsWith("import ")) { + continue; + } + JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(pos.getLine()); + JNode useAtNode = javaNodeByLine == null ? node : getNodeCache().makeFrom(javaNodeByLine); + usageList.add(new CodeNode(useAtNode, line, pos.getLine(), pos.getPos())); + } + } + + private StringRef getLineStrAt(String code, int pos) { + String newLine = ICodeWriter.NL; + int start = code.lastIndexOf(newLine, pos); + int end = code.indexOf(newLine, pos); + if (start == -1 || end == -1) { + return StringRef.fromStr("line not found"); + } + return StringRef.subString(code, start + newLine.length(), end).trim(); + } + @Override protected void loadFinished() { resultsTable.setEnabled(true); @@ -92,12 +111,6 @@ public class UsageDialog extends CommonSearchDialog { @Override protected synchronized void performSearch() { resultsModel.clear(); - - CodeUsageInfo usageInfo = cache.getUsageInfo(); - if (usageInfo == null) { - return; - } - List usageList = usageInfo.getUsageList(node); Collections.sort(usageList); resultsModel.addAll(usageList); // TODO: highlight only needed node usage diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index 5af32ff9c..cae5123a0 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -18,7 +18,6 @@ public class CacheObject { private IndexService indexService; private TextSearchIndex textIndex; - private CodeUsageInfo usageInfo; private CommentsIndex commentsIndex; private String lastSearch; private JNodeCache jNodeCache; @@ -38,7 +37,6 @@ public class CacheObject { textIndex = null; lastSearch = null; jNodeCache = new JNodeCache(); - usageInfo = null; lastSearchOptions = new HashMap<>(); } @@ -59,15 +57,6 @@ public class CacheObject { this.lastSearch = lastSearch; } - @Nullable - public CodeUsageInfo getUsageInfo() { - return usageInfo; - } - - public void setUsageInfo(@Nullable CodeUsageInfo usageInfo) { - this.usageInfo = usageInfo; - } - public CommentsIndex getCommentsIndex() { return commentsIndex; } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java b/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java deleted file mode 100644 index 2f9b6003f..000000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java +++ /dev/null @@ -1,105 +0,0 @@ -package jadx.gui.utils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Predicate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.api.CodePosition; -import jadx.api.JavaClass; -import jadx.api.JavaMethod; -import jadx.api.JavaNode; -import jadx.gui.treemodel.CodeNode; -import jadx.gui.treemodel.JNode; -import jadx.gui.utils.search.StringRef; - -public class CodeUsageInfo { - private static final Logger LOG = LoggerFactory.getLogger(CodeUsageInfo.class); - - public static class UsageInfo { - private final List usageList = new ArrayList<>(); - - public List getUsageList() { - return usageList; - } - - public synchronized void addUsage(CodeNode codeNode) { - usageList.add(codeNode); - } - - public synchronized void removeUsageIf(Predicate filter) { - usageList.removeIf(filter); - } - } - - private final JNodeCache nodeCache; - - public CodeUsageInfo(JNodeCache nodeCache) { - this.nodeCache = nodeCache; - } - - private final Map usageMap = new ConcurrentHashMap<>(); - - public void processClass(JavaClass javaClass, CodeLinesInfo linesInfo, List lines) { - try { - Map usage = javaClass.getUsageMap(); - for (Map.Entry entry : usage.entrySet()) { - CodePosition codePosition = entry.getKey(); - JavaNode javaNode = entry.getValue(); - if (javaNode.getTopParentClass().equals(javaClass) - && codePosition.getPos() == javaNode.getDefPos()) { - // skip declaration - continue; - } - JNode node = nodeCache.makeFrom(javaNode); - addUsage(node, javaClass, linesInfo, codePosition, lines); - if (javaNode instanceof JavaMethod) { - // add constructor usage also as class usage - if (((JavaMethod) javaNode).isConstructor()) { - addUsage(node.getJParent(), javaClass, linesInfo, codePosition, lines); - } - } - } - } catch (Exception e) { - LOG.error("Code usage process failed for class: {}", javaClass, e); - } - } - - private void addUsage(JNode jNode, JavaClass javaClass, - CodeLinesInfo linesInfo, CodePosition codePosition, List lines) { - int line = codePosition.getLine(); - StringRef codeLine = lines.get(line - 1); - if (codeLine.startsWith("import ")) { - // skip imports - return; - } - JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(line); - JNode node = nodeCache.makeFrom(javaNodeByLine == null ? javaClass : javaNodeByLine); - CodeNode codeNode = new CodeNode(node, codeLine, line, codePosition.getPos()); - UsageInfo usageInfo = usageMap.computeIfAbsent(jNode, key -> new UsageInfo()); - usageInfo.addUsage(codeNode); - } - - public List getUsageList(JNode node) { - UsageInfo usageInfo = usageMap.get(node); - if (usageInfo == null) { - return Collections.emptyList(); - } - return usageInfo.getUsageList(); - } - - public void remove(JavaClass cls) { - usageMap.entrySet().removeIf(e -> { - if (e.getKey().getJavaNode().getTopParentClass().equals(cls)) { - return true; - } - e.getValue().removeUsageIf(node -> node.getJavaNode().getTopParentClass().equals(cls)); - return false; - }); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java index 8d4bc1752..b4166414a 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java @@ -23,6 +23,7 @@ public class JNodeCache { if (javaNode == null) { return null; } + // don't use 'computeIfAbsent' method here, it this cause 'Recursive update' exception JNode jNode = cache.get(javaNode); if (jNode == null) { jNode = convert(javaNode); @@ -35,8 +36,20 @@ public class JNodeCache { if (javaCls == null) { return null; } - return (JClass) cache.computeIfAbsent(javaCls, - jn -> new JClass(javaCls, makeFrom(javaCls.getDeclaringClass()))); + JClass jCls = (JClass) cache.get(javaCls); + if (jCls == null) { + jCls = convert(javaCls); + cache.put(javaCls, jCls); + } + return jCls; + } + + private JClass convert(JavaClass cls) { + JavaClass parentCls = cls.getDeclaringClass(); + if (parentCls == cls) { + return new JClass(cls, null); + } + return new JClass(cls, makeFrom(parentCls)); } private JNode convert(JavaNode node) { @@ -44,7 +57,7 @@ public class JNodeCache { return null; } if (node instanceof JavaClass) { - return new JClass((JavaClass) node, makeFrom(node.getDeclaringClass())); + return convert(((JavaClass) node)); } if (node instanceof JavaMethod) { return new JMethod((JavaMethod) node, makeFrom(node.getDeclaringClass())); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java index dbcabcef1..8b5b0cb44 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java @@ -20,11 +20,11 @@ public class CodeIndex { private final List values = new ArrayList<>(); - public synchronized void put(CodeNode value) { + public void put(CodeNode value) { values.add(value); } - public synchronized void removeForCls(JavaClass cls) { + public void removeForCls(JavaClass cls) { values.removeIf(v -> v.getJavaNode().getTopParentClass().equals(cls)); } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java index 1283b22f7..811b2cf02 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java @@ -1,8 +1,8 @@ package jadx.gui.utils.search; +import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; import io.reactivex.BackpressureStrategy; import io.reactivex.Flowable; @@ -12,7 +12,7 @@ import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; public class SimpleIndex { - private final Map data = new ConcurrentHashMap<>(); + private final Map data = new HashMap<>(); public void put(String str, JNode value) { data.put(value, str);