Repository navigation
Graph export and comparison
GraphJson, GraphDot, and GraphDifference work on immutable CallGraphSnapshot
values. They require no active Soot session and can be used after analysis closes.
They complement the reachability and subgraph queries.
Install the library and build the example project using Getting started. From the repository root, use the runnable ExportGraph and GraphQueries programs:
APK=/absolute/path/to/app.apk
PLATFORMS=/absolute/path/to/android-sdk/platforms
java -Xmx4g -cp 'examples/target/classes:examples/target/dependency/*' \
com.jordansamhi.androspecter.examples.ExportGraph \
"$APK" "$PLATFORMS" /tmp/androspecter-graph.json
java -Xmx4g -cp 'examples/target/classes:examples/target/dependency/*' \
com.jordansamhi.androspecter.examples.GraphQueries \
/tmp/androspecter-graph.json \
'<com.example.app.Entry: void start()>' \
'<com.example.app.Output: void send(java.lang.String)>' \
/tmp/androspecter-reachable.dotReplace the two method signatures with actual graph nodes. ExportGraph captures the
complete graph and closes the session before writing JSON. GraphQueries imports that
file, prints reachability and a shortest method path, and optionally writes the subgraph
reachable from the source as DOT. Its fourth argument is optional. Unknown method
signatures yield empty paths or closures, rather than being resolved against an APK.
Output files are replaced if present; their parent directories must already exist.
On Windows, use ; instead of : in the classpath. Graphviz is not required to generate
DOT. If installed separately, it can render the result, for example:
dot -Tsvg /tmp/androspecter-reachable.dot -o /tmp/androspecter-reachable.svgFilter large graphs before rendering when you only need a particular region. Importing JSON reconstructs the graph and its indexes in memory; a large export still needs an appropriate heap budget even though Soot is not opened.
| API | Behavior |
|---|---|
GraphJson.toJson(graph) |
Returns a complete JSON string |
GraphJson.fromJson(json) |
Validates and imports one JSON document |
GraphJson.write(graph, writer) |
Writes to a caller-owned character writer |
GraphJson.read(reader) |
Reads and validates one complete document from a caller-owned reader |
The library declares Gson 2.14.0 for this API. Consumers do not need to construct a Gson
instance or reflectively deserialize private snapshot fields. Use UTF-8 when opening
files, for example Files.newBufferedReader(path, StandardCharsets.UTF_8).
Version 1 uses this shape:
{
"schemaVersion": 1,
"analysisId": "12345678-1234-1234-1234-123456789abc",
"methods": [
"<example.App: void a()>",
"<example.App: void b()>"
],
"edges": [
{
"source": "<example.App: void a()>",
"target": "<example.App: void b()>",
"kind": "STATIC",
"callSite": null,
"callSiteIndex": -1
}
]
}All displayed fields are required. Only callSite may be null. The UUID, field types,
integer version, and edge positions are validated; each edge's endpoints must occur in
methods. Unsupported versions, missing/null required values, duplicate required fields,
invalid JSON, and trailing documents are rejected with IllegalArgumentException.
Unknown extra fields are ignored. Reader/writer I/O failures propagate as IOException.
Method signatures and kinds are opaque strings: import does not resolve methods, validate their declaration in an APK, or restrict kind names to a particular Soot version. Node duplicates are normalized, while equal parallel edge entries remain separate. Explicit isolated nodes, unavailable statement values, the analysis ID, and all edge metadata survive a round trip. Output ordering is deterministic for the same snapshot.
The stream methods neither close nor flush caller-owned streams. Use try-with-resources around file streams. Writes can leave partial output on I/O failure; callers needing atomic publication should write a temporary file and move it after success.
GraphDot.toDot(graph) returns DOT text; GraphDot.write(graph, writer) writes it to a
caller-owned writer without closing or flushing it. Each method is emitted as a quoted
node, including isolated nodes. Each edge occurrence is emitted separately, labelled
with its kind, with optional statement text as a tooltip and its capture-time index as
an attribute. Quotes, backslashes, and line breaks are escaped.
NUL characters in identifiers or attributes cause IllegalArgumentException before any
DOT output is written, because Graphviz cannot represent them. JSON can retain these values.
DOT is intended for visualization. It does not retain the analysis ID and there is no DOT importer. Use the versioned JSON format for graph interchange and later queries.
This complete CompareGraphs.java program accepts two exported JSON paths and compares
them without opening Soot. Place it in the consumer project described in
Getting started:
import com.jordansamhi.androspecter.graph.CallGraphSnapshot;
import com.jordansamhi.androspecter.graph.GraphDifference;
import com.jordansamhi.androspecter.graph.GraphJson;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/** Compares detached graph files without creating an Android analysis session. */
public final class CompareGraphs {
private CompareGraphs() {}
/**
* Prints changed nodes and edge multiplicities using source/target/kind identity.
*
* @param args baseline JSON path followed by the later graph's JSON path
* @throws IllegalArgumentException if arguments or graph documents are invalid
* @throws IOException if either file cannot be read
*/
public static void main(String[] args) throws IOException {
if (args.length != 2) {
throw new IllegalArgumentException("Usage: CompareGraphs <before.json> <after.json>");
}
CallGraphSnapshot before;
CallGraphSnapshot after;
try (Reader input = Files.newBufferedReader(Path.of(args[0]), StandardCharsets.UTF_8)) {
before = GraphJson.read(input);
}
try (Reader input = Files.newBufferedReader(Path.of(args[1]), StandardCharsets.UTF_8)) {
after = GraphJson.read(input);
}
GraphDifference difference = GraphDifference.between(before, after);
System.out.println("Unchanged: " + difference.isEmpty());
System.out.println("Added nodes: " + difference.addedMethods());
System.out.println("Removed nodes: " + difference.removedMethods());
System.out.println("Added edge occurrences: " + difference.addedEdges());
System.out.println("Removed edge occurrences: " + difference.removedEdges());
}
}addedEdges() and removedEdges() map GraphDifference.EdgeIdentity(source, target, kind)
to positive occurrence-count changes. If a baseline contains two equivalent edge occurrences
and the later graph contains three, the added count is 1, not 3. Changing only statement
text or its position creates no difference; changing the edge kind creates a removal and an
addition. Added and removed isolated nodes are reported too. Analysis IDs are ignored, so
snapshots from separate sessions can be compared.
This identity deliberately avoids callSiteIndex, which is only the position in a processed
body at capture time. Inserting probes or changing Jimple locals can alter indices and text
without changing a source/target relationship. Conversely, unchanged graph structure does
not establish unchanged program behavior. Comparisons do not infer why an edge changed or
normalize obfuscated/renamed method signatures across APK versions.
The runnable InstrumentInMemory example also compares snapshots before and after applying its transformation pipeline.
GraphHtml.write(graph, writer) exports a self-contained interactive viewer from
the same detached snapshot. For example, with an existing CallGraphSnapshot graph:
try (java.io.Writer writer = java.nio.file.Files.newBufferedWriter(
java.nio.file.Path.of("callgraph.html"), java.nio.charset.StandardCharsets.UTF_8)) {
com.jordansamhi.androspecter.graph.GraphHtml.write(graph, writer);
}Open the file directly in a browser. Search covers all exported methods; selecting a method shows its callers and callees. Directed edges retain Soot edge kinds, parallel-call counts, and call-site details. Pan, zoom, and fit controls help explore a neighborhood. The viewer caps the visible neighborhood at 60 methods and 180 grouped relationships and displays omitted counts. These rendering limits do not remove data from the file: the full selected snapshot is embedded, so large graphs still consume browser memory. Filter the snapshot before exporting when you need a smaller report.
All scripts, styles, and data are embedded; the viewer requires no server, CDN, or
network access. Graph strings are escaped for HTML embedding and rendered as text.
The compact HTML payload is internal to the viewer; use GraphJson for versioned
interchange between tools. The write method neither closes nor flushes the caller's
writer, while toHtml(graph) returns the entire document as a string.
The standalone CLI also creates these pages without writing Java:
bin/androspecter graph --input-json callgraph.json --format html -o callgraph.htmlSource: GraphJson, GraphDot, GraphHtml, GraphDifference.
- Analysis sessions
- APK metadata
- Call graphs and inspection
- Graph export and comparison
- Taint analysis
- Instrumentation
- Cancellation and logging
- Migration and troubleshooting
- Publishing releases