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.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;
}
} }
/** /**