前言:
又有一阵子没有更新博客了,最近本人在完成一个开源项目。马上又到年尾了,所以时间比较赶。等完成之后会给大家分享一下,希望小伙伴们能多多支持(star)啊!好了,回到今天的主题,热修复这个技术想必大家都听过,或者也试过。这确实是一个非常牛逼的技术。但其实已经不算是新东西了。这些年来国内各大互联网巨头也都推出了自己的热修复方案,比如我要说的微信的tinker,还有像阿里的andfix,等等。之所以会出现这么一个东西,估计大家也都能猜个七八。无非就是传统的APP上线流程带来的各种弊端。小公司的小项目也许可能影响不大。但像大厂的那些DAU海量的项目那就影响非常巨大了。大家也都看到了,我今天分享的是class文件的修复,其实热修复能做的远不止这些啊,它还能对资源文件,so库等等进行修复。限于篇幅和复杂度呢,接下来我将不会涉及到这些。大家如果有兴趣可以去看看Tinker的官网,好了,下面就让我带大家来看一看热修复的简单实现吧!
一,过去与现在:
下面通过两张图我们来看看传统APP开发与热修复开发的不同之处:
很明显在第五步开始出现了不同,那我来总结一下热修复的优势有哪些:
- 无需重新发布新版本省时省力
- 用户无感修复,无需下载新版本,体验很好
- 修复效率高,降低损失
二 ,热修复原理:
关于原理我也直接画了张图,带大家看看
总结起来就是利用反射,把修复好的dexclassloader的pathlist的dexElements,和系统原本的dexElements合并,注意合并时把修复好的放前面,最后把这个合并的赋值给系统的pathList即可。说完这些我估计有很多小伙伴会很懵,首先要给大家说的是,这种class文件修复的做法原理和tinker差不多,这个小伙伴也可以去看看tinker的源码。可能有些小伙伴对这些API或者流程很不理解啊,首先大家要知道,一个apk文件它其实就是打包了dex文件和资源文件,小伙伴可以将apk文件的后缀名改为zip,再去解压一下就知道了。那么dex文件呢就是我们Android工程内所有class文件的合集。因为APP最终运行的时候就是靠Android系统去执行这些dex文件,整个过程就是从我们写下Java文件开始(源码期),经由JVM编译成class文件(编译期),然后通过dex工具转换成dex文件,最后经过Dalvik虚拟机去执行。所以我们的要做到class修复,重点就是要改变这些有bug的dex文件。而这些dex文件就存在于dexclassloader的pathlist的dexElements数组中,之后重点就是各种反射的用法了。关于反射小伙伴们如果不熟悉可以自己去查查资料回顾一下。那么,以上所有这些流程,下面我将通过代码实例展示给大家看看。
三,编码实现:
下面我直接上代码,注释已经很清楚了:
//这里我直接将已修复的dex文件打包好复制到APP私有路径中,实际运用中我们先是从服务器拉取
public void goHotFix(View view) {
//获取到apk的私有存储路径
File filesDir = getDir("dex", Context.MODE_PRIVATE);
//获取到没有bug的dex文件的名字
String name = "Test.dex";
//创建该dex文件的file
String path = new File(filesDir, name).getAbsolutePath();
//根据这个路径去创建一个新的file对象
File file = new File(path);
//如果这个文件存在就删除掉
if(file.exists()){
file.delete();
}
//创建io流
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(new File(Environment.getExternalStorageDirectory(),name));
os = new FileOutputStream(path);
int len = 0;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes))!=-1){
os.write(bytes,0,len);
}
File f = new File(path);
if(f.exists()){
Toast.makeText(this, "文件复制成功", Toast.LENGTH_SHORT).show();
}
FixManager.loadDex(this);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
is.close();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Author by YX, Date on 2019/12/21.
* 修复Dex文件的工具类
*/
public class FixManager {
//创建一个用来存储加载到的dex文件的集合
private static HashSet<File> loadedDexSet = new HashSet<>();
static {
//保证集合操作前为空
loadedDexSet.clear();
}
/**
*这里就是代替dex文件的操作
* 根据上下文来加载dex文件,并放入集合中
* @param context
*/
public static void loadDex(Context context){
if(context == null){
return;
}
//获取当前应用所在私有路径,也就是dex文件的目录
File odexDir = context.getDir("dex", Context.MODE_PRIVATE);
//通过该目录获得目录下所有文件的数组
File[] files = odexDir.listFiles();
for (File file : files) {
if(file.getName().startsWith("classes")||file.getName().endsWith(".dex")){
loadedDexSet.add(file);
}
}
//创建一个目录,用来装载解压的文件
String optimizeDir = odexDir.getAbsolutePath() + File.separator + "opt_dex";
File fopt = new File(optimizeDir);
//如果这个目录不存在就创建
if(!fopt.exists()){
fopt.mkdirs();
}
//遍历这个dex集合
for (File file : loadedDexSet) {
//获取当前dex类加载器
DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
//实现一个类加载器的对象
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
try {
//通过反射拿到系统类加载器
Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field systemPathList = baseDexClassLoaderClass.getDeclaredField("pathList");
systemPathList.setAccessible(true);
Object splObj = systemPathList.get(pathClassLoader);
Class<?> pathListClass = splObj.getClass();
Field dexElements = pathListClass.getDeclaredField("dexElements");
dexElements.setAccessible(true);
Object dexElementsObj = dexElements.get(splObj);
//创建自己的类加载器
Class<?> myBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathList = myBaseDexClassLoaderClass.getDeclaredField("pathList");
myPathList.setAccessible(true);
Object mysplObj = myPathList.get(dexClassLoader);
Class<?> myPathListClass = mysplObj.getClass();
Field myDexElements = myPathListClass.getDeclaredField("dexElements");
myDexElements.setAccessible(true);
Object mydexElementsObj = myDexElements.get(mysplObj);
//进行dex文件的融合
Class<?> componentType = dexElementsObj.getClass().getComponentType();
//分别得到两个dexElements的长度
int systemDEL = Array.getLength(dexElementsObj);
int myDEL = Array.getLength(mydexElementsObj);
//创建一个能放入它们的数组
int newL = systemDEL + myDEL;
Object newDEL = Array.newInstance(componentType, newL);
for (int i = 0; i < newL; i++) {
if(i < myDEL){
Array.set(newDEL,i,Array.get(mydexElementsObj,i));
}else {
Array.set(newDEL,i,Array.get(dexElementsObj,i-myDEL));
}
}
//将融合后的数组赋值给系统
Field systemDexElements = pathListClass.getDeclaredField("dexElements");
systemDexElements.setAccessible(true);
systemDexElements.set(splObj,newDEL);
} catch (Exception e) {
e.printStackTrace();
}finally {
Toast.makeText(context, "修复成功", Toast.LENGTH_SHORT).show();
}
}
}
}
public class MyApp extends Application{
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
//注意这里要到gradle里面先开启分包
MultiDex.install(base);
FixManager.loadDex(base);
super.attachBaseContext(base);
}
}
上面代码就是总体实现,之后我们将编译好的apk解压,要注意的是,由于做了分包处理,我们要把size更大的那个dex文件提取出来使用,为什么要分包,给你一个数字65536。可能有些小伙伴会觉得这样做,岂不是apk的体积会变得很庞大。其实呢这只是一种简单通俗的做法。但也确实会存在上述问题。我们也可以单独把修复的文件打成dex文件,这样就更好一点。当然我还是要说这只是我自己的做法,小伙伴们可以自己去研究tinker的源码看看。如果不知道如何单独把出错经修复的Java文件打包成dex文件,下面我也分几步告诉大家 :
- 首先rebuild一下工程,在app\build\intermediates\classes\debug下拿到想要代替的class文件
- 在AndroidSDK的build-tools下随便选一个版本进去打开cmd命令,
- 输入dx --dex --no-strict --output空格 生成dex文件的路径 空格 class文件的路径 最后回车即可
如果是按我的做法就是把这个dex文件取名为Test放到sdcard根目录下即可,随后就能完成修复工作了。工程源码我已开源,想要的小伙伴看这里 https://github.com/OMGyan/XHotFix
以上!!!