Android实现APP热修复

做安卓开发,尽管每个应用上线之前都是经过反复测试验证的,但是还是不免有漏网之bug。出现这种情况我们可以再出一个更新包,让用户下载升级,这是一种处理方法,但是这样的流量成本多了,而且用户体验相当不好,人家明明刚升级,结果没过一会儿又要升级了?所以热修复方法来了

  所谓热修复,就是在我们应用上线后出现小bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户不知不觉之间修复掉bug。

热修复工具,主要分为两大流派:

  • 1.以阿里为代表的Native层替换方法表中的方法实现热修复[AndFix ,Sophix等]

  • 2.以腾讯美团为代表的在JAVA层实现热修复[Tinker,Robust等]。

  前者不需要重启APP,直接在虚拟机的方法区实现方法替换;后者要实现热修复必须要重启APP。下面记录一种Java层的Android热修复方法(也就是需要重启APP才能实现热修复):

主要的热修复工具类

package com.york.rexiufu.rexiufu;

import android.content.Context;
import android.os.Environment;
import android.support.annotation.NonNull;
import com.york.rexiufu.utils.LogUtil;

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 FixDexUtil {

    private static final String DEX_SUFFIX = ".dex";
    private static final String APK_SUFFIX = ".apk";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    public static final String DEX_DIR = "odex";
    private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
    private static HashSet<File> loadedDex = new HashSet<>();

    static {
        loadedDex.clear();
    }

    /**
     * 加载补丁,使用默认目录:data/data/包名/files/odex
     *
     * @param context
     */
    public static void loadFixedDex(Context context) {
        loadFixedDex(context, null);
    }

    /**
     * 加载补丁
     *
     * @param context       上下文
     * @param patchFilesDir 补丁所在目录
     */
    public static void loadFixedDex(Context context, File patchFilesDir) {
        // dex合并之前的dex
        LogUtil.e("开始修复。。。。。。");
        doDexInject(context, loadedDex);
    }

    /**
     *验证是否需要热修复
     */
    public static boolean isGoingToFix(@NonNull Context context) {
        boolean canFix = false;
        File externalStorageDirectory = Environment.getExternalStorageDirectory();

        // 遍历所有的修复dex , 因为可能是多个dex修复包
        File fileDir = externalStorageDirectory != null ?
                new File(externalStorageDirectory, "YorkFix") :
                new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(这个可以任意位置)

        File[] listFiles = fileDir.listFiles();
        if (listFiles != null) {
            for (File file : listFiles) {
                if (file.getName().startsWith("my") &&
                        (file.getName().endsWith(DEX_SUFFIX)
                                || file.getName().endsWith(APK_SUFFIX)
                                || file.getName().endsWith(JAR_SUFFIX)
                                || file.getName().endsWith(ZIP_SUFFIX))) {

                    loadedDex.add(file);// 存入集合
                    //有目标dex文件, 需要修复
                    canFix = true;
                }
            }
        }
        return canFix;
    }

    private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
        String optimizeDir = appContext.getFilesDir().getAbsolutePath() +
                File.separator + OPTIMIZE_DEX_DIR;
        // data/data/包名/files/optimize_dex(这个必须是自己程序下的目录)

        File fopt = new File(optimizeDir);
        if (!fopt.exists()) {
            fopt.mkdirs();
        }
        try {
            // 1.加载应用程序dex的Loader
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            for (File dex : loadedDex) {
                // 2.加载指定的修复的dex文件的Loader
                DexClassLoader dexLoader = new DexClassLoader(
                        dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
                        fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
                        null,// 加载dex时需要的库
                        pathLoader// 父类加载器
                );
                // 3.开始合并
                // 合并的目标是Element[],重新赋值它的值即可

                /**
                 * BaseDexClassLoader中有 变量: DexPathList pathList
                 * DexPathList中有 变量 Element[] dexElements
                 * 依次反射即可
                 */

                //3.1 准备好pathList的引用
                Object dexPathList = getPathList(dexLoader);
                Object pathPathList = getPathList(pathLoader);
                //3.2 从pathList中反射出element集合
                Object leftDexElements = getDexElements(dexPathList);
                Object rightDexElements = getDexElements(pathPathList);
                //3.3 合并两个dex数组
                Object dexElements = combineArray(leftDexElements, rightDexElements);

                // 重写给PathList里面的Element[] dexElements;赋值
                Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
                setField(pathList, pathList.getClass(), "dexElements", dexElements);
            }
            LogUtil.e("修复完成");//此处可以把所有的dex文件都删除掉
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 反射给对象中的属性重新赋值
     */
    private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cl.getDeclaredField(field);
        declaredField.setAccessible(true);
        declaredField.set(obj, value);
    }

    /**
     * 反射得到对象中的属性值
     */
    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }


    /**
     * 反射得到类加载器中的pathList对象
     */
    private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 反射得到pathList中的dexElements
     */
    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        return getField(pathList, pathList.getClass(), "dexElements");
    }

    /**
     * 数组合并
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> clazz = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组)
        int j = Array.getLength(arrayRhs);// 得到原dex数组长度
        int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
        Object result = Array.newInstance(clazz, k);// 创建一个类型为clazz,长度为k的新数组
        System.arraycopy(arrayLhs, 0, result, 0, i);
        System.arraycopy(arrayRhs, 0, result, i, j);
        return result;
    }
}


YorkFix是手机上我们存放.dex文件的目录。

使用方法

1.先创建一个测试类,写入测试代码

package com.york.rexiufu.rexiufu;

import android.content.Context;
import android.widget.Toast;

public class BugClass {
    public BugClass(Context context){
        Toast.makeText(context,"假装这里有bug!",Toast.LENGTH_SHORT).show();
    }
}

2.在MianActivity进行测试

package com.york.rexiufu.activitys;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

import com.york.rexiufu.R;
import com.york.rexiufu.rexiufu.BugClass;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

    public void check(View view) {
        new BugClass(MainActivity.this);
    }
}

3.运行APP

4.开始修复,修改测试类BugClass

package com.york.rexiufu.rexiufu;

import android.content.Context;
import android.widget.Toast;

public class BugClass {
    public BugClass(Context context){
        Toast.makeText(context,"bug 已经修复了!",Toast.LENGTH_SHORT).show();
    }
}

5.新增一个 WelcomeActivity,在WelcomeActivity中进行热修复

package com.york.rexiufu.activitys;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.widget.Toast;

import com.york.rexiufu.R;
import com.york.rexiufu.rexiufu.FixDexUtil;
import com.york.rexiufu.utils.PermissionUtil;

import java.io.File;

public class WelcomeActivity extends Activity {
    private String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE};
    private PermissionUtil.IPermissionsResult mIPermissionsResult = new PermissionUtil.IPermissionsResult() {
        @Override
        public void passPermissons() {

        }

        @Override
        public void forbitPermissons() {

        }
    };
    @Override
    public void onRequestPermissionsResult(int requestCode,  String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PermissionUtil.getInstance().onRequestPermissionsResult(this, requestCode, permissions, grantResults);
    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        PermissionUtil.getInstance().chekPermissions(this, permissions, mIPermissionsResult);
        init();
    }

    private void init() {
        File externalStorageDirectory = Environment.getExternalStorageDirectory();
        // 遍历所有的修复dex , 因为可能是多个dex修复包
        File fileDir = externalStorageDirectory != null ? new File(externalStorageDirectory,"YorkFix"): new File(getFilesDir(), FixDexUtil.DEX_DIR);// data/user/0/包名/files/odex(这个可以任意位置)
        if (!fileDir.exists()){
            fileDir.mkdirs();
        }
        if (FixDexUtil.isGoingToFix(this)) {
            FixDexUtil.loadFixedDex(this, Environment.getExternalStorageDirectory());
        }
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                startActivity(new Intent(WelcomeActivity.this, MainActivity.class));
                finish();
            }
        },3000);

    }
}

6.在AS中找到生成的 BugClass.class 。拷贝它的目录

D:\york_android\RexiufuDemo\app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes
\com\york\rexiufu\rexiufu\BugClass.class

这里可以把从 com 整个包拷贝出来。

7.使用命令,用SDK中 build tool 里的dx工具,生成 .dex 后缀的修复包。

命令如下:

dx --dex --output=[要生成的.dex文件名称].dex [刚才拷贝的修复bug的类及包名的目录]

演示:

dx --dex --output=my.dex com\york\rexiufu\rexiufu\BugClass.class

这样就会的到一个 my.dex 文件

8.拷贝.dex文件到手机目录

9.重新启动APP

这时就可以看到原来的类被替换修复了,修复成功!

补充: 在实际操作中,我们需要用网络把我们的补丁文件.dex下载到用户手机的指定文件夹中,用户在不知不觉中APP就完成了对bug的修复。所以关键我们还是要早发现bug,解决bug,这样才能给到用户有更好的体验。




参考链接:Android学习——手把手教你实现Android热修复

发布了14 篇原创文章 · 获赞 0 · 访问量 110

猜你喜欢

转载自blog.csdn.net/york2017/article/details/105669147