Android热修复技术-基本知识

一、class文件

1、什么是class文件

能够被JVM识别,加载并执行的文件格式。除了Java,Scala、Python等也可以生产class文件。
在这里插入图片描述

2、怎么生成class文件

  • 通过IDE自动帮我们build
  • 通过javac去生成

3、class文件的作用

记录一个类文件的所有信息,注意是所有信息。多于在java源代码中看到的信息(如java类中并没有this、super等关键字。那为什么我们可以使用这两个字段呢?因为Java虚拟机在生成class文件时补充了this和super字段)。

4、class文件结构

整体特点

  • 一种8位字节的二进制流文件
  • 各个数据按顺序紧密的排列,无间隙(有的文件格式为了读取方便每100个字节一个空格,但是这样会增加文件的大小)
  • 每个类或接口都单独占据一个class文件
    文件结构
    在这里插入图片描述

5、class文件的缺点

  • 内存占用大,不适合移动端
  • 堆栈加载模式,加载速度慢
  • 文件IO操作多,类查找慢
    因为这些缺点,所有Android使用dex文件

二、dex文件

1、什么是dex文件

能够被DVM识别,加载并执行的文件格式。除了Java,通过c或者c++也可以生成dex文件。

2、怎么生成dex文件

  • 通过IDE自动帮我们build生成,使用es或者as run一个工程得到apk解压后就得到dex文件
  • 手动通过dx命令去生成dex文件,IDE其实也是使用dx命令生成的
    1、先javac生成class文件
    2、再使用dx生成dex文件,命令如:dx --dex --output Hello.dex Hello.class
    3、将dex文件push到手机中,命令如:adb push Hello.dex /storage/emulated/0
    4、然后adb shell进入登录到手机控制台
    5、运行dex文件,命令如:dalvikvm -cp /sdcard/Hello.dex Hello

3、dex文件的作用

记录整个工程中所有类文件的信息,注意是整个工程。

4、dex文件的格式

整体特点

  • 一种8位字节的二进制流文件
  • 各个数据按顺序紧密的排列,无间隙(有的文件格式为了读取方便每100个字节一个空格,但是这样会增加文件的大小)
  • 整个应用中所有Java源文件都放在一个dex中
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191122164820503.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTE2ODI2NzM=,size_16,color_FFFFFF,t_70

5、dex文件和class文件的区别

  • 本质上他们都是一样的,dex是从class文件演变而来的
  • class文件存在许多冗余信息,dex会去除冗余,并整合
    在这里插入图片描述

三、JVM

1、整体结构

在这里插入图片描述

2、编译流程

JVM字节码就是class文件,这个编译时Javac实现的。
在这里插入图片描述

3、类加载器

1)、java中的ClassLoader

类加载器将class文件加载到jvm的内存中。
在这里插入图片描述

2)、android中的ClassLoader
a、ClassLoader的分类
  • BootClassLoader 加载android Frmawork层的一些字节码文件
  • PathClassLoader 加载已经安装到系统中的apk文件中的字节码文件
  • DexClassLoader 加载指定目录中的字节码文件
  • BaseDexClassLoader 是PathClassLoader 和DexClassLoader 的父类
    一个app至少需要BootClassLoader 和PathClassLoader 这两个加载器才能正常运行。
b、特点和作用

双亲代理模型的特点。ClassLoader加载一个类时会询问当前ClassLoader是否加载过这个类,如果已经加载过就直接返回,不再重复加载。如果没有加载会去查询parent是否加载过此类,如果加载过,就返回parent加载的字节码文件,如果整个继承链上的父类都没有加载,才会使用该ClassLoader去加载这个类。如果一个类被位于树中任意一个ClassLoader加载过,那么在整个生命周期这个类都不会被重复加载了,这样大大提高类加载效率。
作用:

  • 类加载的共享功能 如Frmawork层的一些类被顶层ClassLoader加载后就不用再加载了,会缓存在内存中,其他地方可以直接使用。
  • 隔离功能 不同继承线上加载的类肯定不是同一个类 避免客户自己写一些代码冒充核心类库,访问核心类库中一些可见的成员变量。
    同一类名、包名、同一类加载器加载的类才是同一个类。
c、加载流程

在这里插入图片描述

d、源码分析

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);
    }
}

DexClassLoader和PathClassLoader的代码很简单,都是继承自BaseDexClassLoader,并且构造方法也同样都调用了父类的构造方法,区别就是第二个参数,DexClassLoader有优化目录参数optimizedDirectory,而PathClassLoader的参数为null。也正是因为这点,导致DexClassLoader和PathClassLoader有不同的使用场景:DexClassLoader可以用于加载任意目录下的dex、zip、jar、apk里面的dex文件,而PathClassLoader只能加载应用路径下的目录(data/app目录)。
DexClassLoader和PathClassLoader的实现都在BaseDexClassLoader ,我们看看
BaseDexClassLoader 源码如下:

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                              String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }
    
    @Override
    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;
    }
}

构造方法参数意义:

  • dexPath:含有类和资源的jar/apk的路径,多个路径默认会被冒号分开
  • optimizedDirectory:优化路径,放置优化后的dex文件的路径,app安装的时候会将dex文件先优化成odex文件才能被使用,此路径就是用于放置odex文件的路径,并且此路径只能是app的内部路径(data/data/包名/XXX)
  • librarySearchPath:加载类时需要使用的本地库
  • parent:父加载器

在构造方法中新建了一个this.pathList = new DexPathList();我们看看DexPathList是什么。DexPathList构造方法源码如下。

final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String zipSeparator = "!/";
    private Element[] dexElements;
    private final Element[] nativeLibraryPathElements;

    public DexPathList(ClassLoader definingContext, String dexPath,
                       String librarySearchPath, File optimizedDirectory) {
		......
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                suppressedExceptions, definingContext);
        ......
    }
}

构造方法中新建了dexElements ,dexElements 是一个Element类型的数组。splitDexPath(dexPath)就是以:为分隔符把dexPath分成多个文件。makeDexElements()源码如下:

private static Element[] makeElements(List<File> files, File optimizedDirectory,
                                      List<IOException> suppressedExceptions,
                                      boolean ignoreDexFiles,
                                      ClassLoader loader) {
    Element[] elements = new Element[files.size()];
    int elementsPos = 0;
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();

        if (path.contains(zipSeparator)) {
            String split[] = path.split(zipSeparator, 2);
            zip = new File(split[0]);
            dir = new File(split[1]);
        } else if (file.isDirectory()) {
            // We support directories for looking up resources and native libraries.
            // Looking up resources in directories is useful for running libcore tests.
            elements[elementsPos++] = new Element(file, true, null, null);
        } else if (file.isFile()) {
            if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory, loader, elements);
                } catch (IOException suppressed) {
                    System.logE("Unable to load dex file: " + file, suppressed);
                    suppressedExceptions.add(suppressed);
                }
            } else {
                zip = file;

                if (!ignoreDexFiles) {
                    try {
                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
                    } catch (IOException suppressed) {
                        /*
                         * IOException might get thrown "legitimately" by the DexFile constructor if
                         * the zip file turns out to be resource-only (that is, no classes.dex file
                         * in it).
                         * Let dex == null and hang on to the exception to add to the tea-leaves for
                         * when findClass returns null.
                         */
                        suppressedExceptions.add(suppressed);
                    }
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }

        if ((zip != null) || (dex != null)) {
            elements[elementsPos++] = new Element(dir, false, zip, dex);
        }
    }
    if (elementsPos != elements.length) {
        elements = Arrays.copyOf(elements, elementsPos);
    }
    return elements;
}

上面代码就是将File列表转换为Element数组。

下面看看是怎么加载类的。loadClass在ClassLoader中实现,ClassLoader是BaseDexClassLoader 的父类。loadClass()源码如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    // First, check if the class has already been loaded
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }

        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    return c;
}

这里就是“双亲委托模式”的实现。findLoadedClass()先看自己有没有加载过这个类,没有加载通过parent.loadClass(name, false)查看父类加载没有,如果还是没有加载就调用findClass()去加载这个类。 如果某个类被ClassLoader继承链上任意一个加载器加载后就不会再重复加载了。
JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。所有热修复的代码要在Application中调用,以免调用了bug代码,加载了bug类,那么修复bug的类就不会调用了。
findClass()在ClassLoader中是一个抽象方法,其在子类中实现。DexClassLoader和PathClassLoader的findClass方法都是在BaseDexClassLoader实现,我们看看findClass的源码:

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()。pathList.findClass()源码如下:

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

遍历dexElements数组,去每一个dex中找这个类。DexFile类中dex.loadClassBinaryName()源码如下:

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
}
    
private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                 DexFile dexFile, List<Throwable> suppressed) {
    Class result = null;
    try {
        result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}

loadClassBinaryName()中直接调用defineClass(),然后调用defineClassNative(),defineClassNative()源码如下:

private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
                                              DexFile dexFile)
        throws ClassNotFoundException, NoClassDefFoundError;

这里就是调用原生方法返回class。

4、内存管理

1)、java栈区

作用:存放java方法执行时的所有数据。
组成:由栈帧组成,一个栈帧代表一个方法的执行。
栈帧:每个方法从调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈。比如a方法在运行时调用了b方法,a调用b时java虚拟机就会创建一个保存b方法的栈帧,然后把这个栈帧压入到栈区当中。当b方法执行完后,要返回到a方法的时候这个栈帧就会弹出栈区。
栈帧中包含局部变量表、栈操作数、动态链接、方法出口。即存储了方法调用过程中的所有内容。

2)、本地方法栈

作用:专门为native方法服务的。

3)、方法区

作用:存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后等数据。方法区永远占据内存。

4)、堆区

作用:所有通过new创建的对象的内存都在堆中分配。
特点:是虚拟机中最大的一块内存,是GC要回收的部分。
堆区内存分布如下图:
在这里插入图片描述刚创建的对象会放到Young Generation,当Young Generation内存不足时,java虚拟机会根据一定的规则和算法将Young Generation中的对象移到Old Generation中。这样Young Generation就有空闲的内存,可以继续分配内存。当Young Generation和Old Generation都没有内存 时,虚拟机就会抛出OutOfMemory 异常。垃圾回收机制主要也是回收Young Generation和Old Generation中的内存。
为什么不使用一块内存,而分成Young Generation和Old Generation两块内存呢?
因为这样方便开发人员动态分配两块内存区的大小。

5、垃圾回收
6、Dalvik与JVM的不同
  • 执行的文件不同,Dalvik是dex,JVM是class
  • 类加载系统不同
  • DVM可以同时存在多个(好处是某一个应用程序挂掉后不会影响其他应用程序),JVM只能存在一个
  • Dalvik是基于寄存器的(运行更快),JVM是基于栈的
    虽然Dalvik相对JVM已经优化,但还是有点慢,所以Goole又开发了ART
7、Dalvik与ART的不同
  • DVM使用JIT将字节码转化为机器码,效率低。JIT就是应用程序每次运行时都要把字节码转化为机器码,然后再去执行。如果应用程序重新启动又重新转化一次。
  • ART采用AOT预编译技术,执行速度更快。ART是应用程序安装时就把字节码转化为机器码存储在存储介质中,这样应用程序每次执行都是直接执行本地机器码,不用每次转化了。ART的缺点是占用更多安装时间和存储空间。

四、Android动态加载难点

  • 有许多组件类需要注册才能使用,如Activity、Service
  • 资源动态加载很复杂(资源访问时使用id访问,资源也是注册后才有id的)
  • Android不同版本对类和资源加载的方式不同
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值