1. 概述
前边我们分析并写了阿里的热修复方法,可以知道阿里的热修复是不能增加成员变量、成员方法和资源的,所以基于这个原因,然后我们上节课又通过对类的加载流程的源码做了一个分析,那么我们这节课就来看下我们自己的一个修复的方法,其实很简单,就是钻了一个空子,说白了,就是根据这几个弊端以及类的加载流程然后得出自己的一个热修复的方法,如果没有看的可以先去看下我的这两篇文章:
Android热修复打补丁技术 - (阿里热修复生成补丁包)
类的加载流程源码分析
2. 回顾阿里热修复流程和类的加载流程
2.1>:首先先来回顾下阿里的热修复流程,流程图如下:
分析如下:
1>:把我们之前有bug的版本打一个包,我们叫做 old.apk,然后我们修复该bug,现在是没有bug的,叫做 new.apk;
2>:在客户端去阿里官网下载 AndFix,然后生成一个 fix.apatch差分包,然后把这个差分包放在我们自己的服务器,让用户去下载;
3>:当用户把app只要一打开,检测到有bug的话,就会去服务器下载这个差分包,然后我们就会在 BaseApplication中调用 addPatch()方法去修复bug;
生成差分包的方法:
1.命令是:
apkpatch.bat -f <new> -t <old> -o <output> -k <keystore> -p <*> -a <alias> -e <*>
-f : 没有Bug的新版本apk.
-t : 有bug的旧版本apk
-o : 生成的补丁文件所放的文件夹
-k : 签名打包密钥
-p : 签名打包密钥密码
-a : 签名密钥别名
-e : 签名别名密码(这样一般和密钥密码一致)
我的:apkpatch.bat -f new.apk -t old.apk -o out -k joke.jks -p 123456 -a test -e 123456
然后点击回车,会在out中生成一个 xxx.apatch文件,将 xxx.apatch文件重名为 fix.apatch;
以上就是阿里热修复的一个流程;
2.2>:回顾类的加载流程,分析流程图如下:
分析如下:场景:从MainActivity启动一个 TestActivity
首先先来看下继承关系:
PathClassLoader --> BaseDexClassLoader --> ClassLoader
1>:首先会去找 PathClassLoader,然后会去找BaseDexClassLoader,然后会去找 ClassLoader;
2>:然后调用 ClassLoader中的findClass()方法,但是由于 子类 BaseDexClassLoader覆盖了父类的该方法,所以这里就调用的是 子类BaseDexClassLoader的findClass()方法,调用方法如下;
3>:由上边方法可以看出,其实是调用的 pathList.findClass()方法,而pathList它就是 DexPathList类,可以发现它里边的 findClass()方法如下:
4>:可以看出, DexPathList中的 findClass()方法其实就是 通过for循环遍历 app中所有的 dexElements的数组,只要找到了 class,这里就是说只要找到了 TestActivity的这个class,那么就直接返回 一个 class给 PathClassLoader,然后 通过 (Acitivty)cl.loadClass(className).newInstance()方法,其实就是通过反射去创建对象;
以上就是类的加载流程分析
那么基于上边的分析,下边我们就来看下我们这节课所要讲解的一个我们自己的热修复方法。
3. 自己的热修复方法,流程图如下:
分析上图可知:
我们其实所采用的方式就是:
1>:首先先去修复好这个bug,然后打一个apk包,然后把后缀名改为 .zip并且解压,解压后会有一个 class.dex文件,这里需要手动去把这个文件重命名为 fix.dex,然后把这个 fix.dex文件放到服务器中;
2>:用户手中的apk是有bug的,上图的左边就是我们 app中所有的 dex文件,比如说我们把有bug的类暂且就叫做 bug.class,比如说它就在 所有 dex文件的最前边;
3>:然后我们只需要把这个 已经修复的 fix.class文件插入到 有bug.class的最前边就可以,由 类的加载流程可以知道,在 DexPathList中的findClass()方法中,会for循环遍历class,这里就是需要去找到 TestActivity,只要找到后就直接 return class,就不会去往后边去找,所以也就不会去找 后边有bug的class了,所以就达到了修复的一个目的;
这样的话,只要修复bug了,那么每次就不会再去找后边有bug的类了。
注意:自己手中的app一定是有bug的,而服务器上边的是没有bug的,只要用户一打开app,就会去下载整个 dex包,然后进行插入修复。
4. 修复后的最终效果
下图就是我们采用我们自己的修复方法的修复结果,只要用户第一次修复成功后,那么以后每次进入app之后就都会提示修复成功的,就不会去找之前有bug的class类,这个我们在上边也都是说过的;
5. 具体代码如下
5.1>:修复的代码如下:
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/5 9:49
* Version 1.0
* Params:
* Description: 修复
*/
public class FixDexManager {
private Context mContext ;
private File mDexDir ;
public FixDexManager(Context context) {
this.mContext = context ;
// 获取应用可以访问的 dex目录
this.mDexDir = context.getDir("odex" , Context.MODE_PRIVATE) ;
}
/**
* 修复dex包
* fixDexPath:下载补丁的路径
*/
public void fixDex(String fixDexPath) throws Exception {
// 2. 然后获取下载好的补丁 dexElement
// 2.1 把fixDexPath移动到 系统能够访问的 dex目录下,因为我们最终要把它变为 ClassLoader
File srcFile = new File(fixDexPath) ;
if (!srcFile.exists()){
throw new FileNotFoundException(fixDexPath) ;
}
File destFile = new File(mDexDir , srcFile.getName()) ;
if (destFile.exists()){
Log.d(TAG, "patch [" + fixDexPath + "] has be loaded.");
return;
}
copyFile(srcFile , destFile);
// 2.2 让该ClassLoader读取 fixDex路径
//需要修复的文件
List<File> fixDexFiles = new ArrayList<>() ;
fixDexFiles.add(destFile) ;
fixDexFiles(fixDexFiles) ;
}
/**
* 把合并的数组applicationDexElements 注入到 原来的 applicationClassLoader中即可
*/
private void injectDexElements(ClassLoader classLoader, Object dexElements) throws Exception {
// 1. 先获取 pathList(通过反射)
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList") ;
// private、public、protected都可以反射
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader) ;
// 2. 获取pathList里边的 dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
// 合并
dexElementsField.set(pathList , 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;
}
/**
* copy file
*
* @param src source file
* @param dest target file
* @throws IOException
*/
public static void copyFile(File src, File dest) throws IOException {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
if (!dest.exists()) {
dest.createNewFile();
}
inChannel = new FileInputStream(src).getChannel();
outChannel = new FileOutputStream(dest).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
} finally {
if (inChannel != null) {
inChannel.close();
}
if (outChannel != null) {
outChannel.close();
}
}
}
/**
* 从 ClassLoader中 获取 dexElements
*/
private Object getDexElementsByClassLoader(ClassLoader classLoader) throws Exception {
// 1. 先获取 pathList(通过反射)
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList") ; // pathList是源码中的
// private、public、protected都可以反射
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader) ;
// 2. 获取pathList里边的 dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements"); // dexElements是源码中的
dexElementsField.setAccessible(true);
return dexElementsField.get(pathList);
}
/**
* 加载全部的修复包
*/
public void loadFixDex() throws Exception {
File[] dexFiles = mDexDir.listFiles() ;
List<File> fixDexFiles = new ArrayList<>() ;
for (File dexFile : dexFiles) {
if (dexFile.getName().endsWith(".dex")){
fixDexFiles.add(dexFile) ;
}
}
fixDexFiles(fixDexFiles) ;
}
/**
* 修复 dex
*/
private void fixDexFiles(List<File> fixDexFiles) throws Exception {
// 1. 先获取应用的 已经运行的 dexElements
ClassLoader applicationClassLoader = mContext.getClassLoader();
Object applicationDexElements = getDexElementsByClassLoader(applicationClassLoader) ;
File optimizedDirectory = new File(mDexDir , "odex") ;
if (!optimizedDirectory.exists()){
optimizedDirectory.mkdirs() ;
}
// 开始修复
for (File fixDexFile : fixDexFiles) {
// dexPath:修复dex的路径 --> fixDexFiles
// optimizedDirectory:解压路径
// libraryPath:so文件的路径
// parent:父的ClassLoader
ClassLoader fixDexClassLoader = new BaseDexClassLoader(
fixDexFile.getAbsolutePath() , // 修复的dex的路径 必须要在应用目录下的odex文件中
optimizedDirectory, // 解压路径
null , // .so文件的路径
applicationClassLoader // 父的 ClassLoader
) ;
// 获取app中的 dexElements数组,然后解析来就需要把 下载的没有bug的 补丁的 dexElement插入到这个数组的最前边
Object fixDexElements = getDexElementsByClassLoader(fixDexClassLoader) ;
// 3. 把补丁的 dexElement插到 已经运行的 dexElement的最前面,其实就是合并,修复就ok
// applicationClassLoader 是一个数组,fixDexElements也是一个数组,就是把两个数组合并
// 3.1 合并完成
// 前者是修复的,后者是没有修复的
applicationDexElements = combineArray(fixDexElements , applicationDexElements) ;
}
// 3.2 把合并的数组注入到 原来的 applicationClassLoader中即可
injectDexElements(applicationClassLoader , applicationDexElements) ;
}
}
5.2>:在MainActivity中的initData()方法中直接调用:
public class MainActivity extends BaseSkinActivity {
@ViewById(R.id.btn_test)
Button btn_test ;
@Override
protected void setContentView() {
setContentView(R.layout.activity_main);
}
@Override
protected void initTitle() {
}
@Override
protected void initView() {
btn_test.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(TestActvity.class);
}
});
}
@Override
protected void initData() {
// 用户只要一打开app,就去调用我们自己的修复方式
fixDexBug() ;
}
private void fixDexBug() {
File fixFile = new File(Environment.getExternalStorageDirectory() , "fix.dex") ;
if (fixFile.exists()) {
FixDexManager fixDexManager = new FixDexManager(this);
try {
fixDexManager.fixDex(fixFile.getAbsolutePath()) ;
Toast.makeText(MainActivity.this , "修复成功" , Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(MainActivity.this , "修复失败" , Toast.LENGTH_SHORT).show();
}
}
}
}
5.3>:最后需要在BaseApplication中调用之前所有修复的 dex包;
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/4/1 12:48
* Version 1.0
* Params:
* Description:
*/
public class BaseApplication extends Application {
public static PatchManager mPatchManager ;
@Override
public void onCreate() {
super.onCreate();
// 加载所有修复的 dex包
try {
FixDexManager fixDexManager = new FixDexManager(this) ;
fixDexManager.loadFixDex() ;
} catch (Exception e) {
e.printStackTrace();
}
}
}
6. 开发中的一些细节
1>:可以把出错的 class 重新单独的打成 一个 fix.dex,在这里就指的是 TestActivity,大小也比较小,不过不太可取,除非说代码不混淆;
2>:可以采用分包,可以把不会出错的类打成一个 dex(这里的不要混淆),有错的留在另一个 dex中(这里可以混淆),但是如果方法数没有超过 65536,系统默认不会给你分包,那么你需要去Android Studio官网找分包,而且运行的时候如果 dex过大会影响启动速度;
3>:直接把整个项目打成apk,然后修改后缀名为 zip并且去解压,然后修改里边的class.dex文件名为 fix.dex,然后把fix.dex文件放在服务器,只要用户一打开app,就会去下载整个 dex包,然后进行插入修复,但也会导致一个问题,就是下载的 补丁的 fix.dex大小可能比较大,2M左右;
一般就用 第3种方法
和阿里的相比较,阿里的不能增加成员变量和成员方法,而我们的可以增加成员变量、成员方法、类,但是不能增加资源(腾讯的可以增加资源)
代码已上传至github:
https://github.com/shuai999/EssayJoke_day_04.git