Fix performance traps of reflection members in `XposedHelpers.java` (#1719)

This commit is contained in:
清茶 2022-02-25 16:01:28 +08:00 committed by GitHub
parent 71dd8f07a1
commit 583be18a7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 223 additions and 132 deletions

View File

@ -23,11 +23,13 @@ package de.robv.android.xposed;
import android.content.res.AssetManager;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.reflect.MemberUtilsX;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -40,10 +42,14 @@ import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
@ -53,12 +59,128 @@ public final class XposedHelpers {
private XposedHelpers() {
}
private static final HashMap<String, Field> fieldCache = new HashMap<>();
private static final HashMap<String, Method> methodCache = new HashMap<>();
private static final HashMap<String, Constructor<?>> constructorCache = new HashMap<>();
private static final ConcurrentHashMap<MemberCacheKey.Field, Optional<Field>> fieldCache = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<MemberCacheKey.Method, Optional<Method>> methodCache = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<MemberCacheKey.Constructor, Optional<Constructor<?>>> constructorCache = new ConcurrentHashMap<>();
private static final WeakHashMap<Object, HashMap<String, Object>> additionalFields = new WeakHashMap<>();
private static final HashMap<String, ThreadLocal<AtomicInteger>> sMethodDepth = new HashMap<>();
/**
* Note that we use object key instead of string here, because string calculation will lose all
* the benefits of 'HashMap', this is basically the solution of performance traps.
* <p>
* So in fact we only need to use the structural comparison results of the reflection object.
*
* @see <a href="https://github.com/RinOrz/LSPosed/blob/a44e1f1cdf0c5e5ebfaface828e5907f5425df1b/benchmark/src/result/ReflectionCacheBenchmark.json">benchmarks for ART</a>
* @see <a href="https://github.com/meowool-catnip/cloak/blob/main/api/src/benchmark/kotlin/com/meowool/cloak/ReflectionObjectAccessTests.kt#L37-L65">benchmarks for JVM</a>
*/
private abstract static class MemberCacheKey {
private final int hash;
protected MemberCacheKey(int hash) {
this.hash = hash;
}
@Override
public abstract boolean equals(@Nullable Object obj);
@Override
public final int hashCode() {
return hash;
}
static final class Constructor extends MemberCacheKey {
private final Class<?> clazz;
private final Class<?>[] parameters;
private final boolean isExact;
public Constructor(Class<?> clazz, Class<?>[] parameters, boolean isExact) {
super(31 * Objects.hash(clazz, isExact) + Arrays.hashCode(parameters));
this.clazz = clazz;
this.parameters = parameters;
this.isExact = isExact;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Constructor)) return false;
Constructor that = (Constructor) o;
return isExact == that.isExact && Objects.equals(clazz, that.clazz) && Arrays.equals(parameters, that.parameters);
}
@NonNull
@Override
public String toString() {
var str = clazz.getName() + getParametersString(parameters);
if (isExact) {
return str + "#exact";
} else {
return str;
}
}
}
static final class Field extends MemberCacheKey {
private final Class<?> clazz;
private final String name;
public Field(Class<?> clazz, String name) {
super(Objects.hash(clazz, name));
this.clazz = clazz;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Field)) return false;
Field field = (Field) o;
return Objects.equals(clazz, field.clazz) && Objects.equals(name, field.name);
}
@NonNull
@Override
public String toString() {
return clazz.getName() + "#" + name;
}
}
static final class Method extends MemberCacheKey {
private final Class<?> clazz;
private final String name;
private final Class<?>[] parameters;
private final boolean isExact;
public Method(Class<?> clazz, String name, Class<?>[] parameters, boolean isExact) {
super(31 * Objects.hash(clazz, name, isExact) + Arrays.hashCode(parameters));
this.clazz = clazz;
this.name = name;
this.parameters = parameters;
this.isExact = isExact;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Method)) return false;
Method method = (Method) o;
return isExact == method.isExact && Objects.equals(clazz, method.clazz) && Objects.equals(name, method.name) && Arrays.equals(parameters, method.parameters);
}
@NonNull
@Override
public String toString() {
var str = clazz.getName() + '#' + name + getParametersString(parameters);
if (isExact) {
return str + "#exact";
} else {
return str;
}
}
}
}
/**
* Look up a class with the specified class loader.
*
@ -111,24 +233,17 @@ public final class XposedHelpers {
* @throws NoSuchFieldError In case the field was not found.
*/
public static Field findField(Class<?> clazz, String fieldName) {
String fullFieldName = clazz.getName() + '#' + fieldName;
var key = new MemberCacheKey.Field(clazz, fieldName);
if (fieldCache.containsKey(fullFieldName)) {
Field field = fieldCache.get(fullFieldName);
if (field == null)
throw new NoSuchFieldError(fullFieldName);
return field;
}
try {
Field field = findFieldRecursiveImpl(clazz, fieldName);
field.setAccessible(true);
fieldCache.put(fullFieldName, field);
return field;
} catch (NoSuchFieldException e) {
fieldCache.put(fullFieldName, null);
throw new NoSuchFieldError(fullFieldName);
}
return fieldCache.computeIfAbsent(key, k -> {
try {
Field newField = findFieldRecursiveImpl(k.clazz, k.name);
newField.setAccessible(true);
return Optional.of(newField);
} catch (NoSuchFieldException e) {
return Optional.empty();
}
}).orElseThrow(() -> new NoSuchFieldError(key.toString()));
}
/**
@ -340,24 +455,17 @@ public final class XposedHelpers {
* <p>This variant requires that you already have reference to all the parameter types.
*/
public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";
var key = new MemberCacheKey.Method(clazz, methodName, parameterTypes, true);
if (methodCache.containsKey(fullMethodName)) {
Method method = methodCache.get(fullMethodName);
if (method == null)
throw new NoSuchMethodError(fullMethodName);
return method;
}
try {
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
methodCache.put(fullMethodName, method);
return method;
} catch (NoSuchMethodException e) {
methodCache.put(fullMethodName, null);
throw new NoSuchMethodError(fullMethodName);
}
return methodCache.computeIfAbsent(key, k -> {
try {
Method method = k.clazz.getDeclaredMethod(k.name, k.parameters);
method.setAccessible(true);
return Optional.of(method);
} catch (NoSuchMethodException e) {
return Optional.empty();
}
}).orElseThrow(() -> new NoSuchMethodError(key.toString()));
}
/**
@ -412,54 +520,49 @@ public final class XposedHelpers {
* @throws NoSuchMethodError In case no suitable method was found.
*/
public static Method findMethodBestMatch(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#bestmatch";
var key = new MemberCacheKey.Method(clazz, methodName, parameterTypes, false);
if (methodCache.containsKey(fullMethodName)) {
Method method = methodCache.get(fullMethodName);
if (method == null)
throw new NoSuchMethodError(fullMethodName);
return method;
}
return methodCache.computeIfAbsent(key, k -> {
// find the exact matching method first
try {
return Optional.of(findMethodExact(k.clazz, k.name, k.parameters));
} catch (NoSuchMethodError ignored) {
}
try {
Method method = findMethodExact(clazz, methodName, parameterTypes);
methodCache.put(fullMethodName, method);
return method;
} catch (NoSuchMethodError ignored) {
}
// then find the best match
Method bestMatch = null;
Class<?> clz = k.clazz;
boolean considerPrivateMethods = true;
do {
for (Method method : clz.getDeclaredMethods()) {
// don't consider private methods of superclasses
if (!considerPrivateMethods && Modifier.isPrivate(method.getModifiers()))
continue;
Method bestMatch = null;
Class<?> clz = clazz;
boolean considerPrivateMethods = true;
do {
for (Method method : clz.getDeclaredMethods()) {
// don't consider private methods of superclasses
if (!considerPrivateMethods && Modifier.isPrivate(method.getModifiers()))
continue;
// compare name and parameters
if (method.getName().equals(methodName) && ClassUtils.isAssignable(parameterTypes, method.getParameterTypes(), true)) {
// get accessible version of method
if (bestMatch == null || MemberUtilsX.compareMethodFit(
method,
bestMatch,
parameterTypes) < 0) {
bestMatch = method;
// compare name and parameters
if (method.getName().equals(k.name) && ClassUtils.isAssignable(
k.parameters,
method.getParameterTypes(),
true)) {
// get accessible version of method
if (bestMatch == null || MemberUtilsX.compareMethodFit(
method,
bestMatch,
k.parameters) < 0) {
bestMatch = method;
}
}
}
}
considerPrivateMethods = false;
} while ((clz = clz.getSuperclass()) != null);
considerPrivateMethods = false;
} while ((clz = clz.getSuperclass()) != null);
if (bestMatch != null) {
bestMatch.setAccessible(true);
methodCache.put(fullMethodName, bestMatch);
return bestMatch;
} else {
NoSuchMethodError e = new NoSuchMethodError(fullMethodName);
methodCache.put(fullMethodName, null);
throw e;
}
if (bestMatch != null) {
bestMatch.setAccessible(true);
return Optional.of(bestMatch);
} else {
return Optional.empty();
}
}).orElseThrow(() -> new NoSuchMethodError(key.toString()));
}
/**
@ -605,24 +708,17 @@ public final class XposedHelpers {
* See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details.
*/
public static Constructor<?> findConstructorExact(Class<?> clazz, Class<?>... parameterTypes) {
String fullConstructorName = clazz.getName() + getParametersString(parameterTypes) + "#exact";
var key = new MemberCacheKey.Constructor(clazz, parameterTypes, true);
if (constructorCache.containsKey(fullConstructorName)) {
Constructor<?> constructor = constructorCache.get(fullConstructorName);
if (constructor == null)
throw new NoSuchMethodError(fullConstructorName);
return constructor;
}
try {
Constructor<?> constructor = clazz.getDeclaredConstructor(parameterTypes);
constructor.setAccessible(true);
constructorCache.put(fullConstructorName, constructor);
return constructor;
} catch (NoSuchMethodException e) {
constructorCache.put(fullConstructorName, null);
throw new NoSuchMethodError(fullConstructorName);
}
return constructorCache.computeIfAbsent(key, k -> {
try {
Constructor<?> constructor = clazz.getDeclaredConstructor(parameterTypes);
constructor.setAccessible(true);
return Optional.of(constructor);
} catch (NoSuchMethodException e) {
return Optional.empty();
}
}).orElseThrow(() -> new NoSuchMethodError(key.toString()));
}
/**
@ -653,46 +749,41 @@ public final class XposedHelpers {
* <p>See {@link #findMethodBestMatch(Class, String, Class...)} for details.
*/
public static Constructor<?> findConstructorBestMatch(Class<?> clazz, Class<?>... parameterTypes) {
String fullConstructorName = clazz.getName() + getParametersString(parameterTypes) + "#bestmatch";
var key = new MemberCacheKey.Constructor(clazz, parameterTypes, false);
if (constructorCache.containsKey(fullConstructorName)) {
Constructor<?> constructor = constructorCache.get(fullConstructorName);
if (constructor == null)
throw new NoSuchMethodError(fullConstructorName);
return constructor;
}
return constructorCache.computeIfAbsent(key, k -> {
// find the exact matching constructor first
try {
return Optional.of(findConstructorExact(k.clazz, k.parameters));
} catch (NoSuchMethodError ignored) {
}
try {
Constructor<?> constructor = findConstructorExact(clazz, parameterTypes);
constructorCache.put(fullConstructorName, constructor);
return constructor;
} catch (NoSuchMethodError ignored) {
}
Constructor<?> bestMatch = null;
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
// compare name and parameters
if (ClassUtils.isAssignable(parameterTypes, constructor.getParameterTypes(), true)) {
// get accessible version of method
if (bestMatch == null || MemberUtilsX.compareConstructorFit(
constructor,
bestMatch,
parameterTypes) < 0) {
bestMatch = constructor;
// then find the best match
Constructor<?> bestMatch = null;
Constructor<?>[] constructors = k.clazz.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
// compare name and parameters
if (ClassUtils.isAssignable(
k.parameters,
constructor.getParameterTypes(),
true)) {
// get accessible version of method
if (bestMatch == null || MemberUtilsX.compareConstructorFit(
constructor,
bestMatch,
k.parameters) < 0) {
bestMatch = constructor;
}
}
}
}
if (bestMatch != null) {
bestMatch.setAccessible(true);
constructorCache.put(fullConstructorName, bestMatch);
return bestMatch;
} else {
NoSuchMethodError e = new NoSuchMethodError(fullConstructorName);
constructorCache.put(fullConstructorName, null);
throw e;
}
if (bestMatch != null) {
bestMatch.setAccessible(true);
return Optional.of(bestMatch);
} else {
return Optional.empty();
}
}).orElseThrow(() -> new NoSuchMethodError(key.toString()));
}
/**