Last active
December 19, 2022 19:13
-
-
Save pyricau/02f6d6c8d0302b196326ca59c5e708cd to your computer and use it in GitHub Desktop.
ClassShot: detect which classes have been loaded at runtime
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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