├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── CMakeLists.txt ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── pqpo │ │ └── methodhook │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ └── method-hook-lib.cpp │ ├── java │ │ └── me │ │ │ └── pqpo │ │ │ └── methodhook │ │ │ ├── HookManager.java │ │ │ ├── MainActivity.java │ │ │ └── MethodHook.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── me │ └── pqpo │ └── methodhook │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/dictionaries 41 | .idea/libraries 42 | 43 | # Keystore files 44 | *.jks 45 | 46 | # External native build folder generated in Android Studio 2.2 and later 47 | .externalNativeBuild 48 | 49 | # Google Services (e.g. APIs or Firebase) 50 | google-services.json 51 | 52 | # Freeline 53 | freeline.py 54 | freeline/ 55 | 56 | freeline_project_description.json 57 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 原文链接: [热修复之 Method Hook 原理分析](https://pqpo.me/2017/07/07/hotfix-method-hook/) 2 | 3 | ## 引言 4 | 5 | 目前国内大厂均开源了自己的 Android 热修复框架,阿里的《深入探索 Android 热修复技术原理》全面介绍了热修复技术的现状,原理与展望。一方面是阿里系为代表的底层方法替换,另一方面是以腾讯系为代表的类加载方案。前者支持立即生效,但是限制比较多;后者必须冷启动生效,相对较稳定,修复范围广。之前分析过微信的热修复框架 Tinker 即属于后者, [《Tinker 接入及源码分析》](https://pqpo.me/2017/01/07/tinker-analysis1/)。本篇文章主要分析以 AndFix 为代表的底层方法替换方案,并且实现了《深入探索 Android 热修复技术原理》中提到的方法替换新方案。 6 | 7 | 方法替换是 AndFix 的热修复方案的关键,虚拟机在加载一个类的时候会将类中方法解析成 ArtMethod 结构体,结构体中保存着一些运行时的必要信息以及需要执行的指令指针地址。那么我们只要在 native 层将原方法的 ArtMethod 结构体替换成新方法的结构体,那么执行原方法的时候便会执行到新方法的指令,完成了方法替换。 8 | 9 | Andfix 中的关键代码如下: 10 | 11 | ```java 12 | 13 | public static void addReplaceMethod(Method src, Method dest) { 14 | try { 15 | replaceMethod(src, dest); 16 | initFields(dest.getDeclaringClass()); 17 | } catch (Throwable e) { 18 | Log.e(TAG, "addReplaceMethod", e); 19 | } 20 | } 21 | 22 | ``` 23 | 24 | replaceMethod 是 native 方法,查看其实现: 25 | 26 | ```c++ 27 | 28 | static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, 29 | jobject dest) { 30 | if (isArt) { 31 | art_replaceMethod(env, src, dest); 32 | } else { 33 | dalvik_replaceMethod(env, src, dest); 34 | } 35 | } 36 | 37 | ``` 38 | 39 | ART 与 Dalvik 虚拟机分别有不同的实现,看 ART 虚拟机下的实现代码: 40 | 41 | ```c++ 42 | 43 | extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod( 44 | JNIEnv* env, jobject src, jobject dest) { 45 | if (apilevel > 23) { 46 | replace_7_0(env, src, dest); 47 | } else if (apilevel > 22) { 48 | replace_6_0(env, src, dest); 49 | } else if (apilevel > 21) { 50 | replace_5_1(env, src, dest); 51 | } else if (apilevel > 19) { 52 | replace_5_0(env, src, dest); 53 | }else{ 54 | replace_4_4(env, src, dest); 55 | } 56 | } 57 | 58 | ``` 59 | 60 | 由于不同的版本 ArtMethod 结构体参数会一样,所以不同的版本又有不同的实现,每种实现本地保留一份不同的结构体代码,我们看 6.0 的版本: 61 | 62 | ```c++ 63 | 64 | void replace_6_0(JNIEnv* env, jobject src, jobject dest) { 65 | art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src); 66 | art::mirror::ArtMethod* dmeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(dest); 67 | 68 | reinterpret_cast(dmeth->declaring_class_)->class_loader_ = 69 | reinterpret_cast(smeth->declaring_class_)->class_loader_; //for plugin classloader 70 | reinterpret_cast(dmeth->declaring_class_)->clinit_thread_id_ = 71 | reinterpret_cast(smeth->declaring_class_)->clinit_thread_id_; 72 | reinterpret_cast(dmeth->declaring_class_)->status_ = reinterpret_cast(smeth->declaring_class_)->status_-1; 73 | //for reflection invoke 74 | reinterpret_cast(dmeth->declaring_class_)->super_class_ = 0; 75 | smeth->declaring_class_ = dmeth->declaring_class_; 76 | smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_; 77 | smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_; 78 | smeth->access_flags_ = dmeth->access_flags_ | 0x0001; 79 | smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_; 80 | smeth->dex_method_index_ = dmeth->dex_method_index_; 81 | smeth->method_index_ = dmeth->method_index_; 82 | smeth->ptr_sized_fields_.entry_point_from_interpreter_ = 83 | dmeth->ptr_sized_fields_.entry_point_from_interpreter_; 84 | smeth->ptr_sized_fields_.entry_point_from_jni_ = 85 | dmeth->ptr_sized_fields_.entry_point_from_jni_; 86 | smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ = 87 | dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_; 88 | } 89 | 90 | ``` 91 | 92 | 除前两行代码,后面都是结构体内容的替换,env->FromReflectedMethod(src) 返回的是 jmethodID ,事实上就是 ArtMethod 结构体的指针地址,所以可以强制类型转换成 ArtMethod 结构体指针。最终就完成了方法替换。 93 | 94 | 上面的实现有很多局限性,由于不同系统版本 ArtMethod 结构体会不一致,所以本地保留了不同版本的 ArtMethod 结构体代码,每次 Android 有新版本发布均需要做一次兼容,而且如果第三方 ROM 修改了 ArtMethod 结构体,那么这种方案就会失效。 95 | 96 | 《深入探索 Android 热修复技术原理》给出了一种更优雅的实现方案,不关心 ArtMethod 的内部结构,直接通过内存地址替换整个 ArtMethod,我使用文中提供的方案使用极少的代码实现了一个 Demo 并上传到了 GitHub 上:[https://github.com/pqpo/MethodHook](https://github.com/pqpo/MethodHook) 97 | 98 | 主要就提供了一个 MethodHook 类完成类方法替换,使用方法如下: 99 | 100 | ```java 101 | 102 | public class MainActivity extends AppCompatActivity { 103 | 104 | MethodHook methodHook; 105 | Method srcMethod; 106 | Method destMethod; 107 | 108 | @Override 109 | protected void onCreate(Bundle savedInstanceState) { 110 | super.onCreate(savedInstanceState); 111 | setContentView(R.layout.activity_main); 112 | methodHook = new MethodHook(); 113 | 114 | Button btnClick = (Button) findViewById(R.id.click); 115 | Button btnHook = (Button) findViewById(R.id.hook); 116 | Button btnRestore = (Button) findViewById(R.id.restore); 117 | 118 | btnClick.setOnClickListener(new View.OnClickListener() { 119 | @Override 120 | public void onClick(View v) { 121 | //方法替换前显示Hello!,方法替换后实际调用showHookToast()显示Hello Hook! 122 | showToast(); 123 | } 124 | }); 125 | 126 | try { 127 | srcMethod = getClass().getDeclaredMethod("showToast"); 128 | destMethod = getClass().getDeclaredMethod("showHookToast"); 129 | } catch (NoSuchMethodException e) { 130 | e.printStackTrace(); 131 | } 132 | 133 | btnHook.setOnClickListener(new View.OnClickListener() { 134 | @Override 135 | public void onClick(View v) { 136 | //方法替换 137 | methodHook.hook(srcMethod, destMethod); 138 | } 139 | }); 140 | 141 | btnRestore.setOnClickListener(new View.OnClickListener() { 142 | @Override 143 | public void onClick(View v) { 144 | //方法恢复 145 | methodHook.restore(srcMethod); 146 | } 147 | }); 148 | 149 | } 150 | 151 | public void showToast() { 152 | Toast.makeText(this, "Hello!", Toast.LENGTH_SHORT).show(); 153 | } 154 | 155 | public void showHookToast() { 156 | Toast.makeText(this, "Hello Hook!", Toast.LENGTH_SHORT).show(); 157 | } 158 | 159 | } 160 | 161 | ``` 162 | 163 | 界面如下: 164 | 165 | ![](https://o0iqn064q.qnssl.com/wp-content/uploads/2017/07/methodhook1-169x300.png) ![](https://o0iqn064q.qnssl.com/wp-content/uploads/2017/07/methodhook2-169x300.png) 166 | 167 | 点击 CLICK 按钮出现图一;点击 HOOK 按钮,再点击 CLICK 按钮出现图二;点击 RESTORE 按钮,再点击 CLICK 按钮出现图一。整个过程完成了方法的替换与恢复。 168 | 169 | Java 层核心方法如下: 170 | 171 | ```java 172 | 173 | public class MethodHook { 174 | 175 | public static void m1(){} 176 | public static void m2(){} 177 | 178 | private Map methodBackup = new ConcurrentHashMap<>(); 179 | 180 | public void hook(Method src, Method dest) { 181 | if (src == null || dest == null) { 182 | return; 183 | } 184 | if (!methodBackup.containsKey(src)) { 185 | methodBackup.put(src, hook_native(src, dest)); 186 | } 187 | } 188 | 189 | public void restore(Method src) { 190 | if (src == null) { 191 | return; 192 | } 193 | Long srcMethodPtr = methodBackup.get(src); 194 | if (srcMethodPtr != null) { 195 | methodBackup.remove(restore_native(src, srcMethodPtr)); 196 | } 197 | } 198 | 199 | private static native long hook_native(Method src, Method dest); 200 | private static native Method restore_native(Method src, long methodPtr); 201 | 202 | static { 203 | System.loadLibrary("method-hook-lib"); 204 | } 205 | 206 | } 207 | 208 | ``` 209 | 210 | 其中 hook 方法将完成方法替换,restore 方法恢复原来的方法, Map methodBackup 用于保存备份的方法地址,Key 为备份的方法,Value 为方法备份的内存地址。 211 | 212 | 主要逻辑位于 native 层,我们知道 System.loadLibrary 加载一个动态库是,首先会调用其 JNI_OnLoad 函数,在 JNI_OnLoad 函数中完成一般会完成 native 方法的动态注册,在本例中还加入了如下代码,用于计算不同平台 ArtMethod 结构体的大小 : 213 | 214 | ```c++ 215 | 216 | methodHookClassInfo.m1 = env -> GetStaticMethodID(classEvaluateUtil, "m1", "()V"); 217 | methodHookClassInfo.m2 = env -> GetStaticMethodID(classEvaluateUtil, "m2", "()V"); 218 | methodHookClassInfo.methodSize = reinterpret_cast(methodHookClassInfo.m2) - reinterpret_cast(methodHookClassInfo.m1); 219 | 220 | ``` 221 | 222 | 在 Java MethodHook 类中有下面两个静态的空方法: 223 | 224 | ```java 225 | 226 | public static void m1(){} 227 | public static void m2(){} 228 | 229 | ``` 230 | 231 | 在 native 层获取这两个方法的 jmethodID, 事实上就是 ArtMethod 结构体的指针地址,由于 m1,m2 方法是相邻的,其在 native 层的 ArtMethod 结构体也是相邻的,所以它们内存地址的差值就是 ArtMethod 结构体的大小。 232 | 接下来看方法替换的逻辑: 233 | 234 | ```c++ 235 | 236 | static long methodHook(JNIEnv* env, jclass type, jobject srcMethodObj, jobject destMethodObj) { 237 | void* srcMethod = reinterpret_cast(env -> FromReflectedMethod(srcMethodObj)); 238 | void* destMethod = reinterpret_cast(env -> FromReflectedMethod(destMethodObj)); 239 | int* backupMethod = new int[methodHookClassInfo.methodSize]; 240 | memcpy(backupMethod, srcMethod, methodHookClassInfo.methodSize); 241 | memcpy(srcMethod, destMethod, methodHookClassInfo.methodSize); 242 | return reinterpret_cast(backupMethod); 243 | } 244 | 245 | ``` 246 | 247 | 这里的 srcMethodObj,destMethodObj 对应 Java 层的 Method 类, 通过 env -> FromReflectedMethod 获取方法的 jmethodID, 实际上就获取了方法位于 native 层 ArtMethod 结构体的指针地址。 248 | 一开始已经计算出 ArtMethod 结构体的大小并保存在了 methodHookClassInfo.methodSize 中。再新构造一个 int 数组来备份原方法。使用 memcpy 将原函数 ArtMethod 内存拷贝至备份的数组中,然后使用 memcpy 将目标函数 ArtMethod 结构体拷贝至原函数结构体指针起始位置完成结构体替换。最后将用于备份的 int 数组的指针强制类型转换为 long 类型返回给 Java 层以供后续恢复之用。 249 | 250 | 下面是方法恢复的函数: 251 | 252 | 253 | ```c++ 254 | 255 | static jobject methodRestore(JNIEnv* env, jclass type, jobject srcMethod, jlong methodPtr) { 256 | int* backupMethod = reinterpret_cast(methodPtr); 257 | void* artMethodSrc = reinterpret_cast(env -> FromReflectedMethod(srcMethod)); 258 | memcpy(artMethodSrc, backupMethod, methodHookClassInfo.methodSize); 259 | delete []backupMethod; 260 | return srcMethod; 261 | } 262 | 263 | ``` 264 | 265 | 将上一步保存的 long 类型地址强制转换成 int 指针,然后获取原函数 ArtMethod 结构体地址,接着将 int 数组的内容恢复至原函数内存地址处,完成恢复函数,最后释放备份用的 int 数组。 266 | 267 | 可以看出这种方法相比较之前的方案优雅不少,不用考虑 ArtMethod 的内部结构,巧妙的计算出不同平台的 ArtMethod 结构体大小,从而不需要做任何适配工作。然而若要真正要将这个技术应用到热修复框架中还需要考虑很多其他因素,本文抛砖引玉,只讲述 Method Hook 的原理,对热修复知识有兴趣的可以阅读阿里的《深入探索 Android 热修复技术原理》。文章讲到了阿里的热修复框架 Sophix 结合了方法替换与类加载替换方案值得期待。 268 | 269 | 参考: 270 | 271 | - [Android热修复升级探索——追寻极致的代码热替换](https://yq.aliyun.com/articles/74598?t=t1#) 272 | - [AndFix](https://github.com/alibaba/AndFix) 273 | - 《深入探索 Android 热修复技术原理》 274 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.4.1) 7 | 8 | # Creates and names a library, sets it as either STATIC 9 | # or SHARED, and provides the relative paths to its source code. 10 | # You can define multiple libraries, and CMake builds them for you. 11 | # Gradle automatically packages shared libraries with your APK. 12 | 13 | add_library( # Sets the name of the library. 14 | method-hook-lib 15 | 16 | # Sets the library as a shared library. 17 | SHARED 18 | 19 | # Provides a relative path to your source file(s). 20 | src/main/cpp/method-hook-lib.cpp ) 21 | 22 | # Searches for a specified prebuilt library and stores the path as a 23 | # variable. Because CMake includes system libraries in the search path by 24 | # default, you only need to specify the name of the public NDK library 25 | # you want to add. CMake verifies that the library exists before 26 | # completing its build. 27 | 28 | find_library( # Sets the name of the path variable. 29 | log-lib 30 | 31 | # Specifies the name of the NDK library that 32 | # you want CMake to locate. 33 | log ) 34 | 35 | # Specifies libraries CMake should link to your target library. You 36 | # can link multiple libraries, such as libraries you define in this 37 | # build script, prebuilt third-party libraries, or system libraries. 38 | 39 | target_link_libraries( # Specifies the target library. 40 | method-hook-lib 41 | 42 | # Links the target library to the log library 43 | # included in the NDK. 44 | ${log-lib} ) -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | defaultConfig { 7 | applicationId "me.pqpo.artmethodhook" 8 | minSdkVersion 14 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | externalNativeBuild { 14 | cmake { 15 | cppFlags "-std=c++11 -frtti -fexceptions" 16 | } 17 | } 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | externalNativeBuild { 26 | cmake { 27 | path "CMakeLists.txt" 28 | } 29 | } 30 | } 31 | 32 | dependencies { 33 | compile fileTree(dir: 'libs', include: ['*.jar']) 34 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 35 | exclude group: 'com.android.support', module: 'support-annotations' 36 | }) 37 | compile 'com.android.support:appcompat-v7:25.3.1' 38 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 39 | testCompile 'junit:junit:4.12' 40 | } 41 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/qiulinmin/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/me/pqpo/methodhook/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package me.pqpo.methodhook; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("me.pqpo.artmethodhook", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/cpp/method-hook-lib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | // 4 | // Created by qiulinmin on 7/7/17. 5 | // 6 | static const char* kClassMethodHookChar = "me/pqpo/methodhook/MethodHook"; 7 | 8 | static struct { 9 | jmethodID m1; 10 | jmethodID m2; 11 | size_t methodSize; 12 | } methodHookClassInfo; 13 | 14 | 15 | static long methodHook(JNIEnv* env, jclass type, jobject srcMethodObj, jobject destMethodObj) { 16 | void* srcMethod = reinterpret_cast(env -> FromReflectedMethod(srcMethodObj)); 17 | void* destMethod = reinterpret_cast(env -> FromReflectedMethod(destMethodObj)); 18 | int* backupMethod = new int[methodHookClassInfo.methodSize]; 19 | memcpy(backupMethod, srcMethod, methodHookClassInfo.methodSize); 20 | memcpy(srcMethod, destMethod, methodHookClassInfo.methodSize); 21 | return reinterpret_cast(backupMethod); 22 | } 23 | 24 | static jobject methodRestore(JNIEnv* env, jclass type, jobject srcMethod, jlong methodPtr) { 25 | int* backupMethod = reinterpret_cast(methodPtr); 26 | void* artMethodSrc = reinterpret_cast(env -> FromReflectedMethod(srcMethod)); 27 | memcpy(artMethodSrc, backupMethod, methodHookClassInfo.methodSize); 28 | delete []backupMethod; 29 | return srcMethod; 30 | } 31 | 32 | static JNINativeMethod gMethods[] = { 33 | { 34 | "hook_native", 35 | "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)J", 36 | (void*)methodHook 37 | }, 38 | { 39 | "restore_native", 40 | "(Ljava/lang/reflect/Method;J)Ljava/lang/reflect/Method;", 41 | (void*)methodRestore 42 | } 43 | }; 44 | 45 | extern "C" 46 | JNIEXPORT jint JNICALL 47 | JNI_OnLoad(JavaVM* vm, void* reserved) { 48 | JNIEnv *env = nullptr; 49 | if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) { 50 | return JNI_FALSE; 51 | } 52 | jclass classEvaluateUtil = env->FindClass(kClassMethodHookChar); 53 | if(env -> RegisterNatives(classEvaluateUtil, gMethods, sizeof(gMethods)/ sizeof(gMethods[0])) < 0) { 54 | return JNI_FALSE; 55 | } 56 | methodHookClassInfo.m1 = env -> GetStaticMethodID(classEvaluateUtil, "m1", "()V"); 57 | methodHookClassInfo.m2 = env -> GetStaticMethodID(classEvaluateUtil, "m2", "()V"); 58 | methodHookClassInfo.methodSize = reinterpret_cast(methodHookClassInfo.m2) - reinterpret_cast(methodHookClassInfo.m1); 59 | return JNI_VERSION_1_4; 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/me/pqpo/methodhook/HookManager.java: -------------------------------------------------------------------------------- 1 | package me.pqpo.methodhook; 2 | 3 | import android.support.v4.util.Pair; 4 | 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.lang.reflect.Method; 7 | import java.lang.reflect.Modifier; 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | 11 | /** 12 | * Created by qiulinmin on 7/10/17. 13 | */ 14 | 15 | public final class HookManager { 16 | 17 | private HookManager(){} 18 | 19 | public static HookManager get() { 20 | return InstanceHolder.sInstance; 21 | } 22 | 23 | private static class InstanceHolder { 24 | private static HookManager sInstance = new HookManager(); 25 | } 26 | 27 | private Map, MethodHook> methodHookMap = new ConcurrentHashMap<>(); 28 | 29 | public void hookMethod(Method originMethod, Method hookMethod) { 30 | if (originMethod == null || hookMethod == null) { 31 | throw new IllegalArgumentException("argument cannot be null"); 32 | } 33 | // if (!Modifier.isStatic(hookMethod.getModifiers())) { 34 | // throw new IllegalArgumentException("hook method must be static"); 35 | // } 36 | Pair key = Pair.create(hookMethod.getDeclaringClass().getName(), hookMethod.getName()); 37 | if (methodHookMap.containsKey(key)) { 38 | MethodHook methodHook = methodHookMap.get(key); 39 | methodHook.restore(); 40 | } 41 | MethodHook methodHook = new MethodHook(originMethod, hookMethod); 42 | methodHookMap.put(key, methodHook); 43 | methodHook.hook(); 44 | } 45 | 46 | public void callOrigin(Object receiver, Object... args) { 47 | StackTraceElement stackTrace = Thread.currentThread().getStackTrace()[3]; 48 | String className = stackTrace.getClassName(); 49 | String methodName = stackTrace.getMethodName(); 50 | MethodHook methodHook = methodHookMap.get(Pair.create(className, methodName)); 51 | if (methodHook != null) { 52 | try { 53 | methodHook.callOrigin(receiver, args); 54 | } catch (InvocationTargetException e) { 55 | e.printStackTrace(); 56 | } catch (IllegalAccessException e) { 57 | e.printStackTrace(); 58 | } 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/me/pqpo/methodhook/MainActivity.java: -------------------------------------------------------------------------------- 1 | package me.pqpo.methodhook; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.Toast; 9 | 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.lang.reflect.Method; 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_main); 19 | 20 | Button btnClick = (Button) findViewById(R.id.click); 21 | Button btnHook = (Button) findViewById(R.id.hook); 22 | Button btnRestore = (Button) findViewById(R.id.restore); 23 | 24 | btnClick.setOnClickListener(new View.OnClickListener() { 25 | @Override 26 | public void onClick(View v) { 27 | showToast("Hello!"); 28 | } 29 | }); 30 | 31 | btnHook.setOnClickListener(new View.OnClickListener() { 32 | @Override 33 | public void onClick(View v) { 34 | try { 35 | Method srcMethod = MainActivity.class.getDeclaredMethod("showToast", String.class); 36 | Method destMethod = MainActivity.class.getDeclaredMethod("showHookToast", String.class); 37 | HookManager.get().hookMethod(srcMethod, destMethod); 38 | } catch (NoSuchMethodException e) { 39 | e.printStackTrace(); 40 | } 41 | } 42 | }); 43 | 44 | btnRestore.setOnClickListener(new View.OnClickListener() { 45 | @Override 46 | public void onClick(View v) { 47 | //hook 失败 48 | // try { 49 | // HookManager.get().hookMethod(Toast.class.getDeclaredMethod("show"), MainActivity.class.getDeclaredMethod("Toast_show")); 50 | // } catch (NoSuchMethodException e) { 51 | // e.printStackTrace(); 52 | // } 53 | } 54 | 55 | }); 56 | 57 | 58 | } 59 | 60 | public void showToast(String msg) { 61 | Toast toast = Toast.makeText(this, msg, Toast.LENGTH_SHORT); 62 | toast.show(); 63 | } 64 | 65 | public void showHookToast(String msg) { 66 | Log.d("MainActivity", "msg:" + msg); 67 | HookManager.get().callOrigin(this, msg + "(Hook)"); 68 | } 69 | 70 | public void Toast_show() { 71 | Log.d("MainActivity", "Toast_show"); 72 | HookManager.get().callOrigin(this); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/me/pqpo/methodhook/MethodHook.java: -------------------------------------------------------------------------------- 1 | package me.pqpo.methodhook; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | 6 | /** 7 | * Created by qiulinmin on 7/7/17. 8 | */ 9 | 10 | public class MethodHook { 11 | 12 | public static void m1(){} 13 | public static void m2(){} 14 | 15 | private Method srcMethod; 16 | private Method hookMethod; 17 | 18 | private long backupMethodPtr; 19 | 20 | public MethodHook(Method src, Method dest) { 21 | srcMethod = src; 22 | hookMethod = dest; 23 | srcMethod.setAccessible(true); 24 | hookMethod.setAccessible(true); 25 | } 26 | 27 | public void hook() { 28 | if (backupMethodPtr == 0) { 29 | backupMethodPtr = hook_native(srcMethod, hookMethod); 30 | } 31 | } 32 | 33 | public void restore() { 34 | if (backupMethodPtr != 0) { 35 | restore_native(srcMethod, backupMethodPtr); 36 | backupMethodPtr = 0; 37 | } 38 | } 39 | 40 | public void callOrigin(Object receiver, Object... args) throws InvocationTargetException, IllegalAccessException { 41 | if (backupMethodPtr != 0) { 42 | restore(); 43 | srcMethod.invoke(receiver, args); 44 | hook(); 45 | } else { 46 | srcMethod.invoke(receiver, args); 47 | } 48 | } 49 | 50 | private static native long hook_native(Method src, Method dest); 51 | private static native Method restore_native(Method src, long methodPtr); 52 | 53 | static { 54 | System.loadLibrary("method-hook-lib"); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 |