├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lxbnjupt │ │ └── hotfixdemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── lxbnjupt │ │ │ └── hotfixdemo │ │ │ ├── BugTest.java │ │ │ ├── HotFixUtils.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ └── com │ └── lxbnjupt │ └── hotfixdemo │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 一步一步手动实现Android热更新 2 | 3 | 在[Android热更新实现原理浅析](https://www.jianshu.com/p/8dcf750acdfe)一文中,我们简单分析了Android热更新的实现原理,那么赶紧趁热打铁,一步一步手动实现热更新,莱茨狗。所见即所得,先看一下最终要达成的效果。 4 | 5 | ![热更新效果演示.gif](https://upload-images.jianshu.io/upload_images/5519943-dc0c372006757f34.gif?imageMogr2/auto-orient/strip) 6 | ### 一、热更新代码实现 7 | 基于之前的分析,我们知道实现热更新可以分为以下几个步骤: 8 | * 通过构造一个DexClassLoader对象来加载我们的热更新dex文件; 9 | * 通过反射获取系统默认的PathClassLoader.pathList.dexElements; 10 | * 将我们的热更新dex与系统默认的Elements数组合并,同时保证热更新dex在系统默认Elements数组之前; 11 | * 将合并完成后的数组设置回PathClassLoader.pathList.dexElements。 12 | 13 | ### Talk is cheap, show me the code. 14 | ```java 15 | public class HotFixUtils { 16 | 17 | private static final String TAG = "lxbnjupt"; 18 | private static final String NAME_BASE_DEX_CLASS_LOADER = "dalvik.system.BaseDexClassLoader"; 19 | private static final String FIELD_DEX_ELEMENTS = "dexElements"; 20 | private static final String FIELD_PATH_LIST = "pathList"; 21 | private static final String DEX_SUFFIX = ".dex"; 22 | private static final String APK_SUFFIX = ".apk"; 23 | private static final String JAR_SUFFIX = ".jar"; 24 | private static final String ZIP_SUFFIX = ".zip"; 25 | private static final String DEX_DIR = "patch"; 26 | private static final String OPTIMIZE_DEX_DIR = "odex"; 27 | 28 | public void doHotFix(Context context) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException { 29 | if (context == null) { 30 | return; 31 | } 32 | // 补丁存放目录为 /storage/emulated/0/Android/data/com.lxbnjupt.hotfixdemo/files/patch 33 | File dexFile = context.getExternalFilesDir(DEX_DIR); 34 | if (dexFile == null || !dexFile.exists()) { 35 | Log.e(TAG,"热更新补丁目录不存在"); 36 | return; 37 | } 38 | File odexFile = context.getDir(OPTIMIZE_DEX_DIR, Context.MODE_PRIVATE); 39 | if (!odexFile.exists()) { 40 | odexFile.mkdir(); 41 | } 42 | File[] listFiles = dexFile.listFiles(); 43 | if (listFiles == null || listFiles.length == 0) { 44 | return; 45 | } 46 | String dexPath = getPatchDexPath(listFiles); 47 | String odexPath = odexFile.getAbsolutePath(); 48 | // 获取PathClassLoader 49 | PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); 50 | // 构建DexClassLoader,用于加载补丁dex 51 | DexClassLoader dexClassLoader = new DexClassLoader(dexPath, odexPath, null, pathClassLoader); 52 | // 获取PathClassLoader的Element数组 53 | Object pathElements = getDexElements(pathClassLoader); 54 | // 获取构建的DexClassLoader的Element数组 55 | Object dexElements = getDexElements(dexClassLoader); 56 | // 合并Element数组 57 | Object combineElementArray = combineElementArray(pathElements, dexElements); 58 | // 通过反射,将合并后的Element数组赋值给PathClassLoader中pathList里面的dexElements变量 59 | setDexElements(pathClassLoader, combineElementArray); 60 | } 61 | 62 | /** 63 | * 获取补丁dex文件路径集合 64 | * @param listFiles 65 | * @return 66 | */ 67 | private String getPatchDexPath(File[] listFiles) { 68 | StringBuilder sb = new StringBuilder(); 69 | for (int i = 0; i < listFiles.length; i++) { 70 | // 遍历查找文件中.dex .jar .apk .zip结尾的文件 71 | File file = listFiles[i]; 72 | if (file.getName().endsWith(DEX_SUFFIX) 73 | || file.getName().endsWith(APK_SUFFIX) 74 | || file.getName().endsWith(JAR_SUFFIX) 75 | || file.getName().endsWith(ZIP_SUFFIX)) { 76 | if (i != 0 && i != (listFiles.length - 1)) { 77 | // 多个dex路径 添加默认的:分隔符 78 | sb.append(File.pathSeparator); 79 | } 80 | sb.append(file.getAbsolutePath()); 81 | } 82 | } 83 | return sb.toString(); 84 | } 85 | 86 | /** 87 | * 合并Element数组,将补丁dex放在最前面 88 | * @param pathElements PathClassLoader中pathList里面的Element数组 89 | * @param dexElements 补丁dex数组 90 | * @return 合并之后的Element数组 91 | */ 92 | private Object combineElementArray(Object pathElements, Object dexElements) { 93 | Class componentType = pathElements.getClass().getComponentType(); 94 | int i = Array.getLength(pathElements);// 原dex数组长度 95 | int j = Array.getLength(dexElements);// 补丁dex数组长度 96 | int k = i + j;// 总数组长度(原dex数组长度 + 补丁dex数组长度) 97 | Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组 98 | System.arraycopy(dexElements, 0, result, 0, j);// 补丁dex数组在前 99 | System.arraycopy(pathElements, 0, result, j, i);// 原dex数组在后 100 | return result; 101 | } 102 | 103 | /** 104 | * 获取Element数组 105 | * @param classLoader 类加载器 106 | * @return 107 | * @throws ClassNotFoundException 108 | * @throws NoSuchFieldException 109 | * @throws IllegalAccessException 110 | */ 111 | private Object getDexElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { 112 | // 获取BaseDexClassLoader,是PathClassLoader以及DexClassLoader的父类 113 | Class BaseDexClassLoaderClazz = Class.forName(NAME_BASE_DEX_CLASS_LOADER); 114 | // 获取pathList字段,并设置为可以访问 115 | Field pathListField = BaseDexClassLoaderClazz.getDeclaredField(FIELD_PATH_LIST); 116 | pathListField.setAccessible(true); 117 | // 获取DexPathList对象 118 | Object dexPathList = pathListField.get(classLoader); 119 | // 获取dexElements字段,并设置为可以访问 120 | Field dexElementsField = dexPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS); 121 | dexElementsField.setAccessible(true); 122 | // 获取Element数组,并返回 123 | return dexElementsField.get(dexPathList); 124 | } 125 | 126 | /** 127 | * 通过反射,将合并后的Element数组赋值给PathClassLoader中pathList里面的dexElements变量 128 | * @param classLoader PathClassLoader类加载器 129 | * @param value 合并后的Element数组 130 | * @throws ClassNotFoundException 131 | * @throws NoSuchFieldException 132 | * @throws IllegalAccessException 133 | */ 134 | private void setDexElements(ClassLoader classLoader, Object value) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { 135 | // 获取BaseDexClassLoader,是PathClassLoader以及DexClassLoader的父类 136 | Class BaseDexClassLoaderClazz = Class.forName(NAME_BASE_DEX_CLASS_LOADER); 137 | // 获取pathList字段,并设置为可以访问 138 | Field pathListField = BaseDexClassLoaderClazz.getDeclaredField(FIELD_PATH_LIST); 139 | pathListField.setAccessible(true); 140 | // 获取DexPathList对象 141 | Object dexPathList = pathListField.get(classLoader); 142 | // 获取dexElements字段,并设置为可以访问 143 | Field dexElementsField = dexPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS); 144 | dexElementsField.setAccessible(true); 145 | // 将合并后的Element数组赋值给dexElements变量 146 | dexElementsField.set(dexPathList, value); 147 | } 148 | } 149 | ``` 150 | 相信代码中的注释已经非常清楚了,这里就不再过多赘述。 151 | 152 | 不过,有一点需要注意一下,就是不要忘记打开读写手机存储权限: 153 | ```java 154 | 155 | 156 | ``` 157 | 158 | ### 二、测试验证 159 | #### 2.1 将java文件编译成class文件 160 | 完成修复bug之后,使用Android Studio的Rebuild Project功能将代码进行编译,然后从build目录下找到对应的class文件。 161 | 162 | ![](https://upload-images.jianshu.io/upload_images/5519943-a2fe4e3048ef452f.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 163 | #### 2.2 将class文件打包成dex文件 164 | ##### step1: 165 | 将修复好的class文件复制到其他任意地方,我这边是选择复制到桌面。注意,在复制这个class文件时,需要把它所在的完整包目录一起复制。上图中修复好的class文件是BugTest.class,其复制出来的目录结构如下图所示: 166 | 167 | ![](https://upload-images.jianshu.io/upload_images/5519943-52c4baf6693ec0a7.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 168 | ##### step2: 169 | 通过dx命令生成dex文件 170 | dx命令的使用有2种选择: 171 | * 配置环境变量(添加到classpath),然后命令行窗口(终端)可以在任意位置使用。 172 | * 不配环境变量,直接在build-tools/Android版本目录下使用命令行窗口(终端)使用。 173 | 这里直接使用第2种方式,命令如下: 174 | dx --dex --output=输出的dex文件完整路径 (空格) 要打包的完整class文件所在目录 175 | 176 | ![](https://upload-images.jianshu.io/upload_images/5519943-41d89e2645e04f10.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 177 | 178 | 完成之后,我们可以进到桌面文件夹下查看dex文件是否已经生成,我这边是成功生成了patch.dex文件。 179 | 180 | ![](https://upload-images.jianshu.io/upload_images/5519943-dc3005464bc90702.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 181 | #### 2.3 将dex文件push至手机 182 | 通过adb命令adb push ,可以将dex文件推到手机指定目录。在上面的demo中,我设置的热更新dex文件的存放路径是/storage/emulated/0/Android/data/com.lxbnjupt.hotfixdemo/files/patch 183 | 184 | ![](https://upload-images.jianshu.io/upload_images/5519943-a27a5f120df14d14.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 185 | #### 2.4 真机运行测试 186 | MainActivity.java 187 | ```java 188 | public class MainActivity extends AppCompatActivity { 189 | 190 | private static final String DEX_DIR = "patch"; 191 | private Button btnRun; 192 | private Button btnRepair; 193 | 194 | @Override 195 | protected void onCreate(Bundle savedInstanceState) { 196 | super.onCreate(savedInstanceState); 197 | setContentView(R.layout.activity_main); 198 | 199 | btnRun = (Button) findViewById(R.id.btn_run); 200 | btnRepair = (Button) findViewById(R.id.btn_repair); 201 | 202 | init(); 203 | setOnClickListener(); 204 | } 205 | 206 | private void init() { 207 | // 补丁存放目录为 /storage/emulated/0/Android/data/com.lxbnjupt.hotfixdemo/files/patch 208 | File patchDir = new File(this.getExternalFilesDir(null), DEX_DIR); 209 | if (!patchDir.exists()) { 210 | patchDir.mkdirs(); 211 | } 212 | } 213 | 214 | private void setOnClickListener() { 215 | btnRun.setOnClickListener(new View.OnClickListener() { 216 | @Override 217 | public void onClick(View v) { 218 | new BugTest().getBug(); 219 | } 220 | }); 221 | 222 | btnRepair.setOnClickListener(new View.OnClickListener() { 223 | @Override 224 | public void onClick(View v) { 225 | try { 226 | new HotFixUtils().doHotFix(MainActivity.this); 227 | } catch (IllegalAccessException e) { 228 | e.printStackTrace(); 229 | } catch (NoSuchFieldException e) { 230 | e.printStackTrace(); 231 | } catch (ClassNotFoundException e) { 232 | e.printStackTrace(); 233 | } 234 | } 235 | }); 236 | } 237 | } 238 | ``` 239 | 如文章开始的gif图所示,打开App,点击「运行」按钮,应用程序会因为NullPointerException直接crash。再次打开App,先点击「修复」按钮,再点击「运行」按钮,此时应用程序不会再出现crash,表示补丁加载成功,bug成功修复。至此,大功告成! 240 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.lxbnjupt.hotfixdemo" 7 | minSdkVersion 18 8 | targetSdkVersion 22 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 28 | } 29 | -------------------------------------------------------------------------------- /app/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 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/lxbnjupt/hotfixdemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.lxbnjupt.hotfixdemo; 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 | * Instrumented 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("com.lxbnjupt.hotfixdemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxbnjupt/hotfixdemo/BugTest.java: -------------------------------------------------------------------------------- 1 | package com.lxbnjupt.hotfixdemo; 2 | 3 | /** 4 | * Created by liuxiaobo on 2018/11/1. 5 | */ 6 | 7 | public class BugTest { 8 | 9 | public void getBug() { 10 | throw new NullPointerException(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxbnjupt/hotfixdemo/HotFixUtils.java: -------------------------------------------------------------------------------- 1 | package com.lxbnjupt.hotfixdemo; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import java.io.File; 7 | import java.lang.reflect.Array; 8 | import java.lang.reflect.Field; 9 | 10 | import dalvik.system.DexClassLoader; 11 | import dalvik.system.PathClassLoader; 12 | 13 | /** 14 | * Created by liuxiaobo on 2018/10/31. 15 | */ 16 | 17 | public class HotFixUtils { 18 | 19 | private static final String TAG = "lxbnjupt"; 20 | private static final String NAME_BASE_DEX_CLASS_LOADER = "dalvik.system.BaseDexClassLoader"; 21 | private static final String FIELD_DEX_ELEMENTS = "dexElements"; 22 | private static final String FIELD_PATH_LIST = "pathList"; 23 | private static final String DEX_SUFFIX = ".dex"; 24 | private static final String APK_SUFFIX = ".apk"; 25 | private static final String JAR_SUFFIX = ".jar"; 26 | private static final String ZIP_SUFFIX = ".zip"; 27 | private static final String DEX_DIR = "patch"; 28 | private static final String OPTIMIZE_DEX_DIR = "odex"; 29 | 30 | public void doHotFix(Context context) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException { 31 | if (context == null) { 32 | return; 33 | } 34 | // 补丁存放目录为 /storage/emulated/0/Android/data/com.lxbnjupt.hotfixdemo/files/patch 35 | File dexFile = context.getExternalFilesDir(DEX_DIR); 36 | if (dexFile == null || !dexFile.exists()) { 37 | Log.e(TAG,"热更新补丁目录不存在"); 38 | return; 39 | } 40 | File odexFile = context.getDir(OPTIMIZE_DEX_DIR, Context.MODE_PRIVATE); 41 | if (!odexFile.exists()) { 42 | odexFile.mkdir(); 43 | } 44 | File[] listFiles = dexFile.listFiles(); 45 | if (listFiles == null || listFiles.length == 0) { 46 | return; 47 | } 48 | String dexPath = getPatchDexPath(listFiles); 49 | String odexPath = odexFile.getAbsolutePath(); 50 | // 获取PathClassLoader 51 | PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); 52 | // 构建DexClassLoader,用于加载补丁dex 53 | DexClassLoader dexClassLoader = new DexClassLoader(dexPath, odexPath, null, pathClassLoader); 54 | // 获取PathClassLoader的Element数组 55 | Object pathElements = getDexElements(pathClassLoader); 56 | // 获取构建的DexClassLoader的Element数组 57 | Object dexElements = getDexElements(dexClassLoader); 58 | // 合并Element数组 59 | Object combineElementArray = combineElementArray(pathElements, dexElements); 60 | // 通过反射,将合并后的Element数组赋值给PathClassLoader中pathList里面的dexElements变量 61 | setDexElements(pathClassLoader, combineElementArray); 62 | } 63 | 64 | /** 65 | * 获取补丁dex文件路径集合 66 | * @param listFiles 67 | * @return 68 | */ 69 | private String getPatchDexPath(File[] listFiles) { 70 | StringBuilder sb = new StringBuilder(); 71 | for (int i = 0; i < listFiles.length; i++) { 72 | // 遍历查找文件中.dex .jar .apk .zip结尾的文件 73 | File file = listFiles[i]; 74 | if (file.getName().endsWith(DEX_SUFFIX) 75 | || file.getName().endsWith(APK_SUFFIX) 76 | || file.getName().endsWith(JAR_SUFFIX) 77 | || file.getName().endsWith(ZIP_SUFFIX)) { 78 | if (i != 0 && i != (listFiles.length - 1)) { 79 | // 多个dex路径 添加默认的:分隔符 80 | sb.append(File.pathSeparator); 81 | } 82 | sb.append(file.getAbsolutePath()); 83 | } 84 | } 85 | return sb.toString(); 86 | } 87 | 88 | /** 89 | * 合并Element数组,将补丁dex放在最前面 90 | * @param pathElements PathClassLoader中pathList里面的Element数组 91 | * @param dexElements 补丁dex数组 92 | * @return 合并之后的Element数组 93 | */ 94 | private Object combineElementArray(Object pathElements, Object dexElements) { 95 | Class componentType = pathElements.getClass().getComponentType(); 96 | int i = Array.getLength(pathElements);// 原dex数组长度 97 | int j = Array.getLength(dexElements);// 补丁dex数组长度 98 | int k = i + j;// 总数组长度(原dex数组长度 + 补丁dex数组长度) 99 | Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组 100 | System.arraycopy(dexElements, 0, result, 0, j);// 补丁dex数组在前 101 | System.arraycopy(pathElements, 0, result, j, i);// 原dex数组在后 102 | return result; 103 | } 104 | 105 | /** 106 | * 获取Element数组 107 | * @param classLoader 类加载器 108 | * @return 109 | * @throws ClassNotFoundException 110 | * @throws NoSuchFieldException 111 | * @throws IllegalAccessException 112 | */ 113 | private Object getDexElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { 114 | // 获取BaseDexClassLoader,是PathClassLoader以及DexClassLoader的父类 115 | Class BaseDexClassLoaderClazz = Class.forName(NAME_BASE_DEX_CLASS_LOADER); 116 | // 获取pathList字段,并设置为可以访问 117 | Field pathListField = BaseDexClassLoaderClazz.getDeclaredField(FIELD_PATH_LIST); 118 | pathListField.setAccessible(true); 119 | // 获取DexPathList对象 120 | Object dexPathList = pathListField.get(classLoader); 121 | // 获取dexElements字段,并设置为可以访问 122 | Field dexElementsField = dexPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS); 123 | dexElementsField.setAccessible(true); 124 | // 获取Element数组,并返回 125 | return dexElementsField.get(dexPathList); 126 | } 127 | 128 | /** 129 | * 通过反射,将合并后的Element数组赋值给PathClassLoader中pathList里面的dexElements变量 130 | * @param classLoader PathClassLoader类加载器 131 | * @param value 合并后的Element数组 132 | * @throws ClassNotFoundException 133 | * @throws NoSuchFieldException 134 | * @throws IllegalAccessException 135 | */ 136 | private void setDexElements(ClassLoader classLoader, Object value) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { 137 | // 获取BaseDexClassLoader,是PathClassLoader以及DexClassLoader的父类 138 | Class BaseDexClassLoaderClazz = Class.forName(NAME_BASE_DEX_CLASS_LOADER); 139 | // 获取pathList字段,并设置为可以访问 140 | Field pathListField = BaseDexClassLoaderClazz.getDeclaredField(FIELD_PATH_LIST); 141 | pathListField.setAccessible(true); 142 | // 获取DexPathList对象 143 | Object dexPathList = pathListField.get(classLoader); 144 | // 获取dexElements字段,并设置为可以访问 145 | Field dexElementsField = dexPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS); 146 | dexElementsField.setAccessible(true); 147 | // 将合并后的Element数组赋值给dexElements变量 148 | dexElementsField.set(dexPathList, value); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/lxbnjupt/hotfixdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.lxbnjupt.hotfixdemo; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.Button; 7 | 8 | import java.io.File; 9 | 10 | public class MainActivity extends AppCompatActivity { 11 | 12 | private static final String DEX_DIR = "patch"; 13 | private Button btnRun; 14 | private Button btnRepair; 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_main); 20 | 21 | btnRun = (Button) findViewById(R.id.btn_run); 22 | btnRepair = (Button) findViewById(R.id.btn_repair); 23 | 24 | init(); 25 | setOnClickListener(); 26 | } 27 | 28 | private void init() { 29 | // 补丁存放目录为 /storage/emulated/0/Android/data/com.lxbnjupt.hotfixdemo/files/patch 30 | File patchDir = new File(this.getExternalFilesDir(null), DEX_DIR); 31 | if (!patchDir.exists()) { 32 | patchDir.mkdirs(); 33 | } 34 | } 35 | 36 | private void setOnClickListener() { 37 | btnRun.setOnClickListener(new View.OnClickListener() { 38 | @Override 39 | public void onClick(View v) { 40 | new BugTest().getBug(); 41 | } 42 | }); 43 | 44 | btnRepair.setOnClickListener(new View.OnClickListener() { 45 | @Override 46 | public void onClick(View v) { 47 | try { 48 | new HotFixUtils().doHotFix(MainActivity.this); 49 | } catch (IllegalAccessException e) { 50 | e.printStackTrace(); 51 | } catch (NoSuchFieldException e) { 52 | e.printStackTrace(); 53 | } catch (ClassNotFoundException e) { 54 | e.printStackTrace(); 55 | } 56 | } 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |