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.AssetManager;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.apache.commons.lang3.ClassUtils;
|
import org.apache.commons.lang3.ClassUtils;
|
||||||
import org.apache.commons.lang3.reflect.MemberUtilsX;
|
import org.apache.commons.lang3.reflect.MemberUtilsX;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
@ -40,10 +42,14 @@ import java.lang.reflect.Modifier;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -53,12 +59,128 @@ public final class XposedHelpers {
|
||||||
private XposedHelpers() {
|
private XposedHelpers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final HashMap<String, Field> fieldCache = new HashMap<>();
|
private static final ConcurrentHashMap<MemberCacheKey.Field, Optional<Field>> fieldCache = new ConcurrentHashMap<>();
|
||||||
private static final HashMap<String, Method> methodCache = new HashMap<>();
|
private static final ConcurrentHashMap<MemberCacheKey.Method, Optional<Method>> methodCache = new ConcurrentHashMap<>();
|
||||||
private static final HashMap<String, Constructor<?>> constructorCache = new HashMap<>();
|
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 WeakHashMap<Object, HashMap<String, Object>> additionalFields = new WeakHashMap<>();
|
||||||
private static final HashMap<String, ThreadLocal<AtomicInteger>> sMethodDepth = new HashMap<>();
|
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.
|
* 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.
|
* @throws NoSuchFieldError In case the field was not found.
|
||||||
*/
|
*/
|
||||||
public static Field findField(Class<?> clazz, String fieldName) {
|
public static Field findField(Class<?> clazz, String fieldName) {
|
||||||
String fullFieldName = clazz.getName() + '#' + fieldName;
|
var key = new MemberCacheKey.Field(clazz, fieldName);
|
||||||
|
|
||||||
if (fieldCache.containsKey(fullFieldName)) {
|
return fieldCache.computeIfAbsent(key, k -> {
|
||||||
Field field = fieldCache.get(fullFieldName);
|
try {
|
||||||
if (field == null)
|
Field newField = findFieldRecursiveImpl(k.clazz, k.name);
|
||||||
throw new NoSuchFieldError(fullFieldName);
|
newField.setAccessible(true);
|
||||||
return field;
|
return Optional.of(newField);
|
||||||
}
|
} catch (NoSuchFieldException e) {
|
||||||
|
return Optional.empty();
|
||||||
try {
|
}
|
||||||
Field field = findFieldRecursiveImpl(clazz, fieldName);
|
}).orElseThrow(() -> new NoSuchFieldError(key.toString()));
|
||||||
field.setAccessible(true);
|
|
||||||
fieldCache.put(fullFieldName, field);
|
|
||||||
return field;
|
|
||||||
} catch (NoSuchFieldException e) {
|
|
||||||
fieldCache.put(fullFieldName, null);
|
|
||||||
throw new NoSuchFieldError(fullFieldName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -340,24 +455,17 @@ public final class XposedHelpers {
|
||||||
* <p>This variant requires that you already have reference to all the parameter types.
|
* <p>This variant requires that you already have reference to all the parameter types.
|
||||||
*/
|
*/
|
||||||
public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
|
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)) {
|
return methodCache.computeIfAbsent(key, k -> {
|
||||||
Method method = methodCache.get(fullMethodName);
|
try {
|
||||||
if (method == null)
|
Method method = k.clazz.getDeclaredMethod(k.name, k.parameters);
|
||||||
throw new NoSuchMethodError(fullMethodName);
|
method.setAccessible(true);
|
||||||
return method;
|
return Optional.of(method);
|
||||||
}
|
} catch (NoSuchMethodException e) {
|
||||||
|
return Optional.empty();
|
||||||
try {
|
}
|
||||||
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
|
}).orElseThrow(() -> new NoSuchMethodError(key.toString()));
|
||||||
method.setAccessible(true);
|
|
||||||
methodCache.put(fullMethodName, method);
|
|
||||||
return method;
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
methodCache.put(fullMethodName, null);
|
|
||||||
throw new NoSuchMethodError(fullMethodName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -412,54 +520,49 @@ public final class XposedHelpers {
|
||||||
* @throws NoSuchMethodError In case no suitable method was found.
|
* @throws NoSuchMethodError In case no suitable method was found.
|
||||||
*/
|
*/
|
||||||
public static Method findMethodBestMatch(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
|
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)) {
|
return methodCache.computeIfAbsent(key, k -> {
|
||||||
Method method = methodCache.get(fullMethodName);
|
// find the exact matching method first
|
||||||
if (method == null)
|
try {
|
||||||
throw new NoSuchMethodError(fullMethodName);
|
return Optional.of(findMethodExact(k.clazz, k.name, k.parameters));
|
||||||
return method;
|
} catch (NoSuchMethodError ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// then find the best match
|
||||||
Method method = findMethodExact(clazz, methodName, parameterTypes);
|
Method bestMatch = null;
|
||||||
methodCache.put(fullMethodName, method);
|
Class<?> clz = k.clazz;
|
||||||
return method;
|
boolean considerPrivateMethods = true;
|
||||||
} catch (NoSuchMethodError ignored) {
|
do {
|
||||||
}
|
for (Method method : clz.getDeclaredMethods()) {
|
||||||
|
// don't consider private methods of superclasses
|
||||||
|
if (!considerPrivateMethods && Modifier.isPrivate(method.getModifiers()))
|
||||||
|
continue;
|
||||||
|
|
||||||
Method bestMatch = null;
|
// compare name and parameters
|
||||||
Class<?> clz = clazz;
|
if (method.getName().equals(k.name) && ClassUtils.isAssignable(
|
||||||
boolean considerPrivateMethods = true;
|
k.parameters,
|
||||||
do {
|
method.getParameterTypes(),
|
||||||
for (Method method : clz.getDeclaredMethods()) {
|
true)) {
|
||||||
// don't consider private methods of superclasses
|
// get accessible version of method
|
||||||
if (!considerPrivateMethods && Modifier.isPrivate(method.getModifiers()))
|
if (bestMatch == null || MemberUtilsX.compareMethodFit(
|
||||||
continue;
|
method,
|
||||||
|
bestMatch,
|
||||||
// compare name and parameters
|
k.parameters) < 0) {
|
||||||
if (method.getName().equals(methodName) && ClassUtils.isAssignable(parameterTypes, method.getParameterTypes(), true)) {
|
bestMatch = method;
|
||||||
// get accessible version of method
|
}
|
||||||
if (bestMatch == null || MemberUtilsX.compareMethodFit(
|
|
||||||
method,
|
|
||||||
bestMatch,
|
|
||||||
parameterTypes) < 0) {
|
|
||||||
bestMatch = method;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
considerPrivateMethods = false;
|
||||||
considerPrivateMethods = false;
|
} while ((clz = clz.getSuperclass()) != null);
|
||||||
} while ((clz = clz.getSuperclass()) != null);
|
|
||||||
|
|
||||||
if (bestMatch != null) {
|
if (bestMatch != null) {
|
||||||
bestMatch.setAccessible(true);
|
bestMatch.setAccessible(true);
|
||||||
methodCache.put(fullMethodName, bestMatch);
|
return Optional.of(bestMatch);
|
||||||
return bestMatch;
|
} else {
|
||||||
} else {
|
return Optional.empty();
|
||||||
NoSuchMethodError e = new NoSuchMethodError(fullMethodName);
|
}
|
||||||
methodCache.put(fullMethodName, null);
|
}).orElseThrow(() -> new NoSuchMethodError(key.toString()));
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -605,24 +708,17 @@ public final class XposedHelpers {
|
||||||
* See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details.
|
* See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details.
|
||||||
*/
|
*/
|
||||||
public static Constructor<?> findConstructorExact(Class<?> clazz, Class<?>... parameterTypes) {
|
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)) {
|
return constructorCache.computeIfAbsent(key, k -> {
|
||||||
Constructor<?> constructor = constructorCache.get(fullConstructorName);
|
try {
|
||||||
if (constructor == null)
|
Constructor<?> constructor = clazz.getDeclaredConstructor(parameterTypes);
|
||||||
throw new NoSuchMethodError(fullConstructorName);
|
constructor.setAccessible(true);
|
||||||
return constructor;
|
return Optional.of(constructor);
|
||||||
}
|
} catch (NoSuchMethodException e) {
|
||||||
|
return Optional.empty();
|
||||||
try {
|
}
|
||||||
Constructor<?> constructor = clazz.getDeclaredConstructor(parameterTypes);
|
}).orElseThrow(() -> new NoSuchMethodError(key.toString()));
|
||||||
constructor.setAccessible(true);
|
|
||||||
constructorCache.put(fullConstructorName, constructor);
|
|
||||||
return constructor;
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
constructorCache.put(fullConstructorName, null);
|
|
||||||
throw new NoSuchMethodError(fullConstructorName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -653,46 +749,41 @@ public final class XposedHelpers {
|
||||||
* <p>See {@link #findMethodBestMatch(Class, String, Class...)} for details.
|
* <p>See {@link #findMethodBestMatch(Class, String, Class...)} for details.
|
||||||
*/
|
*/
|
||||||
public static Constructor<?> findConstructorBestMatch(Class<?> clazz, Class<?>... parameterTypes) {
|
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)) {
|
return constructorCache.computeIfAbsent(key, k -> {
|
||||||
Constructor<?> constructor = constructorCache.get(fullConstructorName);
|
// find the exact matching constructor first
|
||||||
if (constructor == null)
|
try {
|
||||||
throw new NoSuchMethodError(fullConstructorName);
|
return Optional.of(findConstructorExact(k.clazz, k.parameters));
|
||||||
return constructor;
|
} catch (NoSuchMethodError ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// then find the best match
|
||||||
Constructor<?> constructor = findConstructorExact(clazz, parameterTypes);
|
Constructor<?> bestMatch = null;
|
||||||
constructorCache.put(fullConstructorName, constructor);
|
Constructor<?>[] constructors = k.clazz.getDeclaredConstructors();
|
||||||
return constructor;
|
for (Constructor<?> constructor : constructors) {
|
||||||
} catch (NoSuchMethodError ignored) {
|
// compare name and parameters
|
||||||
}
|
if (ClassUtils.isAssignable(
|
||||||
|
k.parameters,
|
||||||
Constructor<?> bestMatch = null;
|
constructor.getParameterTypes(),
|
||||||
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
|
true)) {
|
||||||
for (Constructor<?> constructor : constructors) {
|
// get accessible version of method
|
||||||
// compare name and parameters
|
if (bestMatch == null || MemberUtilsX.compareConstructorFit(
|
||||||
if (ClassUtils.isAssignable(parameterTypes, constructor.getParameterTypes(), true)) {
|
constructor,
|
||||||
// get accessible version of method
|
bestMatch,
|
||||||
if (bestMatch == null || MemberUtilsX.compareConstructorFit(
|
k.parameters) < 0) {
|
||||||
constructor,
|
bestMatch = constructor;
|
||||||
bestMatch,
|
}
|
||||||
parameterTypes) < 0) {
|
|
||||||
bestMatch = constructor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (bestMatch != null) {
|
if (bestMatch != null) {
|
||||||
bestMatch.setAccessible(true);
|
bestMatch.setAccessible(true);
|
||||||
constructorCache.put(fullConstructorName, bestMatch);
|
return Optional.of(bestMatch);
|
||||||
return bestMatch;
|
} else {
|
||||||
} else {
|
return Optional.empty();
|
||||||
NoSuchMethodError e = new NoSuchMethodError(fullConstructorName);
|
}
|
||||||
constructorCache.put(fullConstructorName, null);
|
}).orElseThrow(() -> new NoSuchMethodError(key.toString()));
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue