1 热修复概念
1.1 什么是热修复
一般的bug修复:都是等下一个版本解决,然后发布新的apk。
热修复:可以直接在客户已经安装的程序当中修复bug。
1.2 热修复原理图
2 热修复框架
(1)阿里系:DeXposed、Andfix。从底层C的二进制来入手的。
(2)腾讯系:Tinker、QQ空间补丁技术。Java类加载机制来入手的。
(3)各大平台对比
3 热修复例子
3.1 热修复步骤
(0)打包前注意:
Instant run:做热修复的时候记得把Instant run功能关闭,不然会影响热修复实现。
(1)找到~Class.class
a.修复代码,并编译运行
public class MyTestClass {
public void testFix(Context context){
int i = 10;
int a = 1;
Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
}
}
b.在AS的项目下找到MyTestClass.class文件
\app\build\intermediates\bin\MyTestClass.class
(2)配置dx.bat的环境变量
win:Android\sdk\build-tools\23.0.3\dx.bat
mac:
a.Mac系统终端命令行不执行命令,总出现command not found解决方法。打开命令行执行如下命令。
export PATH=/usr/bin:/usr/sbin:/bin:/sbin:/usr/X11R6/bin
cd ~/
touch .bash_profile //创建bash_profile,可忽略
open .bash_profile //打开并编辑bash_profile
b.配置过的path
export PATH=${PATH}:~/Library/Android/sdk/platform-tools //adb....
export PATH=${PATH}:~/Library/Android/sdk/build-tools/23.0.3 //dx....
参考配置链接:Mac系统终端命令行不执行命令 总出现command not found解决方法
(3)执行命令
win:dx --dex --output=D:\Users\ricky\Desktop\dex\classes2.dex D:\Users\ricky\Desktop\dex
mac:dx --dex --output=/Users/chenliguan/Desktop/dex/classes2.dex /Users/chenliguan/Desktop/dex
命令解释:
–output=D:\Users\ricky\Desktop\dex\classes2.dex:指定输出路径
D:\Users\ricky\Desktop\dex :最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)
3.2 例子源码
(1)FixDexUtils.java
package com.dn.fixutils;
import android.content.Context;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashSet;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
/**
* 热修复工具类
*/
public class FixDexUtils {
private static HashSet<File> loadedDex = new HashSet<File>();
static{
loadedDex.clear();
}
/**
* 加载修复的Dex
*/
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
);
//3.合并
Object dexObj = getPathList(classLoader);
Object pathObj = getPathList(pathLoader);
Object mDexElementsList = getDexElements(dexObj);
Object pathDexElementsList = getDexElements(pathObj);
//合并完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重写给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;
}
// [9876] [12345]
// [9876 12345]
}
(2)activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.dn.main.fix.com.dex.main.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<Button
android:id="@+id/btn_test"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:onClick="test"
android:text="test" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/btn_test"
android:onClick="fix"
android:text="fix" />
</RelativeLayout>
(3)MainActivity.java
package com.dex.main;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
public class MainActivity extends Activity {
private static final int WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void test(View v) {
MyTestClass myTestClass = new MyTestClass();
myTestClass.testFix(this);
}
public void fix(View v) {
/**
* 1.检查权限
*/
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
/**
* 2.申请授权
* 第二个参数是需要申请的权限的字符串数组
* 第三个参数为requestCode,主要用于回调的时候检测
*/
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
WRITE_EXTERNAL_STORAGE_REQUEST_CODE);
} else {
fixBug();
}
}
/**
* 处理权限申请回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case WRITE_EXTERNAL_STORAGE_REQUEST_CODE:
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
fixBug();//调用方法
} else {
Log.e("权限出错:", "权限出错");
}
return;
default:
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
/**
* 修复bug
*/
private void fixBug() {
//目录:/data/data/packageName/odex
File fileDir = getDir("odex", 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 {
String sourceFile = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + name;
is = new FileInputStream(sourceFile);
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();
}
//加载修复的Dex
FixDexUtils.loadFixedDex(this);
} catch (Exception e) {
Log.e("e:", e.toString());
e.printStackTrace();
}
}
}
(4)MyTestClass.java
public class MyTestClass {
public void testFix(Context context){
int i = 10;
int a = 0;
Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
}
}
4 热修复原理
4.1 Android的ClassLoader体系
/**
* 默认加载器,只能加载/data/app中的apk文件。
*
*/
public class PathClassLoader extends BaseDexClassLoader {
}
/**
* 加载任何路径的apk/dex/jar
*/
public class DexClassLoader extends BaseDexClassLoader {
}
4.2 DexClassLoader动态加载的实现
第一步:创建DexClassLoader对象,加载对应的apk/dex/jar文件。
(1)调用方法使用
//加载应用程序的dex
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
//加载指定的修复的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);
//合并完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重写给PathList里面的lement[] dexElements;赋值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
(2)DexClassLoader构造函数
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
(3)BaseDexClassLoader构造函数
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;//DexPathList
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);//DexPathList构造函数
}
}
(4)Element
static class Element {
static class Element {
private final File file; // 它对应的就是需要加载的apk/dex/jar文件
private final boolean isDirectory; // 第一个参数file是否为一个目录,一般为false,因为我们传入的是要加载的文件
private final File zip; // 如果加载的是一个apk或者jar或者zip文件,该对象对应的就是该apk或者jar或者zip文件
private final DexFile dexFile; // 它是得到的dex文件
......
}
......
}
(5)DexPathList
final class DexPathList {
private Element[] dexElements;
/**
* definingContext对应的就是当前classLoader
* dexPath对应的就是上面传进来的apk/dex/jar的路径
* libraryPath就是上面传进来的加载的时候需要用到的lib库的目录,这个一般不用
* optimizedDirectory就是上面传进来的dex的输出路径
*/
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);//调用makeDexElements()方法
}
}
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions,
ClassLoader loader) {
return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
}
/**
* files是一个ArrayList<File>列表,它对应的就是apk/dex/jar文件,因为我们可以指定多个文件。
* optimizedDirectory是前面传入dex的输出路径
* suppressedExceptions为一个异常列表
*/
private static Element[] makeElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions,boolean ignoreDexFiles,ClassLoader loader) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
// We support directories for looking up resources and native libraries.
// Looking up resources in directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file, true, null, null);
} else if (file.isFile()) {
if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);//调用loadDexFile()方法
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
zip = file;
if (!ignoreDexFiles) {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);//调用loadDexFile()方法
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements[elementsPos++] = new Element(dir, false, zip, dex);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
// file为需要加载的apk/dex/jar文件
// optimizedDirectorydex的输出dex文件路径
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);//调用了DexFile.loadDex()方法
}
}
//如果我们没有指定dex输出目录的话,就直接创建一个DexFile对象,
//如果我们指定了dex输出目录,我们就需要构造dex输出路径。
(6)DexFile.loadDex
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
(7)总结
a.在DexClassLoader我们指定了加载的apk/dex/jar文件和dex输出路径optimizedDirectory,它最终会被解析得到DexFile文件。
b.将DexFile文件对象放在Element对象里面,它对应的就是Element对象的dexFile成员变量。
c.将这个Element对象放在一个Element[]数组中,然后将这个数组返回给DexPathList的dexElements成员变量。
d.DexPathList是BaseDexClassLoader的一个成员变量。
第二步:调用dexClassLoader的loadClass,得到加载的dex里面的指定的Class.
clazz = dexClassLoader.loadClass("com.example.apkplugin.PluginTest");
(1)ClassLoader.loadClass
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
(2)BaseDexClassLoader.findClass
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
(3)DexPathList.findClass
/**
* 就是遍历dexElements数组,从每个Element对象中拿到DexFile类型的dex文件,
* 然后就是从dex去加载所需要的class文件,直到找到为止。
*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
(4)总结
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
5 MultiDex分包
Android进阶之使用multidex(产生多个dex)解决Dex超出方法数65535的限制