Skip to content

Instantly share code, notes, and snippets.

@pyricau
Last active December 19, 2022 19:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pyricau/02f6d6c8d0302b196326ca59c5e708cd to your computer and use it in GitHub Desktop.
Save pyricau/02f6d6c8d0302b196326ca59c5e708cd to your computer and use it in GitHub Desktop.
ClassShot: detect which classes have been loaded at runtime
import android.content.Context;
import android.os.Debug;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import com.squareup.haha.perflib.ClassObj;
import com.squareup.haha.perflib.HprofParser;
import com.squareup.haha.perflib.Snapshot;
import com.squareup.haha.perflib.io.HprofBuffer;
import com.squareup.haha.perflib.io.MemoryMappedFileBuffer;
import dalvik.system.DexFile;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* Takes a snapshot of which classes are loaded and which are not at a given moment in time, and
* stores the result to the file system.
*/
public final class ClassShot {
private static final String TAG = ClassShot.class.getSimpleName();
public static void scan(Context context) {
Context appContext = context.getApplicationContext();
new Thread(() -> scanInBackground(appContext), "SquareThread-classhot").start();
}
private static void scanInBackground(Context context) {
try {
Map<String, Set<String>> loadedByPackage = new LinkedHashMap<>();
Map<String, Set<String>> nonLoadedByPackage = new LinkedHashMap<>();
Set<String> loadedClasses = findLoadedClasses(context);
for (String className : findClassesInApk(context)) {
if (!ignore(className)) {
Map<String, Set<String>> packages;
if (loadedClasses.contains(className)) {
packages = loadedByPackage;
} else {
packages = nonLoadedByPackage;
}
Set<String> classes = getPackageClasses(packages, className);
classes.add(className);
}
}
saveToFile(context, "loaded-classes", loadedByPackage);
saveToFile(context, "non-loaded-classes", nonLoadedByPackage);
new Handler(Looper.getMainLooper()).post(
() -> Toast.makeText(context, "Classes dumped, check Logcat.", Toast.LENGTH_LONG).show());
} catch (Exception exception) {
Log.d(TAG, "Could not scan for loaded / unloaded classes", exception);
}
}
private static Set<String> findClassesInApk(Context context) throws IOException {
DexFile dexFile = null;
try {
//noinspection deprecation
dexFile = new DexFile(context.getPackageCodePath());
Set<String> classesInDex = new LinkedHashSet<>();
for (Enumeration<String> entries = dexFile.entries(); entries.hasMoreElements(); ) {
classesInDex.add(entries.nextElement());
}
return classesInDex;
} finally {
if (dexFile != null) {
dexFile.close();
}
}
}
private static Set<String> findLoadedClasses(Context context) throws IOException {
File outputDir = context.getCacheDir();
File heapDumpFile = null;
try {
heapDumpFile = File.createTempFile("classes", "hprof", outputDir);
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
Snapshot snapshot = parser.parse();
Set<String> loadedClasses = new LinkedHashSet<>();
for (ClassObj loadedClass : snapshot.findAllDescendantClasses(Object.class.getName())) {
loadedClasses.add(loadedClass.getClassName());
}
return loadedClasses;
} finally {
if (heapDumpFile != null) {
//noinspection ResultOfMethodCallIgnored
heapDumpFile.delete();
}
}
}
private static Set<String> getPackageClasses(Map<String, Set<String>> classesByPackage,
String className) {
int endPackageIndex = className.lastIndexOf('.');
String packageName;
if (endPackageIndex != -1) {
packageName = className.substring(0, endPackageIndex);
} else {
packageName = "<root>";
}
Set<String> packageClasses = classesByPackage.get(packageName);
if (packageClasses == null) {
packageClasses = new LinkedHashSet<>();
classesByPackage.put(packageName, packageClasses);
}
return packageClasses;
}
private static boolean ignore(String className) {
// [] for arrays, we've also seen "[retrofit.client.OkClient$1]"
if (className.endsWith("]")) {
return true;
}
if (className.equals(ClassShot.class.getName())) {
return true;
}
if (className.startsWith("com.squareup.haha")) {
return true;
}
return false;
}
private static void saveToFile(Context context, String fileName,
Map<String, Set<String>> sortedPackages) throws IOException {
FileWriter fileWriter = null;
try {
File directory = context.getExternalFilesDir("ReaderSDK");
File file = new File(directory, fileName);
fileWriter = new FileWriter(file, false);
fileWriter.write("File created on " + new Date() + "\n");
for (Map.Entry<String, Set<String>> entry : sortedPackages.entrySet()) {
fileWriter.write("\n------------------------------\n");
String packageName = entry.getKey();
Set<String> packageClasses = entry.getValue();
fileWriter.write(packageName + " " + packageClasses.size() + "\n");
for (String className : packageClasses) {
fileWriter.write(className + "\n");
}
}
Log.d(TAG, "Saved " + fileName + ", get it with \"adb pull " + file.getAbsolutePath() + "\"");
} finally {
if (fileWriter != null) {
fileWriter.close();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment