Android热修复——实现原理解析

一、简述

android热修复是这2年较火的新技术,是作为安卓工程师必学的技能之一。在以前,线上产品如果出现了一点bug,就只能在修复后重新打包测试然后审核上线,然后用户还得重新下载安装,费时费力,大大降低了用户体验,但是现在有了热修复方案。
目前较火的热修复方案有很多:

andfix,tinker,还有阿里最新的sophix方案(据称支持大多数加固)

本篇文章通过通俗易懂的方式来解析热修复基本原理,当然和上述成熟产品还有差距。

二、bug修复流程

当线上产品出现bug的时候,比如某个java文件代码写的不好出现了空指针,也就导致这个class文件有问题,所以要做的就是替换这个class文件。

  1. 下发补丁,也就是从服务端获取补丁包。
  2. 替换成新的class文件,实现bug修复。

如何才能替换class文件,就是修复的关键点。首先先来了解一下java的类加载器classLoader。

三、java类加载器

先来了解一下虚拟机,java使用的是jvm,而android使用的是经过优化的Dalvik虚拟机还有之后的ART。

  1. Dalvik执行.dex格式的字节码
  2. JVM执行.class格式的字节码。
    由于执行格式不同,所以java使用的是classLoader来加载,而android需要使用专用的类加载器DexClassLoader和PathClassLoader。

PathClassLoader和DexClassLoader的区别

  • PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
  • DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。
    来看看两者区别,直接贴代码:
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

从源码来看两者都继承自BaseDexClassLoader,唯一区别是DexClassLoader传入了一个optimizedDirectory参数。然后再来看下BaseDexClassLoader的构造函数:

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
  • dexPath:要加载的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目
  • optimizedDirectory:dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的程序文件时会解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件的)。
  • libraryPath:加载程序文件时需要用到的库路径。

注意这里的pathList,在下面会进行解析

通过类加载器获取class

来看BaseDexClassLoader的findClass()方法,该方法用来获取加载到的class

protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

可以看到,实际是调用的是之前提到的pathList对象的findClass方法,所以先来看看DexPathList到底做了什么。

private final Element[] dexElements;
  public DexPathList(ClassLoader definingContext, String dexPath,
83            String libraryPath, File optimizedDirectory) {
84        if (definingContext == null) {
85            throw new NullPointerException("definingContext == null");
86        }
87
88        if (dexPath == null) {
89            throw new NullPointerException("dexPath == null");
90        }
91
92        if (optimizedDirectory != null) {
93            if (!optimizedDirectory.exists())  {
94                throw new IllegalArgumentException(
95                        "optimizedDirectory doesn't exist: "
96                        + optimizedDirectory);
97            }
98
99            if (!(optimizedDirectory.canRead()
100                            && optimizedDirectory.canWrite())) {
101                throw new IllegalArgumentException(
102                        "optimizedDirectory not readable/writable: "
103                        + optimizedDirectory);
104            }
105        }
106
107        this.definingContext = definingContext;
108        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
109        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
110                                           suppressedExceptions);
111        if (suppressedExceptions.size() > 0) {
112            this.dexElementsSuppressedExceptions =
113                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
114        } else {
115            dexElementsSuppressedExceptions = null;
116        }
117        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
118    }

主要看109行的makeDexElements方法,返回了一个dexElements数组,接下来得看该方法到底做了什么(方法中第一个参数方法splitDexPath(dexPath)实际是返回一个dex文件集合):

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
    // 1.创建Element集合
    ArrayList<Element> elements = new ArrayList<Element>();
    // 2.遍历所有dex文件(也可能是jar、apk或zip文件)
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        ...
        // 如果是dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);

        // 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
        } else {
            zip = file;
            dex = loadDexFile(file, optimizedDirectory);
        }
        ...
        // 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    // 4.将Element集合转成Element数组返回
    return elements.toArray(new Element[elements.size()]);
}

在这个方法中可以看出,把dex或压缩文件转成Element对象然后添加Element集合中最后转成数组形式返回。

findClass()

最后来看看DexPathList的findClass()方法:

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        // 遍历出一个dex文件
        DexFile dex = element.dexFile;

        if (dex != null) {
            // 在dex文件中查找类名与name相同的类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

热修复实现原理

从最后的findClass方法可以看到,对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。所以热修复的原理就是:

获取补丁的element数组,然后拿到app原来的element数组,合并成一个新的element数组,注意把补丁的数据放前面,最后把新的element数组通过反射的方式赋值给当前app的pathList的dexElement属性。因为修复好的class所在的element排在有bug的element前面,会先被拿到,所以实现了热修复。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值