diff --git a/core/src/main/java/de/robv/android/xposed/XposedHelpers.java b/core/src/main/java/de/robv/android/xposed/XposedHelpers.java index 684672d0..7dd28ba4 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedHelpers.java +++ b/core/src/main/java/de/robv/android/xposed/XposedHelpers.java @@ -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 fieldCache = new HashMap<>(); - private static final HashMap methodCache = new HashMap<>(); - private static final HashMap> constructorCache = new HashMap<>(); + private static final ConcurrentHashMap> fieldCache = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> methodCache = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap>> constructorCache = new ConcurrentHashMap<>(); private static final WeakHashMap> additionalFields = new WeakHashMap<>(); private static final HashMap> 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. + *

+ * So in fact we only need to use the structural comparison results of the reflection object. + * + * @see benchmarks for ART + * @see benchmarks for JVM + */ + 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 { *

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 { *

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