Fix performance traps of reflection members in `XposedHelpers.java` (#1719)
This commit is contained in:
parent
71dd8f07a1
commit
583be18a7b
|
|
@ -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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue