您的位置:首页 > 移动开发 > Android开发

Android热修复+dex分包+相关知识总结

2017-02-06 18:49 555 查看

Android热修复+dex分包+相关知识总结

先推荐一篇文章,是根据包建强在2016GMTC全球移动开发大会上的演讲整理而成,提纲挈领地讲述了Android插件化技术.包括插件化的历史,入门知识,技术流派,周边知识,开源框架,经验分享以及未来几个方面讲解了Android插件化技术,也包括这篇文章的主要内容Android热修复和dex分包.从宏观的角度给予我们一个全面的介绍.

Android插件化:从入门到放弃

一.Android热修复解决的问题

当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。

这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?

热补丁动态修复技术就是来解决这个问题的。这项技术是QQ空间团队最早提出的.

二.技术流派

在业界内比较著名的有阿里巴巴的AndFix、Dexposed,腾讯QQ空间的超级补丁和微信的Tinker。最近阿里百川推出的HotFix热修复服务就基于AndFix技术,定位于线上紧急BUG的即时修复。下面我们就分别介绍QQ空间超级热补丁技术和微信Tinker以及阿里百川的HotFix技术。

1.QQ空间超级补丁技术

超级补丁技术基于DEX分包方案,使用了多DEX加载的原理,大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。

修复的步骤为:

可以看出是通过获取到当前应用的Classloader,即为BaseDexClassloader

通过反射获取到他的DexPathList属性对象pathList

通过反射调用pathList的dexElements方法把patch.dex转化为Element[]

两个Element[]进行合并,把patch.dex放到最前面去

加载Element[],达到修复目的



2.微信Tinker

微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX文件,以达到修复的目的。



整体的流程如下:



3.阿里百川HotFix

阿里百川推出的热修复HotFix服务,相对于QQ空间超级补丁技术和微信Tinker来说,定位于紧急BUG修复的场景下,能够最及时的修复BUG,下拉补丁立即生效无需等待。



1、AndFix实现原理

AndFix不同于QQ空间超级补丁技术和微信Tinker通过增加或替换整个DEX的方案,提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。

原理图如下:



2、AndFix实现过程

对于实现方法的替换,需要在Native层操作,经过三个步骤:



以上部分主要来自其他博客:

Android热修复技术选型——三大流派解析

三.Android热修复技术实现

1.技术选择

本篇文章主要是讲qq空间的补丁方案的实现.主要是因为现在的项目越来越大,dex分包已经是常用的技术了,而且热修复是即时生效的,用户体验也很好.

2.原理分析(重要)

前面已将说过,现在遇到的问题是,已经发布的项目中有bug,我们不想发布新的版本,需要神不知鬼不觉的修改我们之前的代码,用新的代码将旧的代码替换,就解决了问题.

那么如何将旧代码替换呢?

这就考察我们的基础知识了.java文件编译成.class文件,JVM通过解码class文件中的内容来运行.而在android的Dalvik虚拟机(5.0以下)或者ART(5.0及以上)中都运行的是Dalvik字节码,Dalvik字节码是哪来的呢,是通过Android SDK中的dx工具,将.class文件中的java字节码转化为Dalvik字节码,存放在DEX(Dalvik Executable)可执行文件中.

JVM:  .Java -> .class
Dalvik(ART): .Java -> .class -> .dex
Dex文件其实是所有class文件的集合,并做了很多优化.
实际上Dalvik在加载dex文件时,还是用了DexOpt优化了dex文件,生成了odex文件(有兴趣的同学可以自己去查).


dex中的类时通过ClassLoader来加载的,一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

由此我们可以想到,根据ClassLoader的加载方式,如果我们将修改过的dex文件放到有bug的dex文件前面,不就实现了热修复的功能吗.

那我们怎么才能加载dex文件呢?

app启动的时候会找到默认的classes.dex文件并加载,android系统其实支持多个dex文件,在默认的dex文件中找不到所需要的类时,系统会加载其他的dex文件.我们只要将我们的实现的功能放到一个新的dex文件中,在系统没使用到有bug的dex文件的时候将新生成的dex文件加载进ClassLoader就可以实现热修复.


这里就涉及到dex分包技术了.

稍后再说dex分包,我们再把思路走捋一遍:

将我们自己的代码在打包的时候放的第二个dex文件中(后面会讲如何实现)->用户使用,发现有bug->我们修改好代码,重新编译,生成正确的dex文件->用户打开app是检测到有热修复文件->下载,动态加载新的dex文件放在原有的dex文件前面->没有bug了

3.dex分包介绍

dex分包技术最早应该是解决65536的问题.

当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象:

1. 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT

2. 方法数量过多,编译时出错,提示:

Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

出现这种问题的原因是:

1. Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M

无法安装(Android 2.3 INSTALL_FAILED_DEXOPT)问题,是由dexopt的LinearAlloc限制引起的,在Android版本不同分别经历了4M/5M/8M/16M限制,目前主流4.2.x系统上可能都已到16M, 在Gingerbread或者以下系统LinearAllocHdr分配空间只有5M大小的, 高于Gingerbread的系统提升到了8M。Dalvik linearAlloc是一个固定大小的缓冲区。在应用的安装过程中,系统会运行一个名为dexopt的程序为该应用在当前机型中运行做准备。dexopt使用LinearAlloc来存储应用的方法信息。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃。.

2. 一个dex文件最多只支持65536个方法。

超过最大方法数限制的问题,是由于DEX文件格式限制,一个DEX文件中method个数采用使用原生类型short来索引文件中的方法,也就是4个字节共计最多表达65536个method,field/class的个数也均有此限制。对于DEX文件,则是将工程所需全部class文件合并且压缩到一个DEX文件期间,也就是Android打包的DEX过程中, 单个DEX文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为65536。

关于dex分包方案有很多:

1.使用dx命令,google推荐的方法

具体方法可参考: Android关于Dex拆分(MultiDex)技术详解

2.Eclipse中的方法,使用ant脚本

具体方法可参考: Android APK DEX分包总结

3.使用第三方的插件或工具

具体方法可参考: Android Studio中Dex分包方案

4.dex分包的实现

本文使用的上述第三种方法

使用一个第三方的包dexnkife来完成分包的配置:

https://github.com/ceabie/DexKnifePlugin

具体方法上面推荐的文章里面都介绍了,这里就稍微讲几句.

project的build.gradle加入

buildscript {
dependencies {
classpath 'com.ceabie.dextools:gradle-dexknife-plugin:1.5.6'
}
}


在app的build.gradle加入

apply plugin: 'com.ceabie.dexnkife'


在project的新建dexknife.txt文件,在其中添加dex的分包配置。



主要是在dexknife.txt添加这句:

# 这条配置可以指定这个包下类在第二dex中.
# 包名.文件名.**
com.demo.android.mymultidextest.NewPro.**


将某一文件夹下的类都添加到第二个dex文件中

其他默认的类都添加到主dex文件中.

接下来就只需要编译就行了。系统会自动生成maindexlist.txt也就是主包中的class列表。

运行之后生成了2个dex文件.



那么如何查看dex文件的内容呢?

Android APK反编译就这么简单 详解(附图)

常用的反编译工具 Android反编译工具包(最新)

里面有3个工具:

apktool

作用:资源文件获取,可以提取出图片文件和布局文件进行使用查看

dex2jar

作用:将apk反编译成Java源码(classes.dex转化成jar文件)

jd-gui

作用:查看APK中classes.dex转化成出的jar文件,即源码文件

我们用到dex2jar,将两个dex文件转化成jar文件



使用jd-gui,打开dex文件,可查看里面的内容.



如上图, classes2.dex文件中,只有一个文件夹,就是我们之前写在dexknife.txt文件中的

此时,dex分包基本完成.注意:

1.每次发布新版本的时候,将新的文件夹或类文件添加到dexknife.txt文件中,保证该类在第二个dex文件中,如果第一个dex文件中出现bug,是不能通过今天介绍的方法解决的.

5.热修复的实现

此时,情况是这样的:

我们发布的app有bug,我们修改好代码后,生成了新的dex文件,用户打开app时,将新的dex文件下载到手机了,那么此时应该加载新的dex到内存.

Android 中有三个 ClassLoader, 分别为 URLClassLoader 、 PathClassLoader 、 DexClassLoader 。其中

URLClassLoader 只能用于加载jar文件,但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。

PathClassLoader 它只能加载已经安装的apk。因为 PathClassLoader 只会去读取 /data/dalvik-cache 目录下的 dex 文件。例如我们安装一个包名为 com.hujiang.xxx 的 apk,那么当 apk 安装过程中,就会在 /data/dalvik-cache 目录下生产一个名为 data@app@com.hujiang.xxx-1.apk@classes.dex 的 ODEX 文件。在使用 PathClassLoader 加载 apk 时,它就会去这个文件夹中找相应的 ODEX 文件,如果 apk 没有安装,自然会报 ClassNotFoundException 。

DexClassLoader 是最理想的加载器。它的构造函数包含四个参数,分别为:

1.dexPath,指目标类所在的APK或jar文件的路径.类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径.如果要包含多个路径,路径之间必须使用特定的分割符分隔,特定的分割符可以使用System.getProperty(“path.separtor”)获得.

2.dexOutputDir,由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径.在Android系统中,一个应用程序一般对应一个Linux用户id,应用程序仅对属于自己的数据目录路径有写的权限,因此,该参数可以使用该程序的数据路径.

3.libPath,指目标类中所使用的C/C++库存放的路径

4.classload,是指该装载器的父装载器,一般为当前执行类的装载器

以上一段摘自:Android 热修复,没你想的那么难

首先将新的dex文件下载到手机,我这里放到了根目录下

在项目中创建Application文件,在onCreate()方法中调用以下代码

为啥必须在application中呢? 因为在application中,先拿到加载程序本身的PathClassloader,,我们先称它为originalClassloader,因为在你的originalClassloader里,已经保存有对主程序中dex文件的链接,所以,能找到你的主程序的class。


获得dex文件路径

String libPath= Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes2.dex";


获取PathClassLoader

PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();


创建DexClassLoader

DexClassLoader dexClassLoader = new DexClassLoader(libPath, getDir("dex", 0).getAbsolutePath(), libPath, getClassLoader());


需要把我们的dex加载到系统中,这里主要就是使用反射技术合并dexList即可

try{
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}


public 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;
}


public Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}


public Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}


public Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}


public static void setField(Object obj, Class<?> cl, String field, Object value)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {

Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}


以上方法执行完毕后就将dex文件加载完毕.

四.总结

回顾一下,本问介绍的热修复方法.

当我们新发布的app出现bug需要紧急处理时,我们修改好代码,编译得到新的dex文件,用户下载后需要重新启动app,在Application中回去加载下载的dex文件,达到修复的目的.

这种方法的用户体验还是稍微差一点,不过我的主要目的是介绍其相关的知识,一些android系统的运行过程,希望对大家都有所帮助.

如有错误,欢迎指出!

五.相关文章

Android 热修复,没你想的那么难

安卓App热补丁动态修复技术介绍

Android热修复技术选型——三大流派解析

Android 热补丁动态修复框架小结

Android 热修复其实很简单

Android 热修复,插件式开发—基本知识

Android dex分包方案

Android Dex 分包指南

【转】Android插件化从入门到放弃-最强合集
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息