├── .gitignore ├── LICENSE.txt ├── build.gradle.kts ├── consumer-rules.pro ├── libs ├── api-82-sources.jar └── api-82.jar ├── proguard-rules.pro ├── readme.md └── src └── main ├── AndroidManifest.xml └── java └── io └── github └── xpler ├── XplerEntrance.kt ├── XplerState.kt ├── core ├── XplerHelper.kt ├── XplerHelperExt.kt ├── XplerLog.kt ├── XplerModule.kt ├── callback │ ├── MethodHookCallbackImpl.kt │ └── MethodReplacementCallbackImpl.kt ├── entity │ ├── CallConstructors.kt │ ├── CallMethods.kt │ ├── EmptyHook.kt │ ├── HookEntity.kt │ └── NoneHook.kt ├── entrance │ ├── ApplicationHookStart.kt │ ├── DefaultHookStart.kt │ └── HookStart.kt ├── hook │ ├── ConstructorHook.kt │ ├── ConstructorHookImpl.kt │ ├── MethodHook.kt │ └── MethodHookImpl.kt └── proxy │ ├── LoadParam.kt │ ├── MethodParam.kt │ └── MethodUnhook.kt ├── loader └── XplerClassloader.kt └── utils └── XplerUtils.kt /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | secring.gpg 3 | signing.properties 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | id("com.android.library") 5 | id("org.jetbrains.kotlin.android") 6 | id("com.vanniktech.maven.publish") version "0.29.0" 7 | id("signing") 8 | } 9 | 10 | android { 11 | namespace = "io.github.xpler" 12 | compileSdk = 34 13 | 14 | defaultConfig { 15 | minSdk = 21 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | consumerProguardFiles("consumer-rules.pro") 19 | } 20 | 21 | buildTypes { 22 | release { 23 | isMinifyEnabled = false 24 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_11 29 | targetCompatibility = JavaVersion.VERSION_11 30 | } 31 | kotlinOptions { 32 | jvmTarget = "11" 33 | } 34 | } 35 | 36 | dependencies { 37 | compileOnly(files("libs/api-82.jar")) 38 | implementation("androidx.core:core-ktx:1.1.0") 39 | implementation("androidx.appcompat:appcompat:1.1.0") 40 | } 41 | 42 | mavenPublishing { 43 | coordinates("io.github.thatworld", "xpler", "0.0.2") 44 | pom { 45 | name.set("xpler") 46 | description.set("Xpler is a library for Xposed") 47 | url.set("https://github.com/ThatWorld/xpler") 48 | licenses { 49 | license { 50 | name.set("The Apache Software License, Version 2.0") 51 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 52 | } 53 | } 54 | developers { 55 | developer { 56 | name.set("Gang") 57 | url.set("https://github.com/ThatWorld/xpler") 58 | } 59 | } 60 | scm { 61 | connection.set("scm:git:git://github.com/ThatWorld/xpler.git") 62 | developerConnection.set("scm:git:ssh://github.com/ThatWorld/xpler.git") 63 | url.set("https://github.com/ThatWorld/xpler.git") 64 | } 65 | } 66 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 67 | signAllPublications() 68 | } -------------------------------------------------------------------------------- /consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -keepclassmembers class io.github.xpler.core.callback.MethodHookCallbackImpl { *; } 2 | -keepclassmembers class io.github.xpler.core.callback.MethodReplacementCallbackImpl { *; } 3 | 4 | -keepclassmembers class io.github.xpler.core.proxy.LoadParam{ (...); } 5 | -keepclassmembers class io.github.xpler.core.proxy.MethodParam{ (...); } 6 | -keepclassmembers class io.github.xpler.core.proxy.MethodUnhook{ (...); } 7 | 8 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$KeepParam 9 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$Param 10 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$ReturnType 11 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$HookOnce 12 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$OnBefore 13 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$OnAfter 14 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$OnReplace 15 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$OnConstructorBefore 16 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$OnConstructorAfter 17 | -keepclassmembers, allowobfuscation class io.github.xpler.core.entity.HookEntity$OnConstructorReplace 18 | -keepclassmembers, allowobfuscation class * extends io.github.xpler.core.entity.HookEntity { 19 | @io.github.xpler.core.entity.HookEntity$KeepParam ; 20 | @io.github.xpler.core.entity.HookEntity$Param ; 21 | @io.github.xpler.core.entity.HookEntity$ReturnType ; 22 | @io.github.xpler.core.entity.HookEntity$HookOnce ; 23 | @io.github.xpler.core.entity.HookEntity$OnBefore ; 24 | @io.github.xpler.core.entity.HookEntity$OnAfter ; 25 | @io.github.xpler.core.entity.HookEntity$OnReplace ; 26 | @io.github.xpler.core.entity.HookEntity$OnConstructorBefore ; 27 | @io.github.xpler.core.entity.HookEntity$OnConstructorAfter ; 28 | @io.github.xpler.core.entity.HookEntity$OnConstructorReplace ; 29 | } 30 | 31 | -keepclassmembers class io.github.xpler.XplerEntrance { 32 | public void *(de.robv.android.xposed.IXposedHookZygoteInit$StartupParam); 33 | public void *(de.robv.android.xposed.callbacks.XC_LoadPackage$LoadPackageParam); 34 | } 35 | 36 | -keepclassmembers class io.github.xpler.XplerState { *; } 37 | -------------------------------------------------------------------------------- /libs/api-82-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThatWorld/xpler/6c134681823da7483a20659b560bc44256f83761/libs/api-82-sources.jar -------------------------------------------------------------------------------- /libs/api-82.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThatWorld/xpler/6c134681823da7483a20659b560bc44256f83761/libs/api-82.jar -------------------------------------------------------------------------------- /proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Xpler 2 | 3 | Xposed Kotlin 开发模板,更适合Kotlin编码风格。 4 | 5 | `Xpler` 在原 `Xposed Api` 基础上进一步封装,使其支持Kotlin的DSL特性,更简洁的编写Hook逻辑。 6 | 7 | 注意:使用本模板,你仍需要手动创建和配置 `xposed_init`、`application meta-data`。 8 | 9 | ## Xpler Api 10 | 11 | 作为原 `Xposed Api` 的封装项目,`Xpler` 提供了部分基本Api。 12 | 13 | ### HookEntrance.kt 14 | 15 | 作为 `Xpler` 给 `Xposed` 提供的抽象入口类,你只需要继承 `XplerEntrance`,然后实现相应接口即可。 16 | 17 | - ApplicationHookStart 18 | 19 | ```kotlin 20 | class HookInit : XplerEntrance(), ApplicationHookStart { 21 | override val modulePackage: String 22 | get() = "com.example.module" 23 | 24 | override val scopes: Set 25 | get() = setOf( 26 | "packageName" at "applicationClassName", 27 | "packageName1" at ("applicationClassName1" to "processName"), 28 | ) 29 | 30 | override fun onCreateBefore(lparam: MethodParam, hostApp: Application) { 31 | // Do not write or call the same Hook logic in onBefore and onAfter, as this is meaningless 32 | } 33 | 34 | override fun onCreateAfter(lparam: MethodParam, hostApp: Application) { 35 | HActivity() 36 | } 37 | } 38 | ``` 39 | 40 | 实现该接口后,将自动为宿主注入类加载器,你只需要在 `onCreateBefore` 或 `onCreateAfter` 中书写 41 | Hook逻辑即可。 42 | 43 | `modulePackage` 为模块包名,必须提供,`Xpler` 会用它去加载 `HookState`,以便对于模块启用/未启用状态的获取。 44 | 45 | `scopes` 为宿主列表,需提供 `宿主包名` 和 `宿主启动应用程序(Application)`、 `宿主进程名(prossName)可选` 46 | ,不在 `scopes` 列表中的包名,尽管在`Xposed`中加入生效列表,`Xpler`也不会对该宿主生效。 47 | 48 | 而如果,你只是需要一个简单的Hook,并不需要复杂操作,可以试试 `DefaultHookStart` 接口。 49 | 50 | - DefaultHookStart 51 | 52 | ```kotlin 53 | class HookInit : XplerEntrance(), DefaultHookStart { 54 | override val modulePackage: String 55 | get() = "com.example.module" 56 | 57 | override fun loadPackage(lparam: LoadParam) { 58 | // the original calling logic 59 | } 60 | } 61 | ``` 62 | 63 | 该接口提供的 `loadPackage` 方法就是原始的 `handleLoadPackage` 操作。 64 | 65 | > 记得修改`xposed_init` 中的入口类,如上述的入口类名为:`com.example.module.HookInit`。 66 | > 67 | > 还有,如果有混淆优化,记得保留 `HookInit` 入口类。 68 | > 69 | > ```protobuf 70 | > //proguard-rules.pro 71 | > -keep class com.example.module.HookInit 72 | > ``` 73 | 74 | ### XplerState.kt 75 | 76 | 该类汇总了框架状态,如果你想要判断模块是否生效、框架类型,可使用该类。 77 | 78 | ### XplerHelper.kt 79 | 80 | 区别于原 `XposedHelpers` 该类提供了更符合Kotlin的编码风格: 81 | 82 | - `XposedHelpers` 写法 83 | 84 | ```java 85 | XposedHelpers.findAndHookMethod( 86 | Activity.class, 87 | "onCreate", 88 | Bundle.class, 89 | new XC_MethodHook() { 90 | @Override 91 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 92 | XposedBridge.log(param.method.getName() + " Before!"); 93 | } 94 | 95 | @Override 96 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 97 | XposedBridge.log(param.method.getName() + " After!"); 98 | } 99 | } 100 | ); 101 | ``` 102 | 103 | - `XplerHelper` 写法 104 | 105 | ```kotlin 106 | XplerHelper 107 | .hookClass(Activity::class.java) 108 | .method("onCreate", Bundle::class.java) { 109 | onBefore { 110 | XposedBridge.log("${this.method.name} Before!") 111 | } 112 | 113 | onAfter { 114 | XposedBridge.log("${this.method.name} After!") 115 | } 116 | } 117 | ``` 118 | 119 | 并且 `XplerHelper` 在原基础上缓存了目标 `Class` ,使同一个 `Class` 支持链式 `Hook`,如: 120 | 121 | ```kotlin 122 | XplerHelper 123 | .hookClass(Activity::class.java) 124 | .method("onCreate", Bundle::class.java) { 125 | onBefore { 126 | XposedBridge.log("${this.method.name} Before!") 127 | } 128 | 129 | onAfter { 130 | XposedBridge.log("${this.method.name} After!") 131 | } 132 | } 133 | .method("onResume") { 134 | onBefore { 135 | XposedBridge.log("${this.method.name} Before!") 136 | } 137 | 138 | onAfter { 139 | XposedBridge.log("${this.method.name} After!") 140 | } 141 | 142 | // 当 onReplace 出现时, onBefore、onAfter 将失去意义,它们将不会被执行 143 | onReplace { 144 | XposedBridge.log("${this.method.name} Replace!") 145 | } 146 | 147 | // 解除hook逻辑 148 | onUnhook { 149 | unhook() 150 | } 151 | } 152 | ``` 153 | 154 | 而对于 `XC_MethodHook.MethodHookParam` 的使用,相信通过以上例子已经很明显了。 155 | 156 | 得益于Kotlin的扩展特性,在 `onBefoe{..}`、`onAfter{..}` 、`onReplace{..}`作用域内,都属于 157 | `XC_MethodHook.MethodHookParam`;故此,你可以使用 `this` 来表示 `param` 参数。 158 | 159 | 不过,值得注意的是,`onBefore{..}` 和 `onAfter{..}` 属于同类方法,它们允许同时出现,并分别响应其对应执行周期;而 160 | `onReplace{..}` 出现时,则代表了Hook方法会被直接替换,因此 `onBefore{..}` 和 `onAfter{..}` 不会被执行。 161 | 162 | ### XplerModule.kt 163 | 164 | 模块信息汇总:版本号、模块路径、模块资源 165 | 166 | ### XplerLog.kt 167 | 168 | 在模块开发中更具通俗的Log工具类,与Log类的调用基本一致,支持LogCat面板等级输出日志。 169 | 170 | ### HookEntity.kt 171 | 172 | 为了更合适通俗的编码方式,对于需要被Hook的目标类及其方法 `HookEntity` 支持以传统类定义的方式来书写Hook逻辑,下称 173 | `Hook逻辑类`。 174 | 175 | 对于某个Class目标的Hook,Hook逻辑类需要继承 `HookEntity` 并实现抽象方法`setTargetClass` 176 | 主动设置目标Class,然后通过系列注解完成Hook逻辑的编写,最后在 `主逻辑` 中完成实例化,即可注入相关方法的Hook逻辑,以下是简单示例: 177 | 178 | ```kotlin 179 | // 目标类 Activity 180 | class HActivity : HookEntity() { 181 | 182 | override fun setTargetClass(): Class<*> { 183 | return findClass("android.app.Activity") 184 | } 185 | 186 | @OnBefore("onCreate") 187 | fun onCreateBefore(params: MethodParam, savedInstanceState: Bundle?) { 188 | hookBlockRunning(params) { // this: MethodParam 189 | XplerLog.d( 190 | "savedInstanceState: $savedInstanceState", 191 | "method: ${this.method}" 192 | ) 193 | } 194 | } 195 | 196 | @OnAfter("onResume") 197 | fun onCreateBefore(params: MethodParam) { 198 | hookBlockRunning(params) { // this: MethodParam 199 | XplerLog.d( 200 | "thisObject: ${this.thisObject}", 201 | "method: ${this.method}" 202 | ) 203 | } 204 | } 205 | 206 | ... 207 | } 208 | 209 | ////////////////////////// 210 | 211 | // HookInit 212 | override fun onCreateAfter(lparam: LoadParam, hostApp: Application) { 213 | HActivity() 214 | } 215 | 216 | ``` 217 | 218 | 没错,参数 `params: MethodParam` 不能被省略,并且它只能被放在首位。 219 | 220 | 以下是一个稍复杂的写法,自行体会: 221 | 222 | ```kotlin 223 | class HMainActivity : HookEntity() { 224 | 225 | override fun setTargetClass(): Class<*> { 226 | return findClass("android.app.Activity") 227 | } 228 | 229 | @OnConstructorBefore 230 | fun constructorBefore(params: MethodParam) { 231 | hookBlockRunning(params) { 232 | XplerLog.d("thisObject: $thisObject") 233 | }.onFailure { 234 | XplerLog.e(it) 235 | } 236 | } 237 | 238 | @OnAfter("getUser") 239 | @ReturnType(name = "com.example.bean.User") 240 | fun getUserAfter( 241 | params: MethodParam, 242 | name: String, 243 | @Param("com.example.config.UserConfig") config: Any?, 244 | ) { 245 | hookBlockRunning(params) { // this: MethodParam 246 | XplerLog.d( 247 | "name: ${name}", 248 | "config: ${config}", 249 | "result: ${this.result}" 250 | ) 251 | } 252 | } 253 | } 254 | ``` 255 | 256 | 和前文一样 `Xpler` 提供的时机注解 `@..Before`、`@..After`、`@..Replace`,中的 `@..Replace` 257 | 仍然会替换对应目标方法的逻辑,而这时对于 `@..Before`、`@..After` 则不会生效。 258 | 259 | > Xpler 在 [FreedomPlus](https://github.com/GangJust/FreedomPlus) 260 | > 中被很好的实践运用,如果你想要更多示例,请点击 [这里](https://github.com/GangJust/FreedomPlus/tree/master/core/src/main/java/io/github/fplus/core/hook)。 261 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/XplerEntrance.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler 2 | 3 | import de.robv.android.xposed.IXposedHookLoadPackage 4 | import de.robv.android.xposed.IXposedHookZygoteInit 5 | import de.robv.android.xposed.XposedBridge 6 | import de.robv.android.xposed.XposedHelpers 7 | import de.robv.android.xposed.callbacks.XC_LoadPackage 8 | import io.github.xpler.core.XplerHelper 9 | import io.github.xpler.core.XplerLog 10 | import io.github.xpler.core.XplerModule 11 | import io.github.xpler.core.entrance.ApplicationHookStart 12 | import io.github.xpler.core.entrance.DefaultHookStart 13 | import io.github.xpler.core.entrance.HookStart 14 | import io.github.xpler.core.hookClass 15 | import io.github.xpler.core.proxy.LoadParam 16 | import io.github.xpler.core.thisApplication 17 | import io.github.xpler.loader.injectClassLoader 18 | 19 | // Hook init entrance 20 | abstract class XplerEntrance : IXposedHookLoadPackage, IXposedHookZygoteInit { 21 | private val loadParamConstructor = LoadParam::class.java.getDeclaredConstructor(XC_LoadPackage.LoadPackageParam::class.java) 22 | .apply { isAccessible = true } 23 | 24 | override fun initZygote(sparam: IXposedHookZygoteInit.StartupParam) { 25 | XplerModule.initModule(sparam.modulePath) 26 | } 27 | 28 | override fun handleLoadPackage(lp: XC_LoadPackage.LoadPackageParam) { 29 | if (this !is HookStart) { 30 | XposedBridge.log(IllegalArgumentException("You must implement the `HookStart` sub-level interface.")) 31 | return 32 | } 33 | 34 | val xplerParam = loadParamConstructor.newInstance(lp) 35 | 36 | // init module status 37 | if (xplerParam.packageName == this.modulePackage) { 38 | initModule(xplerParam) 39 | return 40 | } 41 | 42 | // hook entrance 43 | when (this) { 44 | is ApplicationHookStart -> applicationHookStart(xplerParam, this) 45 | is DefaultHookStart -> defaultHookStart(xplerParam, this) 46 | } 47 | } 48 | 49 | // module status hook!! 50 | private fun initModule(param: LoadParam) { 51 | param.hookClass(XplerState::class.java.name) 52 | .method("isEnabled") { 53 | onAfter { 54 | setResult(true) 55 | } 56 | } 57 | .method("getVersion") { 58 | onAfter { 59 | setResult(XposedBridge.getXposedVersion()) 60 | } 61 | } 62 | .method("getFramework") { 63 | onAfter { 64 | val bridgeTag = "${XposedHelpers.getStaticObjectField(XposedBridge::class.java, "TAG")}" 65 | setResult( 66 | if (bridgeTag.startsWith("LSPosed")) { 67 | "LSPosed" 68 | } else if (bridgeTag.startsWith("EdXposed")) { 69 | "EdXposed" 70 | } else if (bridgeTag.startsWith("Xposed")) { 71 | "Xposed" 72 | } else { 73 | bridgeTag 74 | } 75 | ) 76 | } 77 | } 78 | } 79 | 80 | // ApplicationHookStart 81 | private fun applicationHookStart( 82 | param: LoadParam, 83 | start: ApplicationHookStart, 84 | ) { 85 | val scopes = start.scopes 86 | val scopePackageNames = scopes.map { it.packageName } 87 | 88 | // compare package name 89 | if (!scopePackageNames.contains(param.packageName)) { 90 | return 91 | } 92 | 93 | // host application 94 | scopes.forEach { scope -> 95 | // filter package name 96 | if (scope.packageName != param.packageName) { 97 | return@forEach 98 | } 99 | 100 | // filter process name 101 | if (!scope.processName.isNullOrEmpty() && scope.processName != param.processName) { 102 | return@forEach 103 | } 104 | 105 | if (scope.applicationClassName.isEmpty()) { 106 | XposedBridge.log(IllegalArgumentException("This scope provided application class name it's empty.")) 107 | return@forEach 108 | } 109 | 110 | param.hookClass(scope.applicationClassName, param.classLoader) 111 | .method("onCreate") { 112 | onBefore { 113 | XplerLog.isXposed(true) // default output of logs to xposed 114 | XplerHelper.initLoadParam(param) 115 | val application = thisApplication 116 | injectClassLoader(param, application.classLoader) 117 | start.onCreateBefore(param, application) 118 | } 119 | onAfter { 120 | val application = thisApplication 121 | start.onCreateAfter(param, application) 122 | } 123 | } 124 | } 125 | } 126 | 127 | // DefaultHookStart 128 | private fun defaultHookStart( 129 | param: LoadParam, 130 | start: DefaultHookStart, 131 | ) { 132 | XplerLog.isXposed(true) // default output of logs to xposed 133 | XplerHelper.initLoadParam(param) 134 | start.loadPackage(param) 135 | } 136 | } 137 | 138 | -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/XplerState.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.util.Base64 9 | import io.github.xpler.utils.XplerUtils 10 | import org.json.JSONObject 11 | 12 | object XplerState { 13 | 14 | /** 15 | * 判断模块是否可用。 16 | * 17 | * see at: [io.github.xpler.XplerEntrance.initModule] 18 | * 19 | */ 20 | val isEnabled: Boolean 21 | get() = false 22 | 23 | /** 24 | * 似乎每个框架都自定义了 logcat tag。 25 | * 26 | * 通过反射 XposedBridge.TAG 来获取框架类型。 27 | * 28 | * see at: [io.github.xpler.XplerEntrance.initModule] 29 | */ 30 | val framework: String 31 | get() = "Unknown" 32 | 33 | /** 34 | * 获取当前框架版本号。 35 | */ 36 | val version: Int 37 | get() = -1 38 | 39 | /** 40 | * 判断模块是否被太极启用。 41 | * 42 | * @param context Context 43 | * @return 是否被启用 44 | */ 45 | fun isExpActive(context: Context): Boolean { 46 | // 是否安装太极 47 | val installed = XplerUtils.isAppInstalled(context, "me.weishu.exp") 48 | if (!installed) return false 49 | 50 | // 模块启用检测 51 | val resolver = context.contentResolver 52 | val uri = Uri.parse("content://me.weishu.exposed.CP/") 53 | var result: Bundle? = null 54 | try { 55 | try { 56 | result = resolver.call(uri, "active", null, null) 57 | } catch (e: Exception) { 58 | e.printStackTrace() 59 | try { 60 | val intent = Intent("me.weishu.exp.ACTION_ACTIVE") 61 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 62 | context.startActivity(intent) 63 | } catch (e: Exception) { 64 | e.printStackTrace() 65 | return false 66 | } 67 | } 68 | if (result == null) { 69 | result = resolver.call(uri, "active", null, null) 70 | } 71 | } catch (e: Exception) { 72 | e.printStackTrace() 73 | } 74 | 75 | result ?: return false 76 | return result.getBoolean("active", false) 77 | } 78 | 79 | /** 80 | * 判断某个已安装的App是否由LSPatch内置模块。 81 | * 82 | * 调用该方法需要Manifest声明权限:android.permission.QUERY_ALL_PACKAGES 83 | * @param context Context 84 | * @param packageName PackageName 85 | * @return 字符串数组, 包含LSPatch基本信息:运行模式、版本名、版本号。 86 | */ 87 | fun isLSPatchActive(context: Context, packageName: String): Array { 88 | // see at: 89 | // https://github.com/LSPosed/LSPatch/blob/master/manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt#L73 90 | // https://github.com/LSPosed/LSPatch/blob/master/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt#L42 91 | try { 92 | val packageInfo = XplerUtils.getPackageInfo( 93 | context = context, 94 | packageName = packageName, 95 | flags = PackageManager.GET_META_DATA, 96 | ) 97 | val appInfo = packageInfo?.applicationInfo 98 | val config = appInfo?.metaData?.getString("lspatch") ?: return emptyArray() 99 | 100 | val json = Base64.decode(config, Base64.DEFAULT).toString(Charsets.UTF_8) 101 | val patchConfig = JSONObject(json) 102 | val useManager = patchConfig.getBoolean("useManager") 103 | val lspConfig = patchConfig.getJSONObject("lspConfig") 104 | val versionName = lspConfig.getString("VERSION_NAME") 105 | val versionCode = lspConfig.getString("VERSION_CODE") 106 | return arrayOf(if (useManager) "本地模式" else "集成模式", versionName, versionCode) 107 | } catch (e: Exception) { 108 | e.printStackTrace() 109 | } 110 | return emptyArray() 111 | } 112 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/XplerHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core 2 | 3 | import de.robv.android.xposed.XposedBridge 4 | import de.robv.android.xposed.XposedHelpers 5 | import io.github.xpler.core.hook.ConstructorHookImpl 6 | import io.github.xpler.core.hook.MethodHookImpl 7 | import io.github.xpler.core.proxy.LoadParam 8 | import io.github.xpler.core.hook.ConstructorHook 9 | import io.github.xpler.core.hook.MethodHook 10 | import io.github.xpler.utils.XplerUtils 11 | import java.lang.reflect.Field 12 | import java.lang.reflect.Member 13 | import java.lang.reflect.Method 14 | import java.lang.reflect.Modifier 15 | 16 | class XplerHelper private constructor() { 17 | private lateinit var targetClazz: Class<*> 18 | private lateinit var loadParam: LoadParam 19 | 20 | /** 21 | * Hook某个方法 22 | * 23 | * @param argsTypes 参数类型列表 24 | * @param block hook代码块, 可在内部书写hook逻辑 25 | */ 26 | fun constructor(vararg argsTypes: Any, block: ConstructorHook.() -> Unit): XplerHelper { 27 | ConstructorHookImpl(targetClazz, *argsTypes).apply(block).startHook() 28 | return this 29 | } 30 | 31 | /** 32 | * Hook某个类中的所有构造方法 33 | * 34 | * @param block hook代码块, 可在内部书写hook逻辑 35 | */ 36 | fun constructorAll(block: MethodHook.() -> Unit): XplerHelper { 37 | val constructors = targetClazz.declaredConstructors 38 | for (c in constructors) { 39 | c.isAccessible = true 40 | MethodHookImpl(c).apply(block).startHook() 41 | } 42 | return this 43 | } 44 | 45 | /** 46 | * Hook某个方法 47 | * 48 | * @param method 方法 49 | * @param block hook代码块, 可在内部书写hook逻辑 50 | */ 51 | fun method(method: Method, block: MethodHook.() -> Unit): XplerHelper { 52 | MethodHookImpl(method).apply(block).startHook() 53 | return this 54 | } 55 | 56 | /** 57 | * Hook某个方法 58 | * 59 | * @param methodName 方法名 60 | * @param argsTypes 参数类型列表 61 | * @param block hook代码块, 可在内部书写hook逻辑 62 | */ 63 | fun method( 64 | methodName: String, 65 | vararg argsTypes: Any, 66 | block: MethodHook.() -> Unit, 67 | ): XplerHelper { 68 | MethodHookImpl(targetClazz, methodName, *argsTypes).apply(block).startHook() 69 | return this 70 | } 71 | 72 | /** 73 | * Hook某个类中的所有方法(构造方法除外) 74 | * 75 | * @param block hook代码块, 可在内部书写hook逻辑 76 | */ 77 | fun methodAll(block: MethodHook.() -> Unit): XplerHelper { 78 | val methods = targetClazz.declaredMethods 79 | for (method in methods) { 80 | if (Modifier.isAbstract(method.modifiers)) 81 | continue 82 | 83 | method.isAccessible = true 84 | MethodHookImpl(method).apply(block).startHook() 85 | } 86 | return this 87 | } 88 | 89 | /** 90 | * Hook某个类中所有[methodName]同名方法, 91 | * 92 | * 不在乎参数类型、数量 93 | * 94 | * @param methodName 方法名 95 | * @param block hook代码块, 可在内部书写hook逻辑 96 | */ 97 | fun methodAllByName( 98 | methodName: String, 99 | block: MethodHook.() -> Unit, 100 | ): XplerHelper { 101 | val methods = targetClazz.declaredMethods 102 | for (method in methods) { 103 | if (Modifier.isAbstract(method.modifiers)) 104 | continue 105 | 106 | if (method.name != methodName) 107 | continue 108 | 109 | method.isAccessible = true 110 | MethodHookImpl(method).apply(block).startHook() 111 | } 112 | return this 113 | } 114 | 115 | /** 116 | * Hook某个类中所有参数类型[argsTypes]相同的方法, 117 | * 118 | * 不在乎方法名、返回类型 119 | * 120 | * @param argsTypes 方法名 121 | * @param block hook代码块, 可在内部书写hook逻辑 122 | */ 123 | fun methodAllByParamTypes( 124 | vararg argsTypes: Class<*>, 125 | block: MethodHook.() -> Unit, 126 | ): XplerHelper { 127 | if (argsTypes.isEmpty()) { 128 | XplerLog.e("argsTypes is empty!") 129 | return this 130 | } 131 | 132 | val methods = targetClazz.declaredMethods 133 | for (method in methods) { 134 | if (Modifier.isAbstract(method.modifiers)) 135 | continue 136 | 137 | if (!XplerUtils.compareParamTypes(method, argsTypes)) 138 | continue 139 | 140 | method.isAccessible = true 141 | MethodHookImpl(method).apply(block).startHook() 142 | } 143 | return this 144 | } 145 | 146 | /** 147 | * Hook某个类中所有方法返回类型为[returnType]的方法, 148 | * 149 | * 不在乎方法名, 参数类型 150 | * 151 | * @param returnType 返回类型 152 | */ 153 | fun methodAllByReturnType( 154 | returnType: Class<*>, 155 | block: MethodHook.() -> Unit, 156 | ): XplerHelper { 157 | val methods = targetClazz.declaredMethods 158 | for (method in methods) { 159 | if (method.returnType != returnType) 160 | continue 161 | 162 | method.isAccessible = true 163 | MethodHookImpl(method).apply(block).startHook() 164 | } 165 | return this 166 | } 167 | 168 | companion object { 169 | private val instance = XplerHelper() 170 | 171 | /** 172 | * 初始化全局param实例 173 | * 174 | * @param param XplerParam 175 | */ 176 | fun initLoadParam(param: LoadParam) { 177 | instance.loadParam = param 178 | } 179 | 180 | /** 181 | * 获取param实例 182 | */ 183 | val lparam: LoadParam 184 | get() = instance.loadParam 185 | 186 | /** 187 | * Hook某个类 188 | * 189 | * @param clazz 类 190 | */ 191 | fun hookClass(clazz: Class<*>): XplerHelper { 192 | instance.targetClazz = clazz 193 | return instance 194 | } 195 | 196 | /** 197 | * Hook某个类 198 | * 199 | * @param className 类名 200 | * @param loader 类加载器 201 | */ 202 | fun hookClass( 203 | className: String, 204 | loader: ClassLoader? = lparam.classLoader, 205 | ): XplerHelper { 206 | instance.targetClazz = XposedHelpers.findClass(XplerUtils.simpleName(className), loader) 207 | return instance 208 | } 209 | 210 | /** 211 | * 查找某个类 212 | * 213 | * @param className 类名 214 | * @param loader 类加载器 215 | */ 216 | fun findClass( 217 | className: String, 218 | loader: ClassLoader? = lparam.classLoader, 219 | ): Class<*>? { 220 | return XposedHelpers.findClass(XplerUtils.simpleName(className), loader) 221 | } 222 | 223 | /** 224 | * 查找某个字段 225 | * 226 | * @param className 类名 227 | * @param fieldName 字段名 228 | */ 229 | fun findField( 230 | className: String, 231 | fieldName: String, 232 | ): Field? { 233 | return XposedHelpers.findField(findClass(className), fieldName) 234 | } 235 | 236 | /** 237 | * 查找某个方法 238 | * 239 | * @param className 类名 240 | * @param methodName 方法名 241 | * @param argsTypes 参数类型列表 242 | */ 243 | fun findMethod( 244 | className: String, 245 | methodName: String, 246 | vararg argsTypes: Class<*>, 247 | ): Method? { 248 | return XposedHelpers.findMethodExact(findClass(className), methodName, *argsTypes) 249 | } 250 | 251 | /** 252 | * 获取字段值 253 | * 254 | * @param any 对象实例 255 | * @param fieldName 字段名 256 | */ 257 | fun getFieldValue( 258 | any: Any, 259 | fieldName: String, 260 | ): Any? { 261 | return XposedHelpers.getObjectField(any, fieldName) 262 | } 263 | 264 | /** 265 | * 设置字段值 266 | * 267 | * @param any 对象实例 268 | * @param fieldName 字段名 269 | * @param value 值 270 | */ 271 | fun setFieldValue( 272 | any: Any, 273 | fieldName: String, 274 | value: Any?, 275 | ) { 276 | XposedHelpers.setObjectField(any, fieldName, value) 277 | } 278 | 279 | /** 280 | * 获取静态字段值 281 | * 282 | * @param clazz 类 283 | * @param fieldName 字段名 284 | */ 285 | fun getStaticFieldValue( 286 | clazz: Class<*>, 287 | fieldName: String, 288 | ): Any? { 289 | return XposedHelpers.getStaticObjectField(clazz, fieldName) 290 | } 291 | 292 | /** 293 | * 设置静态字段值 294 | * 295 | * @param clazz 类 296 | * @param fieldName 字段名 297 | * @param value 值 298 | */ 299 | fun setStaticFieldValue( 300 | clazz: Class<*>, 301 | fieldName: String, 302 | value: Any?, 303 | ) { 304 | XposedHelpers.setStaticObjectField(clazz, fieldName, value) 305 | } 306 | 307 | /** 308 | * 调用方法 309 | * 310 | * @param any 对象实例 311 | * @param methodName 方法名 312 | * @param args 参数列表实例对象 313 | */ 314 | fun invokeMethod( 315 | any: Any, 316 | methodName: String, 317 | vararg args: Any?, 318 | ): Any? { 319 | return XposedHelpers.callMethod(any, methodName, *args) 320 | } 321 | 322 | /** 323 | * 调用方法 324 | * 325 | * @param any 对象实例 326 | * @param methodName 方法名 327 | * @param argTypes 参数类型列表 328 | * @param args 参数实例对象列表 329 | */ 330 | fun invokeMethod( 331 | any: Any, 332 | methodName: String, 333 | argTypes: Array>, 334 | vararg args: Array, 335 | ): Any? { 336 | return XposedHelpers.callMethod(any, methodName, argTypes, *args) 337 | } 338 | 339 | /** 340 | * 调用静态方法 341 | * 342 | * @param clazz 类 343 | * @param methodName 方法名 344 | * @param args 参数实例对象列表 345 | */ 346 | fun invokeStaticMethod( 347 | clazz: Class<*>, 348 | methodName: String, 349 | vararg args: Any?, 350 | ): Any? { 351 | return XposedHelpers.callStaticMethod(clazz, methodName, *args) 352 | } 353 | 354 | /** 355 | * 调用静态方法 356 | * 357 | * @param clazz 类 358 | * @param methodName 方法名 359 | * @param argTypes 参数类型列表 360 | * @param args 参数实例对象列表 361 | */ 362 | fun invokeStaticMethod( 363 | clazz: Class<*>, 364 | methodName: String, 365 | argTypes: Array>, 366 | vararg args: Array, 367 | ): Any? { 368 | return XposedHelpers.callStaticMethod(clazz, methodName, argTypes, *args) 369 | } 370 | 371 | /** 372 | * 调用原始方法 373 | * 374 | * @param member 方法 375 | * @param any 对象实例 376 | * @param args 参数实例对象列表 377 | */ 378 | fun invokeOriginalMethod( 379 | member: Member, 380 | any: Any, 381 | vararg args: Array, 382 | ): Any? { 383 | return XposedBridge.invokeOriginalMethod(member, any, args) 384 | } 385 | } 386 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/XplerHelperExt.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.content.Context 6 | import android.content.pm.PackageInfo 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.annotation.LayoutRes 12 | import io.github.xpler.core.hook.ConstructorHook 13 | import io.github.xpler.core.hook.ConstructorHookImpl 14 | import io.github.xpler.core.hook.MethodHook 15 | import io.github.xpler.core.hook.MethodHookImpl 16 | import io.github.xpler.core.proxy.LoadParam 17 | import io.github.xpler.core.proxy.MethodParam 18 | import java.lang.reflect.Constructor 19 | import java.lang.reflect.Field 20 | import java.lang.reflect.Method 21 | 22 | /// LoadParam 23 | fun LoadParam.hookClass( 24 | className: String, 25 | loader: ClassLoader? = null, 26 | ): XplerHelper { 27 | return XplerHelper.hookClass(className, loader ?: classLoader) 28 | } 29 | 30 | fun LoadParam.hookClass( 31 | clazz: Class<*>, 32 | ): XplerHelper { 33 | return XplerHelper.hookClass(clazz) 34 | } 35 | 36 | fun LoadParam.findClass( 37 | className: String, 38 | loader: ClassLoader? = null, 39 | ): Class<*>? { 40 | return XplerHelper.findClass(className, loader ?: classLoader) 41 | } 42 | 43 | fun LoadParam.findField( 44 | className: String, 45 | fieldName: String, 46 | ): Field? { 47 | return XplerHelper.findField(className, fieldName) 48 | } 49 | 50 | fun LoadParam.findMethod( 51 | className: String, 52 | methodName: String, 53 | vararg argsTypes: Class<*>, 54 | ): Method? { 55 | return XplerHelper.findMethod(className, methodName, *argsTypes) 56 | } 57 | 58 | 59 | /// Context 60 | val Context.modulePackageInfo: PackageInfo? 61 | get() = XplerModule.modulePackageInfo(this) 62 | 63 | val Context.moduleVersionName: String? 64 | get() = XplerModule.moduleVersionName(this) 65 | 66 | val Context.moduleVersionCode: Long? 67 | get() = XplerModule.moduleVersionCode(this) 68 | 69 | fun Context.inflate( 70 | @LayoutRes res: Int, 71 | root: ViewGroup?, 72 | attachToRoot: Boolean = root != null, 73 | ): View { 74 | return LayoutInflater.from(this).inflate(res, root, attachToRoot) 75 | } 76 | 77 | 78 | /// Any 79 | val Any.stackTraceString: String 80 | get() = Log.getStackTraceString(Throwable("Stack trace")) 81 | 82 | val Any.lparam: LoadParam 83 | get() = XplerHelper.lparam 84 | 85 | 86 | /// MethodParam 87 | @get:Throws(TypeCastException::class) 88 | val MethodParam.thisApplication: Application 89 | get() = thisObject as Application 90 | 91 | val MethodParam.thisApplicationOrNull: Application? 92 | get() { 93 | if (thisObject == null || thisObject !is Application) 94 | return null 95 | 96 | return thisObject as Application 97 | } 98 | 99 | @get:Throws(TypeCastException::class) 100 | val MethodParam.thisActivity: Activity 101 | get() = thisObject as Activity 102 | 103 | val MethodParam.thisActivityOrNull: Activity? 104 | get() { 105 | if (thisObject == null || thisObject !is Activity) 106 | return null 107 | 108 | return thisObject as Activity 109 | } 110 | 111 | @get:Throws(TypeCastException::class) 112 | val MethodParam.thisContext: Context 113 | get() = thisObject as Context 114 | 115 | val MethodParam.thisContextOrNull: Context? 116 | get() { 117 | if (thisObject == null || thisObject !is Context) 118 | return null 119 | 120 | return thisObject as Context 121 | } 122 | 123 | @get:Throws(TypeCastException::class) 124 | val MethodParam.thisView: View 125 | get() = thisObject as View 126 | 127 | val MethodParam.thisViewOrNull: View? 128 | get() { 129 | if (thisObject == null || thisObject !is View) 130 | return null 131 | 132 | return thisObject as View 133 | } 134 | 135 | @get:Throws(TypeCastException::class) 136 | val MethodParam.thisViewGroup: ViewGroup 137 | get() = thisObject as ViewGroup 138 | 139 | val MethodParam.thisViewGroupOrNull: ViewGroup? 140 | get() { 141 | if (thisObject == null || thisObject !is ViewGroup) 142 | return null 143 | 144 | return thisObject as ViewGroup 145 | } 146 | 147 | inline fun hookBlockRunning( 148 | params: MethodParam, 149 | block: MethodParam.() -> R, 150 | ): Result { 151 | return runCatching { 152 | block.invoke(params) 153 | } 154 | } 155 | 156 | 157 | /// more 158 | inline fun constructorHook(constructor: Constructor<*>, block: ConstructorHook.() -> Unit) { 159 | ConstructorHookImpl(constructor).apply(block).startHook() 160 | } 161 | 162 | inline fun methodHook(method: Method, block: MethodHook.() -> Unit) { 163 | MethodHookImpl(method).apply(block).startHook() 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/XplerLog.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core 2 | 3 | import android.util.Log 4 | import de.robv.android.xposed.XposedBridge 5 | 6 | object XplerLog { 7 | private var mTag = "XplerLog" 8 | private var mIsXposed: Boolean = true 9 | 10 | private fun println( 11 | priority: Int, 12 | msg: String, 13 | ) { 14 | Log.println(priority, mTag, msg) 15 | 16 | if (!mIsXposed) 17 | return 18 | XposedBridge.log(msg) 19 | } 20 | 21 | fun xposedLog(msg: String) { 22 | XposedBridge.log(msg) 23 | } 24 | 25 | fun xposedLog(th: Throwable) { 26 | XposedBridge.log(th) 27 | } 28 | 29 | fun setTag(tag: String) { 30 | mTag = tag 31 | } 32 | 33 | fun isXposed(b: Boolean) { 34 | mIsXposed = b 35 | } 36 | 37 | fun d(vararg msg: String) { 38 | msg.forEach { println(Log.DEBUG, it) } 39 | } 40 | 41 | fun i(vararg msg: String) { 42 | msg.forEach { println(Log.INFO, it) } 43 | } 44 | 45 | fun e(vararg err: String) { 46 | err.forEach { println(Log.ERROR, it) } 47 | } 48 | 49 | fun e(th: Throwable) { 50 | println(Log.ERROR, th.stackTraceToString()) 51 | } 52 | 53 | fun stackLog() { 54 | d(Log.getStackTraceString(Throwable("Stack trace"))) 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/XplerModule.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageInfo 5 | import android.content.res.Resources 6 | import android.content.res.XModuleResources 7 | import android.os.Build 8 | import de.robv.android.xposed.XposedHelpers 9 | import io.github.xpler.utils.XplerUtils 10 | import java.io.File 11 | 12 | class XplerModule private constructor() { 13 | private lateinit var mModulePath: String 14 | private var mModuleRes: XModuleResources? = null 15 | private var mPackageInfo: PackageInfo? = null 16 | 17 | companion object { 18 | private val instance = XplerModule() 19 | 20 | /** 21 | * 初始化Xpler 22 | * 23 | * @param modulePath 模块路径 24 | */ 25 | fun initModule(modulePath: String) { 26 | instance.mModulePath = modulePath 27 | } 28 | 29 | /** 30 | * 模块路径 31 | */ 32 | val modulePath: String 33 | get() = instance.mModulePath 34 | 35 | /** 36 | * 模块资源 37 | */ 38 | fun moduleResources(): XModuleResources { 39 | return instance.mModuleRes 40 | ?: XModuleResources.createInstance(modulePath, null) 41 | .also { instance.mModuleRes = it } 42 | } 43 | 44 | /** 45 | * 模块PackageInfo 46 | * 47 | * @param context Context 48 | */ 49 | fun modulePackageInfo(context: Context): PackageInfo? { 50 | return instance.mPackageInfo 51 | ?: XplerUtils.getApkPackageInfo(context, File(instance.mModulePath)) 52 | .also { instance.mPackageInfo = it } 53 | } 54 | 55 | /** 56 | * 模块版本名 57 | * 58 | * @param context Context 59 | */ 60 | fun moduleVersionName(context: Context): String? { 61 | return modulePackageInfo(context)?.versionName 62 | } 63 | 64 | /** 65 | * 模块版本号 66 | * 67 | * @param context Context 68 | */ 69 | fun moduleVersionCode(context: Context): Long? { 70 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 71 | modulePackageInfo(context)?.longVersionCode 72 | } else { 73 | modulePackageInfo(context)?.versionCode?.toLong() 74 | } 75 | } 76 | 77 | /** 78 | * 注入资源, 将模块资源合并到宿主以应对资源找不到的情况 79 | * 避免资源id冲突建议, 重新指定资源id的前缀 80 | * ```kt 81 | * // app->build.gradle.kts 82 | * android { 83 | * ... 84 | * androidResources { 85 | * additionalParameters += arrayOf("--allow-reserved-package-id", "--package-id", "0x60") 86 | * } 87 | * ... 88 | * } 89 | * ``` 90 | * 91 | * @param context Context 宿主Context 92 | */ 93 | fun injectResources(context: Context) { 94 | injectResources(context.resources) 95 | } 96 | 97 | /** 98 | * 注入资源, 将模块资源合并到宿主以应对资源找不到的情况 99 | * 避免资源id冲突, 重新指定修改资源id的前缀 100 | * ```kt 101 | * // app->build.gradle.kts 102 | * android { 103 | * ... 104 | * androidResources { 105 | * additionalParameters += arrayOf("--allow-reserved-package-id", "--package-id", "0x60") 106 | * } 107 | * ... 108 | * } 109 | * ``` 110 | * 111 | * @param resources Resources 宿主Resources 112 | */ 113 | fun injectResources(resources: Resources?) { 114 | resources?.runCatching { 115 | XposedHelpers.callMethod(this, "addAssetPath", modulePath) 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/callback/MethodHookCallbackImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.callback 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import io.github.xpler.core.XplerLog 5 | import io.github.xpler.core.proxy.MethodParam 6 | import io.github.xpler.core.proxy.MethodUnhook 7 | import io.github.xpler.core.hook.OnAfterBlock 8 | import io.github.xpler.core.hook.OnBeforeBlock 9 | import io.github.xpler.core.hook.OnUnhookBlock 10 | 11 | class MethodHookCallbackImpl( 12 | private val before: OnBeforeBlock?, 13 | private val after: OnAfterBlock?, 14 | private val onUnhook: OnUnhookBlock?, 15 | ) : XC_MethodHook() { 16 | var unhook: XC_MethodHook.Unhook? = null 17 | 18 | private val unhookConstruct = MethodUnhook::class.java.getDeclaredConstructor(XC_MethodHook.Unhook::class.java) 19 | .apply { 20 | isAccessible = true 21 | } 22 | 23 | private val paramConstructor = MethodParam::class.java.getDeclaredConstructor(MethodHookParam::class.java) 24 | .apply { 25 | isAccessible = true 26 | } 27 | 28 | override fun beforeHookedMethod(param: MethodHookParam) { 29 | runCatching { 30 | before?.invoke(paramConstructor.newInstance(param)) 31 | if (after != null) return 32 | onUnhook?.invoke(unhookConstruct.newInstance(unhook)) 33 | }.onFailure { 34 | XplerLog.e( 35 | "host app method: ${param.method}\n" + 36 | "message: ${it.message}\n" + 37 | "stack trace: ${it.stackTraceToString()}" 38 | ) 39 | } 40 | } 41 | 42 | override fun afterHookedMethod(param: MethodHookParam) { 43 | runCatching { 44 | after?.invoke(paramConstructor.newInstance(param)) 45 | onUnhook?.invoke(unhookConstruct.newInstance(unhook)) 46 | }.onFailure { 47 | XplerLog.e( 48 | "host app method: ${param.method}\n" + 49 | "message: ${it.message}\n" + 50 | "stack trace: ${it.stackTraceToString()}" 51 | ) 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/callback/MethodReplacementCallbackImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.callback 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import de.robv.android.xposed.XC_MethodReplacement 5 | import io.github.xpler.core.XplerLog 6 | import io.github.xpler.core.proxy.MethodParam 7 | import io.github.xpler.core.proxy.MethodUnhook 8 | import io.github.xpler.core.hook.OnReplaceBlock 9 | import io.github.xpler.core.hook.OnUnhookBlock 10 | 11 | class MethodReplacementCallbackImpl( 12 | private val onReplace: OnReplaceBlock?, 13 | private val onUnhook: OnUnhookBlock?, 14 | ) : XC_MethodReplacement() { 15 | var unhook: XC_MethodHook.Unhook? = null 16 | 17 | private val unhookConstruct = MethodUnhook::class.java.getDeclaredConstructor(XC_MethodHook.Unhook::class.java) 18 | .apply { 19 | isAccessible = true 20 | } 21 | 22 | private val constructor = MethodParam::class.java.getDeclaredConstructor(MethodHookParam::class.java) 23 | .apply { 24 | isAccessible = true 25 | } 26 | 27 | override fun replaceHookedMethod(param: MethodHookParam): Any? { 28 | runCatching { 29 | val invoke = onReplace!!.invoke(constructor.newInstance(param)) 30 | onUnhook?.invoke(unhookConstruct.newInstance(unhook)) 31 | return invoke 32 | }.onFailure { 33 | XplerLog.e( 34 | "host app method: ${param.method}\n" + 35 | "message: ${it.message}\n" + 36 | "stack trace: ${it.stackTraceToString()}" 37 | ) 38 | } 39 | 40 | return param.resultOrThrowable 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/entity/CallConstructors.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.entity 2 | 3 | import io.github.xpler.core.proxy.MethodParam 4 | 5 | interface CallConstructors { 6 | /** 7 | * 该方法会在Hook目标类所有构造方法调用之前,都被执行 8 | * 9 | * @param params MethodParam 10 | */ 11 | fun callOnBeforeConstructors(params: MethodParam) 12 | 13 | /** 14 | * 该方法会在Hook目标类所有构造方法调用之后,都被执行 15 | * 16 | * @param params MethodParam 17 | */ 18 | fun callOnAfterConstructors(params: MethodParam) 19 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/entity/CallMethods.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.entity 2 | 3 | import io.github.xpler.core.proxy.MethodParam 4 | 5 | interface CallMethods { 6 | /** 7 | * 该方法会在Hook目标类所有成员方法调用之前,都被执行 8 | * 9 | * @param params MethodParam 10 | */ 11 | fun callOnBeforeMethods(params: MethodParam) 12 | 13 | /** 14 | * 该方法会在Hook目标类所有成员方法调用之后,都被执行 15 | * 16 | * @param params MethodParam 17 | */ 18 | fun callOnAfterMethods(params: MethodParam) 19 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/entity/EmptyHook.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.entity 2 | 3 | /** 4 | * 当某个 HookEntity 未知时, 可以通过泛型该类进行占位。 5 | * 6 | * ``` 7 | * class HMainActivity : HookEntity{ 8 | * 9 | * fun setTargetClass():Class<*>{ 10 | * return EmptyHook::class.java 11 | * } 12 | * 13 | * fun onInit(){ 14 | * // some hook logic.. 15 | * } 16 | * } 17 | * ``` 18 | */ 19 | class EmptyHook private constructor() -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/entity/HookEntity.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.entity 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import de.robv.android.xposed.XC_MethodReplacement 5 | import de.robv.android.xposed.XposedHelpers 6 | import io.github.xpler.core.XplerHelper 7 | import io.github.xpler.core.XplerLog 8 | import io.github.xpler.core.proxy.LoadParam 9 | import io.github.xpler.core.hook.ConstructorHookImpl 10 | import io.github.xpler.core.hook.MethodHookImpl 11 | import io.github.xpler.core.proxy.MethodParam 12 | import io.github.xpler.utils.XplerUtils 13 | import java.lang.reflect.Method 14 | 15 | /// 使用案例, 详见: https://github.com/GangJust/xpler/blob/master/readme.md 16 | 17 | /** 18 | * HookEntity 实体类,提供对某个Hook目标类的便捷操作 19 | */ 20 | abstract class HookEntity( 21 | protected val param: LoadParam = XplerHelper.lparam, 22 | ) { 23 | private lateinit var mTargetClass: Class<*> 24 | private val targetMethods = mutableSetOf() 25 | 26 | private var helper: XplerHelper? = null 27 | private val mineMethods = mutableSetOf() 28 | 29 | init { 30 | runCatching { 31 | mTargetClass = this.setTargetClass() 32 | if (targetClass == NoneHook::class.java) 33 | return@runCatching 34 | 35 | if (targetClass == Nothing::class.java) 36 | return@runCatching 37 | 38 | if (targetClass != EmptyHook::class.java) { 39 | if (targetClass == Any::class.java) 40 | throw ClassFormatError("Please override the `setTargetClass()` to specify the hook target class!") 41 | 42 | helper = XplerHelper.hookClass(targetClass) 43 | 44 | getMineAllMethods() 45 | getHookTargetAllMethods() 46 | 47 | invOnBefore() 48 | invOnAfter() 49 | invOnReplace() 50 | 51 | invOnConstructorBefore() 52 | invOnConstructorAfter() 53 | invOnConstructorReplace() 54 | 55 | hookTargetAllMethod() 56 | hookTargetAllConstructor() 57 | } 58 | 59 | this.onInit() 60 | }.onFailure { 61 | XplerLog.e(it) 62 | } 63 | } 64 | 65 | /** 66 | * 子类手动设置目标类 67 | */ 68 | abstract fun setTargetClass(): Class<*> 69 | 70 | /** 71 | * 获取当前目标类 72 | */ 73 | protected val targetClass get() = mTargetClass 74 | 75 | /** 76 | * 相当于每个Hook逻辑类的入口方法 77 | */ 78 | open fun onInit() {} 79 | 80 | /** 81 | * 查找某类 82 | */ 83 | open fun findClass(className: String, classLoader: ClassLoader? = null): Class<*> { 84 | return try { 85 | XposedHelpers.findClass(simpleName(className), classLoader ?: param.classLoader) 86 | } catch (e: Exception) { 87 | XplerLog.e(e) 88 | NoneHook::class.java 89 | } 90 | } 91 | 92 | /** 93 | * 字节码签名转换:Landroid/view/View; -> android.view.View 94 | */ 95 | protected fun simpleName(name: String): String { 96 | return XplerUtils.simpleName(name) 97 | } 98 | 99 | /** 100 | * 获取子类的所有方法 101 | */ 102 | private fun getMineAllMethods() { 103 | mineMethods.addAll(this::class.java.declaredMethods) 104 | } 105 | 106 | /** 107 | * 获取Hook目标类中的所有方法 108 | */ 109 | private fun getHookTargetAllMethods() { 110 | targetMethods.addAll(targetClass.declaredMethods) 111 | } 112 | 113 | /** 114 | * 过滤目标方法。 115 | * 116 | * @param names 117 | * @param paramTypes 118 | * @param returnType 119 | * @return List 120 | */ 121 | private fun filterHookMethods( 122 | value: Method, 123 | names: Array, 124 | paramTypes: Array?>, 125 | returnType: String, 126 | ): List { 127 | // 目标方法名、参数列表、返回类型同时为空 128 | if (names.isEmpty() && paramTypes.isEmpty() && returnType.isEmpty()) { 129 | return emptyList() 130 | } 131 | 132 | var sequence = targetMethods.asSequence() 133 | 134 | // 具有方法名 135 | if (names.isNotEmpty()) { 136 | sequence = sequence.filter { names.contains(it.name) } 137 | } 138 | 139 | // 具有参数列表 140 | if (paramTypes.isNotEmpty()) { 141 | sequence = sequence.filter { XplerUtils.compareParamTypes(it, paramTypes) } 142 | } 143 | 144 | // 具有返回值 145 | if (returnType.isNotEmpty()) { 146 | sequence = sequence.filter { simpleName(returnType) == it.returnType.name } 147 | } 148 | 149 | return sequence.toList().also { item -> 150 | if (item.isEmpty()) { 151 | val errMsg = StringBuilder() 152 | errMsg.append("\n╭───────────────────────────────\n") 153 | errMsg.append("├─${NoSuchMethodException::class.java.name}: No method in the target class meets the following conditions!\n") 154 | errMsg.append("├─entity: ${this.javaClass.name}#${value.name}\n") 155 | errMsg.append("├─target: ${targetClass.name}\n") 156 | errMsg.append("├─names: ${names.joinToString(", ")}\n") 157 | errMsg.append("├─params: ${paramTypes.joinToString(", ") { it?.name ?: "null" }}\n") 158 | errMsg.append("├─return: $returnType") 159 | errMsg.append("\n╰───────────────────────────────\n") 160 | XplerLog.e(errMsg.toString()) 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * 查找子类方法[mineMethods]中被 [OnBefore] 标注的所有方法, 并将其Hook 167 | */ 168 | private fun invOnBefore() { 169 | val methodMap = getAnnotationMethod(OnBefore::class.java) 170 | for ((key, value) in methodMap) { 171 | if (value.getAnnotation(OnReplace::class.java) != null) continue 172 | value.isAccessible = true 173 | 174 | val names = key.key.name 175 | val paramTypes = getTargetMethodParamTypes(value) 176 | val returnType = value.getAnnotation(ReturnType::class.java)?.name?.trim() ?: "" 177 | 178 | val methods = filterHookMethods(value, names, paramTypes, returnType) 179 | 180 | methods.forEach { 181 | MethodHookImpl(it).apply { 182 | onBefore { 183 | val invArgs = if (paramTypes.isEmpty()) { 184 | arrayOf(this) // 如果只提供了方法名、返回值类型 185 | } else { 186 | arrayOf(this, *args) 187 | } 188 | value.invoke(this@HookEntity, *invArgs) 189 | } 190 | if (value.getAnnotation(HookOnce::class.java) != null) { 191 | onUnhook { unhook() } 192 | } 193 | }.startHook() 194 | } 195 | } 196 | } 197 | 198 | /** 199 | * 查找子类方法[mineMethods]中被 [OnAfter] 标注的所有方法, 并将其Hook 200 | */ 201 | private fun invOnAfter() { 202 | val methodMap = getAnnotationMethod(OnAfter::class.java) 203 | for ((key, value) in methodMap) { 204 | if (value.getAnnotation(OnReplace::class.java) != null) continue 205 | value.isAccessible = true 206 | 207 | val names = key.key.name 208 | val paramTypes = getTargetMethodParamTypes(value) 209 | val returnType = value.getAnnotation(ReturnType::class.java)?.name?.trim() ?: "" 210 | 211 | val methods = filterHookMethods(value, names, paramTypes, returnType) 212 | 213 | methods.forEach { 214 | MethodHookImpl(it).apply { 215 | onAfter { 216 | val invArgs = if (paramTypes.isEmpty()) { 217 | arrayOf(this) // 如果只提供了方法名、返回值类型 218 | } else { 219 | arrayOf(this, *args) 220 | } 221 | value.invoke(this@HookEntity, *invArgs) 222 | } 223 | if (value.getAnnotation(HookOnce::class.java) != null) { 224 | onUnhook { unhook() } 225 | } 226 | }.startHook() 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * 查找子类方法[mineMethods]中被 [OnReplace] 标注的所有方法,并将其Hook 233 | * 值得注意的是, 某个方法一旦标注了 [OnReplace] 如果该方法又同时 234 | * 被[@OnBefore]或[@OnAfter]标注, 该方法跳过它们, 只对 [@OnReplace] 生效 235 | */ 236 | private fun invOnReplace() { 237 | val methodMap = getAnnotationMethod(OnReplace::class.java) 238 | for ((key, value) in methodMap) { 239 | value.isAccessible = true 240 | 241 | val names = key.key.name 242 | val paramTypes = getTargetMethodParamTypes(value) 243 | val returnType = value.getAnnotation(ReturnType::class.java)?.name?.trim() ?: "" 244 | 245 | val methods = filterHookMethods(value, names, paramTypes, returnType) 246 | 247 | methods.forEach { 248 | MethodHookImpl(it).apply { 249 | onReplace { 250 | val invArgs = if (paramTypes.isEmpty()) { 251 | arrayOf(this) // 如果只提供了方法名、返回值类型 252 | } else { 253 | arrayOf(this, *args) 254 | } 255 | value.invoke(this@HookEntity, *invArgs) ?: Unit 256 | } 257 | if (value.getAnnotation(HookOnce::class.java) != null) { 258 | onUnhook { unhook() } 259 | } 260 | }.startHook() 261 | } 262 | } 263 | } 264 | 265 | /** 266 | * 查找子类方法[mineMethods]中被 [OnConstructorBefore] 标注的所有方法, 并将其Hook 267 | */ 268 | private fun invOnConstructorBefore() { 269 | val methodMap = getAnnotationMethod(OnConstructorBefore::class.java) 270 | for ((_, value) in methodMap) { 271 | if (value.getAnnotation(OnConstructorReplace::class.java) != null) continue 272 | value.isAccessible = true 273 | 274 | val paramTypes = getTargetMethodParamTypes(value) 275 | val normalParamTypes = paramTypes.map { it ?: Any::class.java }.toTypedArray() 276 | ConstructorHookImpl(targetClass, *normalParamTypes) 277 | .apply { 278 | onBefore { 279 | val invArgs = arrayOf(this, *args) 280 | value.invoke(this@HookEntity, *invArgs) 281 | } 282 | if (value.getAnnotation(HookOnce::class.java) != null) { 283 | onUnhook { unhook() } 284 | } 285 | }.startHook() 286 | } 287 | } 288 | 289 | /** 290 | * 查找子类方法[mineMethods]中被 [OnConstructorAfter] 标注的所有方法, 并将其Hook 291 | */ 292 | private fun invOnConstructorAfter() { 293 | val methodMap = getAnnotationMethod(OnConstructorAfter::class.java) 294 | for ((_, value) in methodMap) { 295 | if (value.getAnnotation(OnConstructorReplace::class.java) != null) continue 296 | value.isAccessible = true 297 | 298 | val paramTypes = getTargetMethodParamTypes(value) 299 | val normalParamTypes = paramTypes.map { it ?: Any::class.java }.toTypedArray() 300 | ConstructorHookImpl(targetClass, *normalParamTypes) 301 | .apply { 302 | onAfter { 303 | val invArgs = arrayOf(this, *args) 304 | value.invoke(this@HookEntity, *invArgs) 305 | } 306 | if (value.getAnnotation(HookOnce::class.java) != null) { 307 | onUnhook { unhook() } 308 | } 309 | }.startHook() 310 | } 311 | } 312 | 313 | /** 314 | * 查找子类方法[mineMethods]中被 [OnConstructorReplace] 标注的所有方法, 并将其Hook 315 | */ 316 | private fun invOnConstructorReplace() { 317 | val methodMap = getAnnotationMethod(OnConstructorReplace::class.java) 318 | for ((_, value) in methodMap) { 319 | value.isAccessible = true 320 | val paramTypes = getTargetMethodParamTypes(value) 321 | val normalParamTypes = paramTypes.map { it ?: Any::class.java }.toTypedArray() 322 | ConstructorHookImpl(targetClass, *normalParamTypes) 323 | .apply { 324 | onReplace { 325 | val invArgs = arrayOf(this, *args) 326 | value.invoke(this@HookEntity, *invArgs) 327 | thisObject 328 | } 329 | if (value.getAnnotation(HookOnce::class.java) != null) { 330 | onUnhook { unhook() } 331 | } 332 | }.startHook() 333 | } 334 | } 335 | 336 | /** 337 | * 获取被指定注解标注的方法集合 338 | * @param a a extends Annotation 339 | * @return Map 340 | */ 341 | @Throws(IllegalArgumentException::class) 342 | private fun getAnnotationMethod(a: Class): Map, Method> { 343 | val map = mutableMapOf, Method>() 344 | for (method in mineMethods) { 345 | val annotation = method.getAnnotation(a) ?: continue 346 | 347 | val mineParamsTypes = method.parameterTypes 348 | if (mineParamsTypes.isEmpty()) { 349 | throw IllegalArgumentException("parameterTypes empty.") 350 | } 351 | if (mineParamsTypes.first() != MethodParam::class.java) { 352 | throw IllegalArgumentException("parameterTypes[0] must be `MethodParam`.") 353 | } 354 | 355 | map[IdentifyKey("$method", annotation)] = method 356 | } 357 | return map 358 | } 359 | 360 | /** 361 | * 替换 [Param] 注解, 获取目标方法中的的真实参数列表 362 | * @param method 目标方法 363 | * @return Array 364 | */ 365 | private fun getTargetMethodParamTypes(method: Method): Array?> { 366 | val parameterAnnotations = method.parameterAnnotations 367 | val parameterTypes = method.parameterTypes 368 | 369 | // 只含注解参数的情况下 370 | return if (parameterAnnotations.size < parameterTypes.size) { 371 | getTargetMethodParamTypesOnlyAnnotations(parameterAnnotations, parameterTypes) 372 | } else { 373 | getTargetMethodParamTypesNormal(parameterAnnotations, parameterTypes) 374 | } 375 | } 376 | 377 | /** 378 | * 部分情况下 [parameterAnnotations] 只会返回含有注解的参数 379 | * ``` 380 | * val targetMethodParamTypes = method.parameterTypes 381 | * val parameterAnnotations = method.parameterAnnotations 382 | * 383 | * // 通常情况下 targetMethodParamTypes.size == parameterAnnotations.size 的结果应该是 `true` 384 | * // 而某些情况下 targetMethodParamTypes.size > parameterAnnotations.size 的结果才是 `true` 385 | * // 因为 `parameterAnnotations.size == 被注解标注了的参数的数量` 而那些未被注解的参数本来应该是空数组的,但是它却没了。 386 | * ``` 387 | * @param parameterAnnotations 注解数组 388 | * @param parameterTypes 参数数组 389 | */ 390 | private fun getTargetMethodParamTypesOnlyAnnotations( 391 | parameterAnnotations: Array>, 392 | parameterTypes: Array> 393 | ): Array?> { 394 | // 整理参数, 将第一个参数`MethodParam`移除 395 | val paramTypes = parameterTypes.toMutableList().apply { removeAt(0) } 396 | 397 | var index = 0 398 | return paramTypes.map { clazz -> 399 | if (clazz == Any::class.java) { 400 | val param = parameterAnnotations[index++].filterIsInstance() 401 | .ifEmpty { return@map clazz } // 如果某个参数没有@Param注解, 直接return 402 | findTargetParamClass(param[0].name)// 寻找注解类 403 | } else { 404 | clazz 405 | } 406 | }.toTypedArray() 407 | } 408 | 409 | /** 410 | * 正常情况下 [parameterAnnotations] 能获取到全部参数的注解, 未被注解标注的参数是一个空注解数组 411 | * 412 | * @param parameterAnnotations 注解数组 413 | * @param parameterTypes 参数数组 414 | */ 415 | private fun getTargetMethodParamTypesNormal( 416 | parameterAnnotations: Array>, 417 | parameterTypes: Array?> 418 | ): Array?> { 419 | // 整理参数、参数注解列表, 将第一个参数`MethodParam`移除 420 | val paramAnnotations = parameterAnnotations.toMutableList().apply { removeAt(0) } 421 | val paramTypes = parameterTypes.toMutableList().apply { removeAt(0) } 422 | 423 | // 替换 @Param 指定的参数类型 424 | val finalParamTypes = paramTypes.mapIndexed { index, clazz -> 425 | if (paramAnnotations[index].isEmpty()) return@mapIndexed clazz // 如果某个参数没有注解 426 | val param = paramAnnotations[index].filterIsInstance() 427 | .ifEmpty { return@mapIndexed clazz } // 如果某个参数有注解, 但没有@Param注解, 直接return 428 | findTargetParamClass(param[0].name) // 寻找注解类 429 | }.toTypedArray() 430 | 431 | return finalParamTypes 432 | } 433 | 434 | /** 435 | * 查找方法参数注解中的类,未找到则用 null 代替。 436 | */ 437 | private fun findTargetParamClass( 438 | name: String, 439 | classLoader: ClassLoader? = null, 440 | ): Class<*>? { 441 | val optName = simpleName(name) 442 | if (optName.isEmpty() || optName.isBlank() || optName == "null") { 443 | return null 444 | } 445 | 446 | // 447 | return try { 448 | XposedHelpers.findClass(optName, classLoader ?: param.classLoader) 449 | } catch (e: Exception) { 450 | return null 451 | } 452 | } 453 | 454 | /** 455 | * 勾住所有普通方法 456 | */ 457 | private fun hookTargetAllMethod() { 458 | // 所有普通方法 459 | if (this@HookEntity is CallMethods) { 460 | helper?.methodAll { 461 | onBefore { 462 | callOnBeforeMethods(this) 463 | } 464 | onAfter { 465 | thisObject ?: return@onAfter 466 | callOnAfterMethods(this) 467 | } 468 | } 469 | } 470 | } 471 | 472 | /** 473 | * 勾住所有构造方法 474 | */ 475 | private fun hookTargetAllConstructor() { 476 | // 所有构造方法 477 | if (this@HookEntity is CallConstructors) { 478 | helper?.constructorAll { 479 | onBefore { 480 | callOnBeforeConstructors(this) 481 | } 482 | onAfter { 483 | thisObject ?: return@onAfter 484 | callOnAfterConstructors(this) 485 | } 486 | } 487 | } 488 | } 489 | 490 | private data class IdentifyKey( 491 | val identify: String, 492 | val key: T, 493 | ) 494 | 495 | /** 496 | * 对于目标类某个普通方法的挂勾,等价于 [XC_MethodHook.beforeHookedMethod]。 497 | * 498 | * 被标注的方法第一个参数需要是 [MethodParam],其后才是原方法参数列表; 499 | * 若某个方法中的参数类型无法直接被引用,可参考使用 [Param] 注解直接指定。 500 | * 501 | * @param name Hook目标方法名 502 | * 503 | */ 504 | @Target(AnnotationTarget.FUNCTION) 505 | protected annotation class OnBefore(vararg val name: String) 506 | 507 | /** 508 | * 对于目标类某个普通方法的挂勾,等价于 [XC_MethodHook.afterHookedMethod]。 509 | * 510 | * 被标注的方法第一个参数需要是 [MethodParam],其后才是原方法参数列表; 511 | * 若某个方法中的参数类型无法直接被引用,可参考使用 [Param] 注解直接指定。 512 | * 513 | * @param name Hook目标方法名 514 | * 515 | */ 516 | @Target(AnnotationTarget.FUNCTION) 517 | protected annotation class OnAfter(vararg val name: String) 518 | 519 | /** 520 | * 对于目标类某个普通方法的挂勾,等价于 [XC_MethodReplacement.replaceHookedMethod]。 521 | * 522 | * 被标注的方法第一个参数需要是 [MethodParam],其后才是原方法参数列表; 523 | * 若某个方法中的参数类型无法直接被引用,可参考使用 [Param] 注解直接指定。 524 | * 525 | * @param name Hook目标方法名 526 | * 527 | */ 528 | @Target(AnnotationTarget.FUNCTION) 529 | protected annotation class OnReplace(vararg val name: String) 530 | 531 | /** 532 | * 对于目标类某个构造方法的挂勾,等价于 [XC_MethodHook.beforeHookedMethod]。 533 | * 534 | * 被标注的方法第一个参数需要是 [MethodParam],其后才是原方法参数列表; 535 | * 若某个方法中的参数类型无法直接被引用,可参考使用 [Param] 注解直接指定。 536 | */ 537 | @Target(AnnotationTarget.FUNCTION) 538 | protected annotation class OnConstructorBefore 539 | 540 | /** 541 | * 对于目标类某个构造方法的挂勾,等价于 [XC_MethodHook.afterHookedMethod]。 542 | * 543 | * 被标注的方法第一个参数需要是 [MethodParam],其后才是原方法参数列表; 544 | * 若某个方法中的参数类型无法直接被引用,可参考使用 [Param] 注解直接指定。 545 | */ 546 | @Target(AnnotationTarget.FUNCTION) 547 | protected annotation class OnConstructorAfter 548 | 549 | /** 550 | * 对于目标类某个构造方法的挂勾,等价于 [XC_MethodReplacement.replaceHookedMethod]。 551 | * 552 | * 被标注的方法第一个参数需要是 [MethodParam],其后才是原方法参数列表; 553 | * 若某个方法中的参数类型无法直接被引用,可参考使用 [Param] 注解直接指定。 554 | */ 555 | @Target(AnnotationTarget.FUNCTION) 556 | protected annotation class OnConstructorReplace 557 | 558 | /** 559 | * 对于目标方法中出现的不确定参数类型,默认模糊匹配。 560 | * 561 | * 若某个参数属于宿主如:`com.sample.User`,书写时无法直接通过`import`引入,可使用该注解手动指定。 562 | * 而该类型则需要使用[java.lang.Object]/[kotlin.Any]顶层类代替。 563 | * 564 | * ``` 565 | * @OnBefore("exampleMethod") 566 | * fun exampleMethodBefore( 567 | * params: MethodParam, 568 | * @Param("com.sample.User") user:Any? //user!!.javaClass == com.sample.User (Mandatory type) 569 | * @Param arg1:Any? //arg1!!.javaClass == Any (Any type) 570 | * @Param("java.lang.Object") arg2:Any? //arg2!!.javaClass == java.lang.Object/kotlin.Any (Mandatory type) 571 | * ){ 572 | * hookBlockRunning(params){ 573 | * //some logic.. 574 | * }.onFailure { 575 | * XplerLog.e(it) 576 | * } 577 | * } 578 | * ``` 579 | * 580 | * @param name 应该是一个完整的类名, 如: com.sample.User; 581 | * 允许为 `"null"` 或 `""` 字符串,将模糊匹配任意类型。 582 | */ 583 | @Target(AnnotationTarget.VALUE_PARAMETER) 584 | protected annotation class Param(val name: String = "null") 585 | 586 | /** 587 | * [Param]的衍生类,对于处理某些情况下原始类型本身是[java.lang.Object]/[kotlin.Any]的情况。 588 | * 589 | * [KeepParam]出现的原因见 [HookEntity.getTargetMethodParamTypesOnlyAnnotations] 的解释。 590 | * 591 | * 实际上它只是为了给注解二维数组占位,并未有任何实际意义。 592 | * 593 | * 或者只要你愿意,可以为每一个原始类型这样注解: 594 | * ``` 595 | * @OnBefore 596 | * fun test( 597 | * //@KeepParam any: Any?, //与下一行目的相同 598 | * @Param("java.lang.Object") any: Any?, 599 | * @Param("com.test.User") user: Any?, 600 | * ){ 601 | * //hook logic 602 | * } 603 | * ``` 604 | */ 605 | @Target(AnnotationTarget.VALUE_PARAMETER) 606 | protected annotation class KeepParam 607 | 608 | /** 609 | * 目标类的某个方法类型,精确匹配用得上。 610 | * 611 | * 数组类型见: [Class.getName] 612 | * 613 | * @param name 类型字符串,应该是一个全类名 614 | */ 615 | @Target(AnnotationTarget.FUNCTION) 616 | protected annotation class ReturnType(val name: String = "") 617 | 618 | /** 619 | * 一次性的Hook注解。 620 | * 621 | * 需要搭配 [OnBefore]、[OnAfter]、[OnReplace]、[OnConstructorBefore]、[OnConstructorAfter]、[OnConstructorReplace] 使用。 622 | * 623 | * 被该标注的某个逻辑方法会在执行一次Hook后立即解开。 624 | */ 625 | @Target(AnnotationTarget.FUNCTION) 626 | protected annotation class HookOnce 627 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/entity/NoneHook.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.entity 2 | 3 | /** 4 | * 当 [HookEntity.targetClass] 返回该类时, 将不会执行Hook逻辑。 5 | */ 6 | class NoneHook private constructor() -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/entrance/ApplicationHookStart.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.entrance 2 | 3 | import android.app.Application 4 | import io.github.xpler.core.proxy.LoadParam 5 | 6 | /** 7 | * 需要宿主Application时的Hook 8 | * 9 | * see at: [io.github.xpler.XplerEntrance.applicationHookStart] 10 | */ 11 | interface ApplicationHookStart : HookStart { 12 | /** 13 | * 由子类设置宿主的 `package` 和 `application`。 14 | */ 15 | val scopes: Set 16 | 17 | /** 18 | * 在宿主Application onCreate 之前调用 19 | * 20 | * 不要在 [onCreateBefore]、[onCreateAfter] 中书写或调用相同的Hook逻辑, 毫无意义。 21 | */ 22 | fun onCreateBefore( 23 | lparam: LoadParam, 24 | hostApp: Application, 25 | ) 26 | 27 | /** 28 | * 在宿主Application onCreate 之后调用 29 | * 30 | * 不要在 [onCreateBefore]、[onCreateAfter] 中书写或调用相同的Hook逻辑, 毫无意义。 31 | */ 32 | fun onCreateAfter( 33 | lparam: LoadParam, 34 | hostApp: Application, 35 | ) 36 | 37 | /** 38 | * 针对应用的Hook作用域。 39 | * 40 | * @param packageName 宿主包名 41 | * @param applicationClassName 宿主Application 42 | * @param processName 指定某个进程名,为空则对宿主所有进程生效 43 | */ 44 | data class Scope( 45 | val packageName: String, 46 | val applicationClassName: String, 47 | val processName: String? = null, 48 | ) { 49 | override fun equals(other: Any?): Boolean { 50 | if (this === other) return true 51 | if (javaClass != other?.javaClass) return false 52 | 53 | other as Scope 54 | 55 | if (packageName != other.packageName) return false 56 | if (applicationClassName != other.applicationClassName) return false 57 | if (processName != other.processName) return false 58 | 59 | return true 60 | } 61 | 62 | override fun hashCode(): Int { 63 | var result = packageName.hashCode() 64 | result = 31 * result + applicationClassName.hashCode() 65 | result = 31 * result + (processName?.hashCode() ?: 0) 66 | return result 67 | } 68 | } 69 | } 70 | 71 | // 72 | infix fun String.at(applicationClassName: String): ApplicationHookStart.Scope = 73 | ApplicationHookStart.Scope( 74 | packageName = this, 75 | applicationClassName = applicationClassName, 76 | ) 77 | 78 | infix fun String.at(part: Pair): ApplicationHookStart.Scope = 79 | ApplicationHookStart.Scope( 80 | packageName = this, 81 | applicationClassName = part.first, 82 | processName = part.second 83 | ) -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/entrance/DefaultHookStart.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.entrance 2 | 3 | import io.github.xpler.core.proxy.LoadParam 4 | 5 | interface DefaultHookStart : HookStart { 6 | 7 | /** 8 | * 与 [de.robv.android.xposed.IXposedHookLoadPackage.handleLoadPackage] 一致,提供原始的加载方式。 9 | * 10 | * see at: [io.github.xpler.XplerEntrance.defaultHookStart] 11 | */ 12 | fun loadPackage(lparam: LoadParam) 13 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/entrance/HookStart.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.entrance 2 | 3 | interface HookStart { 4 | 5 | /** 6 | * 由子类设置模块的`package`,用作获取模块启用状态[io.github.xpler.XplerState.isEnabled] 7 | */ 8 | val modulePackage: String 9 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/hook/ConstructorHook.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.hook 2 | 3 | /// 对构造方法的Hook 4 | interface ConstructorHook : MethodHook { 5 | 6 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/hook/ConstructorHookImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.hook 2 | 3 | import de.robv.android.xposed.XposedHelpers 4 | import java.lang.reflect.Constructor 5 | 6 | 7 | /// 实现类 8 | class ConstructorHookImpl(constructor: Constructor<*>) : 9 | MethodHookImpl(constructor), ConstructorHook { 10 | 11 | constructor(clazz: Class<*>, vararg argsTypes: Any) 12 | : this(XposedHelpers.findConstructorExact(clazz, *argsTypes)) 13 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/hook/MethodHook.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.hook 2 | 3 | import io.github.xpler.core.proxy.MethodParam 4 | import io.github.xpler.core.proxy.MethodUnhook 5 | 6 | /// 扩展方法 7 | typealias OnBeforeBlock = MethodParam.() -> Unit 8 | 9 | typealias OnAfterBlock = MethodParam.() -> Unit 10 | 11 | typealias OnReplaceBlock = MethodParam.() -> Any? 12 | 13 | typealias OnUnhookBlock = MethodUnhook.() -> Unit 14 | 15 | /// 对普通方法的 Hook 封装 16 | interface MethodHook { 17 | 18 | /** 19 | * [onBefore]会在某个Hook方法执行之前被调用, 20 | * 等价于[de.robv.android.xposed.XC_MethodHook.beforeHookedMethod]方法。 21 | * 22 | * @param block hook代码块, 可在内部书写hook逻辑 23 | */ 24 | fun onBefore(block: OnBeforeBlock) 25 | 26 | /** 27 | * [onAfter]会在某个Hook方法执行之后被调用, 28 | * 等价于[de.robv.android.xposed.XC_MethodHook.afterHookedMethod]方法。 29 | * 30 | * @param block hook代码块, 可在内部书写hook逻辑 31 | */ 32 | fun onAfter(block: OnAfterBlock) 33 | 34 | /** 35 | * 该方法会直接 替换 某个Hook方法, 一旦[onReplace]被显示调用, [onBefore]和[onAfter]将不会被响应, 36 | * 等价于[de.robv.android.xposed.XC_MethodReplacement.replaceHookedMethod]方法。 37 | * 38 | * @param block hook代码块, 可在内部书写hook逻辑 39 | */ 40 | fun onReplace(block: OnReplaceBlock) 41 | 42 | /** 43 | * 该方法会解开某个Hook方法, 被书写后在每次[onReplace]、[onBefore]、[onAfter]执行完毕, 都会被调用。 44 | * 45 | * 如果不需要在某个Hook方法执行之后解Hook (即表示当前进程下, hook逻辑只被执行一次), 请不要书写该方法。 46 | * 47 | * @param block deHook代码块 48 | */ 49 | fun onUnhook(block: OnUnhookBlock) 50 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/hook/MethodHookImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.hook 2 | 3 | import de.robv.android.xposed.XposedBridge 4 | import de.robv.android.xposed.XposedHelpers 5 | import io.github.xpler.core.XplerLog 6 | import io.github.xpler.core.callback.MethodHookCallbackImpl 7 | import io.github.xpler.core.callback.MethodReplacementCallbackImpl 8 | import java.lang.reflect.Member 9 | import java.lang.reflect.Modifier 10 | 11 | /// 实现类 12 | open class MethodHookImpl(private var method: Member) : MethodHook { 13 | private var beforeBlock: OnBeforeBlock? = null 14 | private var afterBlock: OnAfterBlock? = null 15 | private var replaceBlock: OnReplaceBlock? = null 16 | private var unHookBlock: OnUnhookBlock? = null 17 | 18 | constructor(clazz: Class<*>, methodName: String, vararg argsTypes: Any) : 19 | this(XposedHelpers.findMethodExact(clazz, methodName, *argsTypes)) 20 | 21 | override fun onBefore(block: OnBeforeBlock) { 22 | this.beforeBlock = block 23 | } 24 | 25 | override fun onAfter(block: OnAfterBlock) { 26 | this.afterBlock = block 27 | } 28 | 29 | override fun onReplace(block: OnReplaceBlock) { 30 | this.replaceBlock = block 31 | } 32 | 33 | override fun onUnhook(block: OnUnhookBlock) { 34 | this.unHookBlock = block 35 | } 36 | 37 | // 开启hook, 统一执行Hook逻辑 38 | fun startHook() { 39 | // 跳过抽象方法 40 | if (Modifier.isAbstract(method.modifiers)) { 41 | XplerLog.e(IllegalArgumentException("Cannot hook abstract method: $method")) 42 | return 43 | } 44 | 45 | if (replaceBlock != null) { 46 | val impl = MethodReplacementCallbackImpl(replaceBlock, unHookBlock) 47 | impl.unhook = XposedBridge.hookMethod(method, impl) 48 | } else { 49 | val impl = MethodHookCallbackImpl(beforeBlock, afterBlock, unHookBlock) 50 | impl.unhook = XposedBridge.hookMethod(method, impl) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/proxy/LoadParam.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.proxy 2 | 3 | import android.content.pm.ApplicationInfo 4 | import de.robv.android.xposed.callbacks.XC_LoadPackage 5 | import org.json.JSONObject 6 | 7 | class LoadParam private constructor( 8 | private val lpparam: XC_LoadPackage.LoadPackageParam, 9 | ) { 10 | var packageName: String 11 | get() = lpparam.packageName 12 | set(value) { 13 | lpparam.packageName = value 14 | } 15 | 16 | var processName: String 17 | get() = lpparam.processName 18 | set(value) { 19 | lpparam.packageName = value 20 | } 21 | 22 | var classLoader: ClassLoader 23 | get() = lpparam.classLoader 24 | set(value) { 25 | lpparam.classLoader = value 26 | } 27 | 28 | var appInfo: ApplicationInfo 29 | get() = lpparam.appInfo 30 | set(value) { 31 | lpparam.appInfo = value 32 | } 33 | 34 | var isFirstApplication: Boolean 35 | get() = lpparam.isFirstApplication 36 | set(value) { 37 | lpparam.isFirstApplication = value 38 | } 39 | 40 | override fun toString(): String { 41 | return JSONObject().apply { 42 | putOpt("packageName", packageName) 43 | putOpt("processName", processName) 44 | putOpt("classLoader", "$classLoader") 45 | putOpt("appInfo", "$appInfo") 46 | putOpt("isFirstApplication", isFirstApplication) 47 | }.toString(2) 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/proxy/MethodParam.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.proxy 2 | 3 | import android.util.Log 4 | import de.robv.android.xposed.XC_MethodHook 5 | import org.json.JSONArray 6 | import org.json.JSONObject 7 | import java.lang.reflect.Member 8 | 9 | class MethodParam private constructor( 10 | private val param: XC_MethodHook.MethodHookParam, 11 | ) { 12 | val method: Member 13 | get() = param.method 14 | 15 | val thisObject: Any? 16 | get() = param.thisObject 17 | 18 | val args: Array 19 | get() = param.args 20 | 21 | val result: Any? 22 | get() = param.result 23 | 24 | val hasThrowable: Boolean 25 | get() = param.hasThrowable() 26 | 27 | val throwable: Throwable? 28 | get() = param.throwable 29 | 30 | val resultOrThrowable: Any? 31 | @Throws(Throwable::class) 32 | get() = param.resultOrThrowable 33 | 34 | fun setMethod(member: Member) { 35 | param.method = member 36 | } 37 | 38 | fun setThisObject(value: Any?) { 39 | param.thisObject = value 40 | } 41 | 42 | fun setArgs(args: Array) { 43 | param.args = args 44 | } 45 | 46 | fun setResult(result: Any?) { 47 | param.result = result 48 | } 49 | 50 | fun setResultVoid() { 51 | param.result = Void.TYPE 52 | } 53 | 54 | fun setThrowable(th: Throwable?) { 55 | param.throwable = th 56 | } 57 | 58 | val getStackTraceString: String 59 | get() = Log.getStackTraceString(Throwable("getStackTraceString")) 60 | 61 | override fun toString(): String { 62 | return JSONObject().let { json -> 63 | json.putOpt("method", "$method") 64 | json.putOpt( 65 | "thisObj", JSONObject() 66 | .putOpt("value", "$thisObject") 67 | .putOpt("type", "${thisObject?.javaClass?.name}") 68 | ) 69 | json.putOpt("args", JSONArray().apply { 70 | args.forEachIndexed { index, any -> 71 | put( 72 | JSONObject() 73 | .putOpt("index", index) 74 | .putOpt("value", "$any") 75 | .putOpt("type", "${any?.javaClass?.name}") 76 | ) 77 | } 78 | }) 79 | json.putOpt( 80 | "throwable", JSONObject() 81 | .putOpt("value", "$throwable") 82 | .putOpt("type", "${throwable?.javaClass?.name}") 83 | ) 84 | json.putOpt( 85 | "result", JSONObject() 86 | .putOpt("value", "$result") 87 | .putOpt("type", "${result?.javaClass?.name}") 88 | ) 89 | }.toString() 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/core/proxy/MethodUnhook.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.core.proxy 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import java.lang.reflect.Member 5 | 6 | class MethodUnhook private constructor( 7 | private val unhook: XC_MethodHook.Unhook, 8 | ) { 9 | val hookedMethod:Member 10 | get() = unhook.hookedMethod 11 | 12 | fun unhook() { 13 | unhook.unhook() 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/loader/XplerClassloader.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.loader 2 | 3 | import android.content.Context 4 | import io.github.xpler.XplerEntrance 5 | import io.github.xpler.core.proxy.LoadParam 6 | import java.net.URL 7 | 8 | class XplerClassloader( 9 | private val host: ClassLoader, 10 | private val parent: ClassLoader, 11 | ) : ClassLoader() { 12 | private val bootClassloader = Context::class.java.classLoader 13 | 14 | private fun skipLoad(name: String): Boolean { 15 | return name.startsWith("android.") || name.startsWith("androidx.") 16 | || name.startsWith("kotlin.") || name.startsWith("kotlinx.") 17 | } 18 | 19 | @Throws(ClassNotFoundException::class) 20 | override fun loadClass(name: String, resolve: Boolean): Class<*> { 21 | try { 22 | return bootClassloader?.loadClass(name) ?: throw ClassNotFoundException(name) 23 | } catch (e: ClassNotFoundException) { 24 | // e.printStackTrace() 25 | } 26 | 27 | /// bootClassloader优先加载, 然后跳过冲突类。fix: 太极框架崩溃 28 | if (skipLoad(name)) 29 | throw ClassNotFoundException(name) 30 | 31 | /// 32 | try { 33 | return host.loadClass(name) 34 | } catch (e: ClassNotFoundException) { 35 | // e.printStackTrace() 36 | } 37 | 38 | try { 39 | return parent.loadClass(name) 40 | } catch (e: ClassNotFoundException) { 41 | // e.printStackTrace() 42 | } 43 | 44 | throw ClassNotFoundException(name) 45 | } 46 | 47 | override fun getResource(name: String?): URL { 48 | return parent.getResource(name) ?: host.getResource(name) 49 | } 50 | } 51 | 52 | fun injectClassLoader( 53 | param: LoadParam, 54 | loader: ClassLoader, 55 | ) { 56 | val fParent = ClassLoader::class.java.declaredFields.first { it.name == "parent" }.apply { isAccessible = true } 57 | val mine = XplerEntrance::class.java.classLoader 58 | val parent = fParent.get(mine) as ClassLoader 59 | if (parent::class.java != XplerClassloader::class.java) { 60 | hostClassloader = loader 61 | moduleClassloader = mine 62 | fParent.set(mine, XplerClassloader(loader, parent).also { param.classLoader = it }) 63 | } else { 64 | param.classLoader = parent 65 | } 66 | } 67 | 68 | var hostClassloader: ClassLoader? = null 69 | var moduleClassloader: ClassLoader? = null -------------------------------------------------------------------------------- /src/main/java/io/github/xpler/utils/XplerUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.xpler.utils 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageInfo 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import java.io.File 8 | import java.lang.reflect.Method 9 | 10 | object XplerUtils { 11 | 12 | /** 13 | * 获取 APK 包信息. 14 | * 15 | * @param context 上下文 16 | * @param apkFile APK 文件 17 | */ 18 | fun getApkPackageInfo( 19 | context: Context, 20 | apkFile: File, 21 | ): PackageInfo? { 22 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 23 | val flags = PackageManager.PackageInfoFlags.of(PackageManager.GET_ACTIVITIES.toLong()) 24 | context.packageManager.getPackageArchiveInfo(apkFile.absolutePath, flags) 25 | } else { 26 | context.packageManager.getPackageArchiveInfo( 27 | apkFile.absolutePath, 28 | PackageManager.GET_ACTIVITIES 29 | ) 30 | } 31 | } 32 | 33 | /** 34 | * 获取应用信息. 35 | * 36 | * @param context 上下文 37 | * @param packageName 包名 38 | * @param flags 标志 39 | */ 40 | fun getPackageInfo( 41 | context: Context, 42 | packageName: String = context.packageName, 43 | flags: Int = PackageManager.GET_ACTIVITIES, 44 | ): PackageInfo? { 45 | return try { 46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 47 | context.packageManager.getPackageInfo( 48 | packageName, 49 | PackageManager.PackageInfoFlags.of(flags.toLong()) 50 | ) 51 | } else { 52 | context.packageManager.getPackageInfo(packageName, flags) 53 | } 54 | } catch (e: Exception) { 55 | e.printStackTrace() 56 | null 57 | } 58 | } 59 | 60 | /** 61 | * 判断应用是否安装. 62 | * 63 | * @param context 上下文 64 | * @param packageName 包名 65 | */ 66 | fun isAppInstalled( 67 | context: Context, 68 | packageName: String = context.packageName, 69 | ): Boolean { 70 | return try { 71 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 72 | context.packageManager.getPackageInfo( 73 | packageName, 74 | PackageManager.PackageInfoFlags.of(PackageManager.GET_ACTIVITIES.toLong()) 75 | ) 76 | } else { 77 | context.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) 78 | } 79 | true 80 | } catch (e: PackageManager.NameNotFoundException) { 81 | e.printStackTrace() 82 | false 83 | } 84 | } 85 | 86 | /** 87 | * 字节码签名转换:Landroid/view/View; -> android.view.View 88 | * 89 | * @param name 类名 90 | */ 91 | fun simpleName(name: String): String { 92 | if (name.startsWith('L') && name.endsWith(';') || name.contains('/')) 93 | return name.removePrefix("L").removeSuffix(";").replace("/", ".") 94 | 95 | return name 96 | } 97 | 98 | /** 99 | * 参数类型比较, 若某个参数为 null 则模糊匹配, 返回 `true`, 否则进行类型比较。 100 | * 101 | * @param method 被比较方法 102 | * @param targetParamTypes 被比较的参数类型列表 103 | */ 104 | fun compareParamTypes(method: Method, targetParamTypes: Array?>): Boolean { 105 | val parameterTypes = method.parameterTypes 106 | 107 | // 比较数量 108 | if (parameterTypes.size != targetParamTypes.size) { 109 | return false 110 | } 111 | 112 | for (i in parameterTypes.indices) { 113 | val type = parameterTypes[i] 114 | val targetType = targetParamTypes[i] ?: continue // null则模糊匹配 115 | 116 | // 类直接比较 117 | if (type == targetType) { 118 | continue 119 | } 120 | 121 | // 参数类型不一致 122 | return false 123 | } 124 | 125 | // 所有参数类型一致 126 | return true 127 | } 128 | } --------------------------------------------------------------------------------