美文网首页
Android - 自己实现android热修复

Android - 自己实现android热修复

作者: 赛德赛高 | 来源:发表于2019-10-09 16:49 被阅读0次

    1.前言

           查阅整合了一下网上的资料,快速实现一个自定义的无需重启的在JAVA层的热修复。

    2.热修复的简单介绍

           所谓热修复就是在一些小bug出现后不需要发布新的安装包,直接发布补丁解决问题。JAVA虚拟机JVM在运行时,加载的是.classes的字节码文件。而Android的虚拟机Dalvik/ART虚拟机加载的是dex文件,他们遵循相同的工作原理,都是通过ClassLoader类加载器。android在ClassLoader的基础上又定义类PathClassLoader和DexClassloader,两者都继承自BaseDexClassLoader。

    • PathClassLoader主要用于加载系统类和应用类。
    • DexClassLoader主要用来加载jar、apk、dex文件。加载jar、apk也是最终抽取里面的Dex文件进行加载

    3.一些前提准备工作

           因为是在JAVA层实现热修复,基本思路就是自己打出dex文件放置于服务器端,app启动后在首页与服务器交互获得dex文件存入手机内,插入下载的dex文件实现修复。

    1>生成dex文件的方法

    1.在project目录下进入build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com下找到class文件。
    2.找到Android SDK目录下的build-tools文件目录。
    3.打开cmd 进入tools目录 运行命令:
    dx --dex --output 需要生成dex的目录和名称 class文件所在的目录

    *** 如果报了does not match path 错误。其实路径目录都对。但是编译器就是通不过。这时候只要在--dex 后面加上--no-strict 就可以了。
    如下:dx --dex --no-strict --output

    2>热修复核心工具类
    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
            doDexInject(context, loadedDex);
        }
    
        /**
         *@author bthvi
         *@time 2018/6/25 0025 15:51
         *@desc 验证是否需要热修复
         */
        public static boolean isGoingToFix(@NonNull Context context) {
            boolean canFix = false;
            File externalStorageDirectory = Environment.getExternalStorageDirectory();
    
            // 遍历所有的修复dex , 因为可能是多个dex修复包
            File fileDir = externalStorageDirectory != null ?
                    new File(externalStorageDirectory,"007"):
                    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("classes") &&
                            (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);
                }
                Toast.makeText(appContext, "修复完成", Toast.LENGTH_SHORT).show();
            } 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;
        }
    }
    

    4.准备完成,开始正式进行热修复。

    1.检查权限,既然涉及文件下载,读写权限必须要给到。方法如下。

    //进入启动页检测权限
     private boolean checkAndRequestPermission() {
            List<String> listPermissionNeeded = new ArrayList<>();
            for (String perm : appPermission) {
                if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) {
                    listPermissionNeeded.add(perm);
                }
            }
            if (!listPermissionNeeded.isEmpty()) {
                ActivityCompat.requestPermissions(this, listPermissionNeeded.toArray(new String[listPermissionNeeded.size()]), PERMISSION_REQUEST_CODE);
                return false;
            }
            return true;
        }
    
    //给予权限
    @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            if (requestCode == PERMISSION_REQUEST_CODE) {
                HashMap<String, Integer> permissionResults = new HashMap<>();
                List<String> permissionNeeded = new ArrayList<>();
                int deniedCount = 0;
    
                for (int i = 0; i < grantResults.length; i++) {
                    if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
                        permissionResults.put(permissions[i], grantResults[i]);
                        permissionNeeded.add(permissions[i]);
                        deniedCount++;
                    }
                }
                if (deniedCount == 0) {
          //在此处下载热修复所需的dex文件
                } else {
                    if (!permissionNeeded.isEmpty()) {
                        ActivityCompat.requestPermissions(this, permissionNeeded.toArray(new String[permissionNeeded.size()]), PERMISSION_REQUEST_CODE);
                    }
                }
            }
    
        }
    
      private static final String savePath = "storage/emulated/0/007/"; //保存到SD卡的路径
    
        public void writeToDisk(final String apkUrl) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        URL url = new URL(apkUrl);
                        HttpURLConnection conn = (HttpURLConnection) url
                                .openConnection();
                        conn.setConnectTimeout(10*1000);
                        conn.setReadTimeout(10*1000);
                        conn.connect();
                        InputStream is = conn.getInputStream();
                        File file = new File(savePath);
                        if (!file.exists()) {
                            file.mkdirs();
                        }
                        String apkFile = savePath + "classes.dex";
                        File ApkFile = new File(apkFile);
                        FileOutputStream fos = new FileOutputStream(ApkFile);
    
                        if (is != null) {
                            byte[] buf = new byte[1024];
                            int ch;
                            while ((ch = is.read(buf)) != -1) {
                                fos.write(buf, 0, ch);//将获取到的流写入文件中
                            }
                        }
                        fos.flush();
                        fos.close();
                        byte buf[] = new byte[64];
                        while (is.read(buf) != -1) {
                            int numread = is.read(buf);
                            fos.write(buf, 0, numread);
                        }
                       // TODO:在此处通知dex下载成功
                        fos.close();
                        is.close();
                    } catch (Exception e) {
                        mHandler.sendEmptyMessage(DOWNLOAD_FAILED);
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    

    待到用户成功下载后准备工作结束,此时可以调用热修复工具类进行热修复。

        private void init() {
            File externalStorageDirectory = Environment.getExternalStorageDirectory();
    
            // 遍历所有的修复dex , 因为可能是多个dex修复包
            File fileDir = externalStorageDirectory != null ?
                    new File(externalStorageDirectory, "007") :
                    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(),mHandler);
            }else {
                Log.i("loge", "do not have dex");
      //失败处理
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        startActivity(new Intent(StartActivity.this, Splash.class));
                        finish();
                    }
                },500);
            }
    
        }
    

    至此大功告成,修复成功。

    ****注意事项

    1.修复内容中如有四大组件需要提前在清单文件注册

    相关文章

      网友评论

          本文标题:Android - 自己实现android热修复

          本文链接:https://www.haomeiwen.com/subject/iedgpctx.html