Upgrade Gradle and AGP, add docs and impl save cache#9
Upgrade Gradle and AGP, add docs and impl save cache#9Howard20181 wants to merge 80 commits intolibxposed:masterfrom
Conversation
|
尝试实现按条件查找方法,发现 LSPosedDexParser 返回的 invoked_methods 全是空的 |
There was a problem hiding this comment.
Pull request overview
This PR modernizes the build/tooling (Gradle wrapper + Android Gradle Plugin), expands public API documentation for HookBuilder, and adds infrastructure intended to persist and reuse DEX/matcher caches across runs (including new DEX-invocation relationship collection).
Changes:
- Upgrade Gradle wrapper and AGP; add Foojay toolchain resolver support.
- Add extensive Javadoc/KDoc for
HookBuilderand Kotlin extensions. - Add (de)serialization for
MatchCacheand introduce method/constructor invocation relationship tracking during DEX analysis.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| settings.gradle.kts | Upgrades AGP and adds Foojay toolchain resolver plugin. |
| helper/build.gradle.kts | Bumps compileSdk and androidx annotation dependency versions. |
| helper/proguard-rules.pro | Reworks R8/Proguard rules for reflection-heavy helper library. |
| helper/src/main/java/io/github/libxposed/helper/Misc.java | Adds cache encoding helpers for reflection objects. |
| helper/src/main/java/io/github/libxposed/helper/HookBuilderImpl.java | Implements cache save, adjusts DEX reading, adds invocation graph collection, and wires cache observers. |
| helper/src/main/java/io/github/libxposed/helper/HookBuilder.java | Adds extensive API documentation and usage examples. |
| helper-ktx/src/main/kotlin/io/github/libxposed/helper/ktx/HookBuilderKt.kt | Adds documentation for parameter matching and signature helpers. |
| helper-ktx/build.gradle.kts | Updates compileSdk and fixes signing to run only when keys exist. |
| gradlew / gradlew.bat | Wrapper script updates from Gradle upgrade. |
| gradle/wrapper/gradle-wrapper.properties | Points wrapper at Gradle 8.14.4 and adds wrapper safety knobs. |
| gradle/wrapper/gradle-wrapper.jar | Updates wrapper JAR to match the new Gradle version. |
| gradle/gradle-daemon-jvm.properties | Adds generated Foojay toolchain URLs / JVM toolchain settings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static String encodeField(@Nullable java.lang.reflect.Field field) { | ||
| if (field == null) return ""; | ||
| return field.getDeclaringClass().getName() + "->" + field.getName() + ":" + field.getType().getName(); | ||
| } | ||
|
|
||
| /** | ||
| * Encode a Method to its string representation for caching. | ||
| * Format: declaringClass->methodName(param1,param2,...)returnType | ||
| * Returns empty string if method is null. | ||
| */ | ||
| @NonNull | ||
| static String encodeMethod(@Nullable java.lang.reflect.Method method) { | ||
| if (method == null) return ""; | ||
| var params = new StringBuilder(); | ||
| var parameterTypes = method.getParameterTypes(); | ||
| for (int i = 0; i < parameterTypes.length; i++) { | ||
| if (i > 0) params.append(","); | ||
| params.append(parameterTypes[i].getName()); | ||
| } | ||
| return method.getDeclaringClass().getName() + "->" + method.getName() + "(" + params + ")" + method.getReturnType().getName(); | ||
| } | ||
|
|
||
| /** | ||
| * Encode a Constructor to its string representation for caching. | ||
| * Format: declaringClass-><init>(param1,param2,...)V | ||
| * Returns empty string if constructor is null. | ||
| */ | ||
| @NonNull | ||
| static String encodeConstructor(@Nullable java.lang.reflect.Constructor<?> constructor) { | ||
| if (constructor == null) return ""; | ||
| var params = new StringBuilder(); | ||
| var parameterTypes = constructor.getParameterTypes(); | ||
| for (int i = 0; i < parameterTypes.length; i++) { | ||
| if (i > 0) params.append(","); | ||
| params.append(parameterTypes[i].getName()); | ||
| } | ||
| return constructor.getDeclaringClass().getName() + "-><init>(" + params + ")V"; | ||
| } |
There was a problem hiding this comment.
encodeField/encodeMethod/encodeConstructor generate signatures using Java type names + commas, but the cache reload path feeds these strings into Reflector.loadField/loadMethod/loadConstructor, which expect either Smali descriptors (when using ->) or the specific Java-like formats those parsers support. As-is, cached entries for methods/constructors/fields will fail to deserialize and will always miss; please align the encoded format with what Reflector can parse (e.g., emit Smali descriptors, or emit the non--> Java-like formats that Reflector supports).
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback align the encoded format with Smali descriptors
| } catch (Throwable e) { | ||
| if (exceptionHandler != null) exceptionHandler.test(e); | ||
| return; | ||
| } |
There was a problem hiding this comment.
The outer catch (Throwable e) in analysisDex() now silently returns without invoking exceptionHandler. This makes DEX parsing failures very hard to diagnose and changes behavior from other error paths in this class; please report the exception via exceptionHandler (or rethrow) before returning.
| private Class<?>[] getParameterTypesFromProto(DexParser.ProtoId protoId) { | ||
| try { | ||
| var paramTypeIds = protoId.getParameters(); | ||
| if (paramTypeIds == null || paramTypeIds.length == 0) { | ||
| return new Class<?>[0]; | ||
| } | ||
| var paramTypes = new Class<?>[paramTypeIds.length]; | ||
| for (int i = 0; i < paramTypeIds.length; i++) { | ||
| var descriptor = paramTypeIds[i].getDescriptor().getString(); | ||
| paramTypes[i] = reflector.loadClass(descriptor); | ||
| } |
There was a problem hiding this comment.
getParameterTypesFromProto() passes DEX descriptors directly to reflector.loadClass(). For primitive descriptors (e.g., "I", "Z"), Reflector.loadClass() will throw ClassNotFoundException, so any method with primitive parameters will fail to resolve and be skipped. Please map primitive descriptors to their primitive Class (and handle arrays) before falling back to reflector.loadClass() for reference types.
| // Check invoked methods constraint | ||
| if (invokedMethods != null && reflect instanceof Method) { | ||
| var currentMethod = (Method) reflect; | ||
| var invokedMethodsSet = methodInvocationsMap.get(currentMethod); | ||
| if (invokedMethodsSet == null || invokedMethodsSet.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| // Test if the invoked methods set matches the constraint | ||
| var hashSet = new HashSet<>(invokedMethodsSet); | ||
| if (!invokedMethods.test(hashSet)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Check invoked constructors constraint | ||
| if (invokedConstructors != null && reflect instanceof Method) { | ||
| var currentMethod = (Method) reflect; | ||
| var invokedConstructorsSet = constructorInvocationsMap.get(currentMethod); | ||
| if (invokedConstructorsSet == null || invokedConstructorsSet.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| // Test if the invoked constructors set matches the constraint | ||
| var hashSet = new HashSet<>(invokedConstructorsSet); | ||
| return invokedConstructors.test(hashSet); | ||
| } |
There was a problem hiding this comment.
setInvokedMethods/setInvokedConstructors are part of ExecutableMatcher (methods + constructors), but the constraint checks here only run when reflect instanceof Method. As a result, these filters will never apply to constructors, and analysisDex() also resolves the current executable via getDeclaredMethod, so <init> bodies are skipped entirely. If constructors are intended to be supported, collect invocation relationships for constructors too and apply these constraints for Constructor<?> candidates.
| public final FieldLazySequence getAssignedFields() { | ||
| dexAnalysis = true; | ||
| return null; | ||
| final var m = new FieldLazySequenceImpl(rootMatcher); | ||
| addObserver((ItemObserver<Reflect>) result -> { | ||
| // DEX analysis will populate assigned fields | ||
| // The actual implementation requires DEX parsing to extract field assignment information | ||
| m.match(Collections.emptyList()); | ||
| }); | ||
| return m; | ||
| } | ||
|
|
||
| @DexAnalysis | ||
| @NonNull | ||
| @Override | ||
| public final FieldLazySequence getAccessedFields() { | ||
| dexAnalysis = true; | ||
| return null; | ||
| final var m = new FieldLazySequenceImpl(rootMatcher); | ||
| addObserver((ItemObserver<Reflect>) result -> { | ||
| // DEX analysis will populate accessed fields | ||
| // The actual implementation requires DEX parsing to extract field access information | ||
| m.match(Collections.emptyList()); | ||
| }); | ||
| return m; | ||
| } | ||
|
|
||
| @DexAnalysis | ||
| @NonNull | ||
| @Override | ||
| public final MethodLazySequence getInvokedMethods() { | ||
| dexAnalysis = true; | ||
| return null; | ||
| final var m = new MethodLazySequenceImpl(rootMatcher); | ||
| addObserver((ItemObserver<Reflect>) result -> { | ||
| // DEX analysis will populate invoked methods | ||
| // The actual implementation requires DEX parsing to extract method invocation information | ||
| m.match(Collections.emptyList()); | ||
| }); | ||
| return m; | ||
| } | ||
|
|
||
| @DexAnalysis | ||
| @NonNull | ||
| @Override | ||
| public final ConstructorLazySequence getInvokedConstructors() { | ||
| dexAnalysis = true; | ||
| return null; | ||
| final var m = new ConstructorLazySequenceImpl(rootMatcher); | ||
| addObserver((ItemObserver<Reflect>) result -> { | ||
| // DEX analysis will populate invoked constructors | ||
| // The actual implementation requires DEX parsing to extract constructor invocation information | ||
| m.match(Collections.emptyList()); | ||
| }); | ||
| return m; |
There was a problem hiding this comment.
getAssignedFields/getAccessedFields/getInvokedMethods/getInvokedConstructors now return non-null sequences but they are always matched with Collections.emptyList(). Given the interface contract/docstrings, callers will expect these sequences to reflect actual DEX analysis results. Please either wire these accessors to the data produced by analysisDex() (e.g., the invocation maps), or keep the previous behavior but avoid violating @NonNull/contract expectations (e.g., throw an explicit unsupported exception).
…l.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…l.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…l.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…l.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…l.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…l.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…l.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
The observer was guarded by `if (key != null && matchCache != null)` but `key` is assigned only after the sequence is constructed (in onBuild()), meaning the observer would never be added and constructor list results wouldn't be persisted into matchCache. Changed to match the pattern used by all other LazySequenceImpl classes: register the observer whenever matchCache != null and check key inside the callback. Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
…n DEX analysis Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
…s null Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
…vior Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
… ignoring Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
…ionMap usage Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Replace Executable keys with Member for method/constructor/field maps to unify reflection handling and remove the asExecutable helper. Update constraint evaluation to check assignedFields and accessedFields against the current Member, returning early when constraints fail. Small cleanups: convert parameter type loops to enhanced for-loops in MatchCache and add null-safety checks when resolving primitive classes in Reflector.
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Fix non-idempotent Future.get() causing repeated cache write failures
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
No description provided.