Android 动手实现热修复

有了前面文章的理论支持,下面动手自己写一下热修复.

创建工程

1
2
3
4
5
6
7
8
9
├── main
│   ├── AndroidManifest.xml
│   ├── java
│   │   └── xyz
│   │   └── hanks
│   │   └── fix
│   │   ├── BugClass.java
│   │   ├── FixApplication.java
│   │   └── MainActivity.java

通过Android Studio创建一个工程. BugClass 类是需要修复的类, MainActivity是主Activity, FixApplication是自定义的Application. 初始的MainActivity如下

1
2
3
4
5
6
7
8
9
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = (TextView) findViewById(R.id.text);
textView.setText(new BugClass().showToast("Happy new year"));
}
}
1
2
3
4
5
6
7
8
/**
* Created by hanks on 16-1-2.
*/
public class BugClass {
public String showToast(String content) {
return content;
}
}

现在运行程序,界面的文本显示成文字 Happy new year,MainActivity类会被加上 CLASS_ISPREVERIFIED 标志,因为BugClass 和 MainActivity 都属于同一个dex. 如果现在直接加载补丁包中的 BugClass 类,那么就会出现 Class ref in pre-verified class resolved to unexpected implementation 错误.

引用hack.dex,防止类加上CLASS_ISPREVERIFIED

因为我们要修复BugClass类,而调用是在MainActivity中,也就是说,当打上补丁包之后,MainActivity调用的BugClass将会是补丁包中的BugClass(也就是来自于其他的dex),那么我就就需要防止MainActivity被加上 CLASS_ISPREVERIFIED 标志. 那么怎么防止呢? 需要在MainActivity中引用别的dex(hack.dex)中的一个类.那么代码如下:

1
2
3
4
5
6
7
8
9
10
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = (TextView) findViewById(R.id.text);
System.out.print(Hack.class); // 引用 hack.dex中的Hack类
textView.setText(new BugClass().showToast("Happy new year"));
}
}

上面代码只是简单的引用了一下 Hack.class, 这样程序运行起来就不会把MainActivity加上 CLASS_ISPREVERIFIED. 注意现在的代码是编译不过的. 引用我们的程序中没有Hack.class, 要想编译通过,那么我们就得有 Hack.class, 于是新建一个library, 然后app这个依赖与这个library, 但是注意不要使用 compile, 使用 provided 关键字,这样标示这个library这是提供引用,并不被编译到apk中(不在MainActivity的dex中).这样就解决了编译问题.
现在运行起来程序还是有错误, 因为MainActivity引用了Hack.class,虽然编译通过了,但是实际上是没有这个类的,所以这个时候就需要在调用Hack这个类之前,先动态加载进来.

先加载Hack.dex,保证引用不会出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 加载
* Created by hanks on 16-1-3.
*/
public class FixApplication extends Application {
@Override public void onCreate() {
super.onCreate();
try {
PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
String hackFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() +"/hack.dex"; // hack.dex的路径
Object a = combineArray(getDexElements(getPathList(pathClassLoader)), // 原有的 dex
getDexElements(getPathList(new DexClassLoader(hackFilePath, getDir("dex", 0).getAbsolutePath(), hackFilePath, getClassLoader())))); // 将新的dex插入到dexElements数组的前面
Object a2 = getPathList(pathClassLoader);
setField(a2, a2.getClass(), "dexElements", a); // 通过反射修改dexElements数组
pathClassLoader.loadClass("xyz.hanks.Hack");
} catch (Exception e) {
e.printStackTrace();
}
}
}

这样程序就正常运行起来了.

生成补丁包

现在BugClass出现bug了. 修改一下, 然后将修改后的BugClass导出jar包,然后通过dx工具转换成dex,就叫做 patch.dex 吧.然后放入到sdcard目录下.

现在可以加载补丁包了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 加载
* Created by hanks on 16-1-3.
*/
public class FixApplication extends Application {
@Override public void onCreate() {
super.onCreate();
try {
PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
String hackFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() +"/hack.dex"; // hack.dex的路径
Object a = combineArray(getDexElements(getPathList(pathClassLoader)), // 原有的 dex
getDexElements(getPathList(new DexClassLoader(hackFilePath, getDir("dex", 0).getAbsolutePath(), hackFilePath, getClassLoader())))); // 将新的dex插入到dexElements数组的前面
Object a2 = getPathList(pathClassLoader);
setField(a2, a2.getClass(), "dexElements", a); // 通过反射修改dexElements数组
pathClassLoader.loadClass("xyz.hanks.Hack");
// 加载补丁包
String patchFilePath = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/patch.dex";
Object a3 = combineArray(getDexElements(getPathList(pathClassLoader)), // 原有的 dex
getDexElements(getPathList(new DexClassLoader(patchFilePath, getDir("dex", 0).getAbsolutePath(), patchFilePath, getClassLoader()))));
Object a4 = getPathList(pathClassLoader);
setField(a4, a4.getClass(), "dexElements", a3);
pathClassLoader.loadClass("xyz.hanks.fix.BugClass");
} catch (Exception e) {
e.printStackTrace();
}
}
}

每次修改BugClass类之后,生产补丁,放到sdcard,重启程序即可成效(不一定重启程序,目的是要在bugclass被第一次加载之前,加载补丁中的bugclass)