服务器中的热修复怎么做,热修复改进版 - 自己的热修复方法

1. 概述

前边我们分析并写了阿里的热修复方法,可以知道阿里的热修复是不能增加成员变量、成员方法和资源的,所以基于这个原因,然后我们上节课又通过对类的加载流程的源码做了一个分析,那么我们这节课就来看下我们自己的一个修复的方法,其实很简单,就是钻了一个空子,说白了,就是根据这几个弊端以及类的加载流程然后得出自己的一个热修复的方法,如果没有看的可以先去看下我的这两篇文章:

2. 回顾阿里热修复流程和类的加载流程

2.1>:首先先来回顾下阿里的热修复流程,流程图如下:

3cc104016e95?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

阿里打补丁流程.png

分析如下:

1>:把我们之前有bug的版本打一个包,我们叫做 old.apk,然后我们修复该bug,现在是没有bug的,叫做 new.apk;

2>:在客户端去阿里官网下载 AndFix,然后生成一个 fix.apatch差分包,然后把这个差分包放在我们自己的服务器,让用户去下载;

3>:当用户把app只要一打开,检测到有bug的话,就会去服务器下载这个差分包,然后我们就会在 BaseApplication中调用 addPatch()方法去修复bug;

生成差分包的方法:

1.命令是:

apkpatch.bat -f -t -o -k -p -a -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>:回顾类的加载流程,分析流程图如下:

3cc104016e95?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

类的加载机制.png

分析如下:场景:从MainActivity启动一个 TestActivity

首先先来看下继承关系:

PathClassLoader --> BaseDexClassLoader --> ClassLoader

1>:首先会去找 PathClassLoader,然后会去找BaseDexClassLoader,然后会去找 ClassLoader;

2>:然后调用 ClassLoader中的findClass()方法,但是由于 子类 BaseDexClassLoader覆盖了父类的该方法,所以这里就调用的是 子类BaseDexClassLoader的findClass()方法,调用方法如下;

3cc104016e95?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

图片.png

3>:由上边方法可以看出,其实是调用的 pathList.findClass()方法,而pathList它就是 DexPathList类,可以发现它里边的 findClass()方法如下:

3cc104016e95?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

图片.png

4>:可以看出, DexPathList中的 findClass()方法其实就是 通过for循环遍历 app中所有的 dexElements的数组,只要找到了 class,这里就是说只要找到了 TestActivity的这个class,那么就直接返回 一个 class给 PathClassLoader,然后 通过 (Acitivty)cl.loadClass(className).newInstance()方法,其实就是通过反射去创建对象;

以上就是类的加载流程分析

那么基于上边的分析,下边我们就来看下我们这节课所要讲解的一个我们自己的热修复方法。

3. 自己的热修复方法,流程图如下:

3cc104016e95?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

热修复原理.png

分析上图可知:

我们其实所采用的方式就是:

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类,这个我们在上边也都是说过的;

3cc104016e95?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

图片.png

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 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 fixDexFiles = new ArrayList<>() ;

for (File dexFile : dexFiles) {

if (dexFile.getName().endsWith(".dex")){

fixDexFiles.add(dexFile) ;

}

}

fixDexFiles(fixDexFiles) ;

}

/**

* 修复 dex

*/

private void fixDexFiles(List 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种方法

和阿里的相比较,阿里的不能增加成员变量和成员方法,而我们的可以增加成员变量、成员方法、类,但是不能增加资源(腾讯的可以增加资源)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
  1. 首先添加依赖     compile 'com.alipay.euler:andfix:0.3.1@aar'   然后在Application.onCreate()添加以下代码:     patchManager = new PatchManager(context); //初始化补丁包管理器     patchManager.init(appversion); //版本 String appversion= getPackageManager().getPackageInfo(getPackageName(), 0).versionName;     patchManager.loadPatch(); //加载所有补丁   注意每次appversion变更都会导致所有补丁被删除,如果appversion没有改变,则会加载已经保存的所有补丁。   然后在需要的地方调用PatchManager.addPatch()方法加载新补丁,比如可以在下载补丁文件之后调用。   如果你使用混淆,你要保存mapping.txt,这样的话你在新版本的构建是就可以借助 "-apply mapping” 来使用它了。   混淆需要加入:     -keep class * extends java.lang.annotation.Annotation     -keepclasseswithmembernames class * {       native <methods>;     }     -keep class com.alipay.euler.andfix.** { *; }   2. 打补丁包,首先生成一个apk文件,然后更改代码,在修复bug后生成另一个apk。     通过AndFix官方提供的补丁包创建工具apkpatch生成一个.apatch格式的补丁文件,需要提供原apk,修复后的apk,以及一个签名文件。     可以直接使用命令apkpatch查看具体的使用方法。     使用示例:apkpatch -o D:/Patch/ -k debug.keystore -p android -a androiddebugkey -e android f bug-fix.apk t release.apk     usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>     -a,--alias <alias> keystore entry alias.     -e,--epassword <***> keystore entry password.     -f,--from <loc> new Apk file path.     -k,--keystore <loc> keystore path.     -n,--name <name> patch name.     -o,--out <dir> output dir.     -p,--kpassword <***> keystore password.     -t,--to <loc> old Apk file path.   有时候你的团队成员可能会fix各自的bug,这个时候就会有不止一个补丁文件.apatch。这种情况下,可以用这个工具merge这些.apatch文件:     usage: apkpatch -m <apatch_path...> -o <output> -k <keystore> -p <***> -a <alias> -e <***>     -a,--alias <alias> keystore entry alias.     -e,--epassword <***> keystore entry password.     -k,--keystore <loc> keystore path.     -m,--merge <loc...> path of .apatch files.     -n,--name <name> patch name.     -o,--out <dir> output dir.     -p,--kpassword <***> keystore password.   3. 通过网络传输或者adb push的方式将apatch文件传到手机上;   4. 然后运行到patchManager.addPatch(path)的时候就会加载补丁。//path:补丁文件下载到
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值