一步步手动实现热修复(一)-dex文件的生成与加载

简介: *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 热修复技术自从QQ空间团队搞出来之后便渐渐趋于成熟。我们这个系列主要介绍如何一步步手动实现基本的热修复功能,无需使用第三方框架。

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

热修复技术自从QQ空间团队搞出来之后便渐渐趋于成熟。

我们这个系列主要介绍如何一步步手动实现基本的热修复功能,无需使用第三方框架。

在开始学习之前,需要对基本的热修复技术有些了解,以下文章可以帮助到你:


本节课程主要分为3块:

本节示例所用到的任何资源都已开源,项目中包含工程中所用到代码、示例图片、说明文档。项目地址为:
https://code.csdn.net/u011064099/sahadevhotfix/tree/master

dex文件的生成与加载

我们在这部分主要做的流程有:

  • 1.编写基本的Java文件并编译为.class文件。
  • 2.将.class文件转为.dex文件。
  • 3.将转好的dex文件放入创建好的Android工程内并在启动时将其写入本地。
  • 4.加载解压后的.dex文件中的类,并调用其方法进行测试。

Note: 在阅读本节之前最好先了解一下类加载器的双亲委派原则、DexClassLoader的使用以及反射的知识点。

编写基本的Java文件并编译为.class文件

首先我们在一个工程目录下开始创建并编写我们的Java文件,你可能会选择各种IDE来做这件事,但我在这里劝你不要这么做,因为有坑在等你。等把基本流程搞清楚可以再选择更进阶的方法。这里我们可以选择文本编辑器比如EditPlus来对Java文件进行编辑。

新建一个Java文件,并命名为:ClassStudent.java,并在java文件内键入以下代码:

public class ClassStudent {
    private String name;

    public ClassStudent() {

    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName(){
        return this.name + ".Mr";   
    }
}

Note: 这里要注意,不要对类添加包名,因为在后期对class文件处理时会遇到问题,具体问题会稍后说明。上面的getName方法在返回时对this.name属性添加了一段字符串,这里请注意,后面会用到。

在文件创建好之后,对Java文件进行编译:
这里写图片描述

将.class文件转为.dex文件

好,现在我们使用class文件生成对应的dex文件。生成dex文件所需要的工具为dx,dx工具位于sdk的build-tools文件夹内,如下图所示:
这里写图片描述

Tips: 为了方便使用,建议将dx的路径添加到环境变量中。如果对dx工具不熟悉的,可以在终端中输入dx –help以获取帮助。

dx工具的基本用法是:

dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]

Tips: 刚开始自己摸索的时候,就没有仔细看命令,导致后面两个参数的顺序颠倒了,搞出了一些让人疑惑难解的问题,最后又不得不去找dx工具的源码调试,最后才发现自己的问题在哪。如果有对dx工具感兴趣的,可以对dx的包进行反编译或者获取dx的相关源代码进行了解。dx.lib文件位于dx.bat的下级目录lib文件夹中,可以使用JD-GUI工具对其进行查看或导出。如果需要获取源代码的,请使用以下命令进行克隆:

git clone https://android.googlesource.com/platform/dalvik

我们使用以下命令生成dex文件:

dx --dex --output=user.dex ClassStudent.class

这里我为了防止出错,提前在当前目录下新建好了user.dex文件。上述命令依赖编译.class文件的JDK版本,如果使用的是JDK8编译的class会提示以下问题:

PARSE ERROR:
unsupported class file version 52.0
...while parsing ClassStudent.class
1 error; aborting

这里的52.0意味着class文件不被支持,需要使用JDK8以下的版本进行编译,但是dx所需的环境还是需要为JDK8的,这里我编译class文件使用的是JDK7,请注意。

上面我们提到了为什么先不要在ClassStudent中使用包名,因为在执行dx的时候会报以下异常,这是因为以下第二项条件没有通过,该代码位于com.android.dx.cf.direct.DirectClassFile文件内:

    String thisClassName = thisClass.getClassType().getClassName();
    if(!(filePath.endsWith(".class") && filePath.startsWith(thisClassName) && (filePath.length()==(thisClassName.length()+6)))){
        throw new ParseException("class name (" + thisClassName + ") does not match path (" + filePath + ")");
    }

运行截图如下所示:
这里写图片描述

好了,到此为止我们的目录应该如下:
这里写图片描述

写入dex到本地磁盘

接下来将生成好的user.dex文件放入Android工程的res\raw文件夹下:
这里写图片描述

在系统启动时将其写入到磁盘,这里不再贴出具体的写入代码,项目的MainActivity中包含了此部分代码。

加载dex中的类并测试

在写入完毕之后使用DexClassLoader对其进行加载。DexClassLoader的构造方法需要4个参数,这里对这4个参数进行简要说明:

  • String dexPath:dex文件的绝对路径。在这里我将其放入了应用的cache文件夹下。
  • String optimizedDirectory:优化后的dex文件存放路径。DexClassLoader在构造完毕之后会对原有的dex文件优化并生成一个新的dex文件,在这里我选择的是…/cache/optimizedDirectory/目录。此外,API文档对该目录有严格的说明:Do not cache optimized classes on external storage.出于安全考虑,请不要将优化后的dex文件放入外部存储器中。
  • String libraryPath:dex文件所需要的库文件路径。这里没有依赖,使用空字符串代替。
  • ClassLoader parent:双亲委派原则中提到的父类加载器。这里我们使用默认的加载器,通过getClassLoader()方法获得。

在解释完毕DexClassLoader的构造参数之后,我们开始对刚刚的dex文件进行加载:

DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", classLoader);

接来下开始load我们刚刚写入在dex文件中的ClassStudent类:

Class<?> aClass = dexClassLoader.loadClass("ClassStudent");

然后我们对其进行初始化,并调用相关的get/set方法对其进行验证,在这里我传给ClassStudent对象一个字符串,然后调用它的get方法获取在方法内合并后的字符串:

    Object instance = aClass.newInstance();
    Method method = aClass.getMethod("setName", String.class);
    method.invoke(instance, "Sahadev");

    Method getNameMethod = aClass.getMethod("getName");
    Object invoke = getNameMethod.invoke(instance););

最后我们实现的代码可能是这样的:

    /**
     * 加载指定路径的dex
     *
     * @param apkPath
     */
    private void loadClass(String apkPath) {
        ClassLoader classLoader = getClassLoader();

        File file = new File(apkPath);

        try {

            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", classLoader);
            Class<?> aClass = dexClassLoader.loadClass("ClassStudent");
            mLog.i(TAG, "ClassStudent = " + aClass);

            Object instance = aClass.newInstance();
            Method method = aClass.getMethod("setName", String.class);
            method.invoke(instance, "Sahadev");

            Method getNameMethod = aClass.getMethod("getName");
            Object invoke = getNameMethod.invoke(instance);

            mLog.i(TAG, "invoke result = " + invoke);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

最后附上我们的运行截图:
这里写图片描述

如果在实现过程中遇到问题的,请在下方留言。

目录
相关文章
|
Java Android开发
autojs非常见函数2
autojs非常见函数2
1220 0
|
Android开发
Autox.js 脚本开发环境搭建,从案例到打包apk(详细流程)
Autox.js 脚本开发环境搭建,从案例到打包apk(详细流程)
3609 0
|
Java
Mac 下安装jdk1.7(国内镜像)
Mac 下安装jdk1.7(国内镜像)
2793 0
|
自然语言处理 Java Go
Fury:一个基于JIT动态编译的高性能多语言原生序列化框架
Fury是一个基于JIT动态编译的多语言原生序列化框架,支持Java/Python/Golang/C++等语言,提供全自动的对象多语言/跨语言序列化能力,以及相比于别的框架最高20~200倍的性能。
Fury:一个基于JIT动态编译的高性能多语言原生序列化框架
|
监控 安全 JavaScript
浅谈移动端设备标识码:DeviceID、IMEI、IDFA、UDID和UUID
场景 : 客户提出一个问题就是把用户的登录记录和设备绑定到一起,就是每个人都是固定的设备(可能是安全因素吧)。一开始想的是回去设备的IMEI号和用户账号绑定起来,结果发现IMEI不对外开发,只能另寻他法,最后通过获取设备序列号作为唯一标识。
浅谈移动端设备标识码:DeviceID、IMEI、IDFA、UDID和UUID
|
11月前
|
存储 安全 物联网
浅析Kismet:无线网络监测与分析工具
Kismet是一款开源的无线网络监测和入侵检测系统(IDS),支持Wi-Fi、Bluetooth、ZigBee等协议,具备被动监听、实时数据分析、地理定位等功能。广泛应用于安全审计、网络优化和频谱管理。本文介绍其安装配置、基本操作及高级应用技巧,帮助用户掌握这一强大的无线网络安全工具。
868 9
浅析Kismet:无线网络监测与分析工具
Class.forName()方法总结
Class.forName()方法总结
|
存储 运维 NoSQL
分布式表的写入流程
分布式表的写入流程
167 2
|
存储 Java 开发者
丸辣!BigDecimal又踩坑了
【10月更文挑战第13天】本文介绍了在Java中使用BigDecimal进行高精度计算时容易遇到的几个坑,并给出了相应的解决方案。文章通过具体的示例详细讲解了BigDecimal在创建实例、toString方法、比较大小、运算以及计算价格时的常见问题和最佳实践。适合需要进行金融计算的开发者阅读。
|
JavaScript Linux Android开发
mac环境下搭建frida环境并连接网易mumu模拟器
这篇文章介绍了如何在mac环境下搭建Frida环境,并详细说明了如何连接网易MuMu模拟器进行动态分析。
901 1