Android热更新技术(MutiDex)
学习流程:
一、热更新概念
二、源码分析
1.代码总结
2.思考热修复原理
三、原理分析
四、小Demo
一、热更新概念
热更新技术简单点说就是“打补丁”,这样做的好处就是避免了每次出bug,甚至只修改了一行代码就要发布新版本的尴尬,而是通过下载或者推送的方式让用户在不察觉的情况下加载修复好包,这样就实现了热修复。
二、源码分析
重点需要知道的就是,Android的ClassLoader体系,Android中加载类一般使用两个类加载器,PathClassLoader和DexClassLoader,它们都继承自BaseDexClassLoader。
1.PathClassLoader用来加载应用程序的dex,可以理解为加载已经安装的dex文件。
2.DexClassLoader可以加载指定的某个dex文件,可以理解为加载未安装的dex文件。(限制:必须要在应用程序的目录下面,请记住这句话)。
PathClassLoader类源码如下(后面会附上源码地址,请自行扒代码):
image.png
DexClassLoader类源码如下:
image.png
可以看到两个类简单到只有构造方法,但他们有共同的父类BaseDexClassLoader,那么BaseDexClassLoader里有什么好东西,请看圈圈里:
image.png
接着找DexPathList这个类里的findClass方法:
image.png
代码总结:
每个dex文件都是一个Element对象,存放在有序的dexElements数组中,通过类名使用DexFile类的loadClassBinaryName方法查找当前dex中的类,一旦找到马上返回Class实例,找不到就去下一个dex文件找,就是这么简单明了。
思考热修复原理 :
所有我们就可以把修复好的dex文件放在dexElements的最前面,PathClassLoader加载需要的class文件时就会从最开始的补丁文件找,一旦找到直接加载,这样有bug的dex文件就不用再去加载,从而实现了修复补丁的效果。
三、原理分析
直接来张图简单粗暴:
无标题.png
其中蓝色dex是从服务器中下载的修改之后的文件,它和红色dex文件包含有相同名字的目标class文件,比如下面Demo中提到的MyTestClass.class文件,可想而知加载需要的MyTestClass.class时,只用找到蓝色dex中的文件,而不会再去加载红色中的class文件。那么我们只需要拿到原来的dexElements和现有的dexElements然后合并,最后重写给PathList里面的Element[] dexElements。
再啰嗦下具体流程:
1.通过PathClassLoader 来加载我们自身App的dex,其中包含有bug的dex文件
2.通过DexClassLoader来加载我们的补丁dex文件,这是我们修复好的没有bug的dex文件
3.用反射的方式拿到pathList和dexElements
4.合并两个反射到的Element 数组,把我们的补丁dex放在数组的最前面
5.将合并的新的数组,通过Field重新设置到用户App的dexElements数组中,其实就是覆盖原来的数组
6.通过Android build-tools 中的dx命令打包一个修复好bug的dex文件,推送给用户。
四、实例实现
首先要实现的效果是:点击提示按钮吐司10/1的结果,但是bug是我们不小心把1写成了0,点击就会崩掉(为什么会崩掉,不懂的请翻阅小学三年级上册数学课本)。然后我们修复bug后,点击修复按钮,再次点击提示按钮,会顺利的吐司出大大的10!
新建项目MyHotFix_MutiDex,配置gradle和Manifest文件。请按照下面五点自行配置。
image.png
image.png
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.btn);
Button buttonFix = (Button) findViewById(R.id.btn_fix);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
MyTestClass myTestClass = new MyTestClass();
myTestClass.testFix(MainActivity.this);
}
});
buttonFix.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
fixBug();
}
});
}
private void fixBug() {
//目录:/data/data/packageName/odex
File fileDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE);
//往该目录下面放置我们修复好的dex文件。
String name = "classes2.dex";
String filePath = fileDir.getAbsolutePath() + File.separator + name;
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
//搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + name);
os = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
File f = new File(filePath);
if (f.exists()) {
Toast.makeText(this, "dex 重写成功", Toast.LENGTH_SHORT).show();
}
//热修复
FixDexUtils.loadFixedDex(this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
fixBug()方法中我们热修复之前的一系列操作就是把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录下,原因上面已经说过了,DexClassLoader可以加载指定的某个dex文件,必须要在应用程序的目录下面。其中MyTestClass类就是我们的出bug的类,我们看一下是什么,简单几行代码:
public class MyTestClass {
public void testFix(Context context) {
int i = 10;
int a = 0;
Toast.makeText(context, "结果:"+i/a, Toast.LENGTH_SHORT).show();
}
}
可以很明显的看出这样写是错误的!不用管,先让它错着。在FixBug方法里调用了 FixDexUtils.loadFixedDex(this),看一下FieDexUtils类:
public class FixDexUtils {
private static HashSet<File> loadedDex = new HashSet<File>();
static{
loadedDex.clear();
}
public static void loadFixedDex(Context context){
if(context == null){
return ;
}
//遍历所有的修复的dex
File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
File[] listFiles = fileDir.listFiles();
for(File file:listFiles){
if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){
loadedDex.add(file);//存入集合
}
}
//dex合并之前的dex
doDexInject(context,fileDir,loadedDex);
}
private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj,value);
}
private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
File fopt = new File(optimizeDir);
if(!fopt.exists()){
fopt.mkdirs();
}
//1.加载应用程序的dex
try {
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
//2.加载指定的修复的dex文件。
DexClassLoader classLoader = new DexClassLoader(
dex.getAbsolutePath(),//String dexPath,
fopt.getAbsolutePath(),//String optimizedDirectory,
null,//String libraryPath,
pathLoader//ClassLoader parent
);
Object dexObj = getPathList(classLoader);
Object pathObj = getPathList(pathLoader);
Object mDexElementsList = getDexElements(dexObj);
Object pathDexElementsList = getDexElements(pathObj);
//3.合并完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//4.重写给PathList里面的lement[] dexElements;赋值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
private static Object getDexElements(Object obj) throws Exception {
return getField(obj,obj.getClass(),"dexElements");
}
/**
* 两个数组合并
* @param arrayLhs
* @param arrayRhs
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
}
友情提示:别忘了AndroidManifest中加权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
最终我们要做的事情就是:
1.准备两个手机,手机1刷有bug的apk
2.修改bug之后,手机2刷修复好的apk,然后手机2可以扔掉了,我们只要编译好的正确的class文件
3.由于我们只改动了MyTestClass这个类,只需要找到MyTestClass.class文件,打包成dex文件就可以了。简单粗暴在项目的app目录下直接搜索MyTestClass,拿到MyTestClass.class
4.配置dx.bat的环境变量,找到SDK中build-tools的路径,例如我的:D:\SDK\build-tools\26.0.1,我用的是26.0.1中的dx.bat
5.命令行输入dx --dex --output=dex的输出路径 class文件的路径,例如我的:dx --dex --output=C:\Users\lhx\Desktop\dex\classes2.dex C:\Users\lhx\Desktop\dex
第一个路径是dex输出路径,classes2.dex是自己起的文件名;第二个路径是MyTestClass.class文件所在的文件夹,这个路径可以不写完整,会自动到指向的文件下找class文件(注意要包括全路径的文件夹,例如我的全路径C:\Users\lhx\Desktop\dex\com\example\zt\myhotfix_mutidex,也可以有多个class),回车!然后就会在C:\Users\lhx\Desktop\dex路径下生成一个叫做classes2.dex文件,感觉自己好啰嗦~~
image.png
6.这个dex文件就是我们要的补丁文件啦,把它拷出来放到手机1的根目录下,点击我们刷上的错误的app,点击“按钮”,崩掉(因为还没有修复bug)!再次打开app,点击“修复按钮”,然后点击“按钮”,就不会再崩掉,赫然吐司出一个10!掌声在哪里~~这就是我们要的结果,此时bug就算修复成功了!
PathClassLoader,DexClassLoader,BaseDexClassLoader源码链接
特别感谢:动脑学院Ricky老师,讲解的特别详细。