虚拟机JVM,Dalvik,ART

一想到虚拟机,大家可能都慌了,那是一本书呀,怎么三言两语就能说清楚呢。理解虚拟机并不是说要了解内部的代码实现,而是要知道关键原理,比如:JVM,Dalvik,ART他们之间的区别,一些关键技术(JIT/AOT),类加载,class文件 dex文件等等。

了解虚拟机原理后,知道了类加载机制,那么再去理解:热修复,插件化,增量更新就比较容易了

一,JVM

1.1 内存区域

为什么先介绍内存区域呢,如果上来就问你Full GC你会怎么回答,可能一脸萌萌哒,或者根本不知道Full Gc是什么。如果知道了,把概念一说:“Full GC是清除堆空间的,包括新生代和老年代”,这就完了吗,如果对知识结构不清楚,接下来会有很多问题。比如:什么情况下产生Full GC等问题。面试不要让面试官牵着鼻子走,要主动出击,如果是体系的回答,这样会少很多问题。好了,不扯别的了,开始脑补。

先上图:这张图就是体系,一定要记住。记住这张图才能继续往下说,每个区都是干嘛的。

在这里插入图片描述

简单介绍各个区域:注意堆和栈一定要搞清楚,不然面试会很难看。可能会问你堆和栈是干嘛的,最起码的理论得知道。
①,方法区:存储已被虚拟机加载的类,常量,静态变量等。
②,堆区:Java堆用于存储Java对象,Native堆用于存储本地代码和数据。
③,虚拟机栈:用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
④,本地方法栈:与虚拟机栈一样,只不过为native层服务。
⑤,程序计数器:当前线程所执行的字节码的行号指示器,此区域不会出现内存溢出。

再次脑补,内存区域图,然后想想每个区域的作用。

1.2 内存分配

内存区域知道了,就该说说内存分配了,这样很有条理和逻辑。

先上图:这是堆内存图,分为新生代,老年代,永久代(jdk1.8无永久代,使用metaspace实现)

在这里插入图片描述
新生代:新生代又分为Eden和Survivor区,对象优先分配在Eden区。
老年代:分配大对象,长期存活的对象。

1.3 介绍GC

有了上边的介绍,这时候就可以说说GC了,这样说的时候就很清晰了。

Minor GC:清理新生代,当Eden区没有足够的空间进行分配时,虚拟机触发Minor GC。
Major GC:清理老年代,经常会伴随至少一次的Minor GC。
Full GC:清理新生代和老年代。

Full GC产生的条件:

  • 当堆空间已满。
  • 调用System.gc()时,系统建议执行Full GC,但是不必然执行。

虚拟机是如何区分新生代和老年代的,这里还可以讲一下,对象年龄计数器,也就是分代收集算法。如果Eden区满了执行Minor GC后,对象依然存活,将会被移到Survivor区,同时对象年龄设置为1。以此类推如果Minor GC后再Survivor区依然存活,对象再加1,直到达到年龄阈值,被移到老年区。

讲到这里其实已经不错了,如果再去说一下垃圾收集器算法就更厉害了。

1.4 垃圾收集器算法

在垃圾回收前怎么知道对象要被回收呢,如下:

①,引用计数:很好理解,就是记录对象被引用的次数。
②,可达性分析算法:当一个对象到GC Roots没有任何引用链时,证明对象不可用。

再谈引用

①,强引用:只要引用存在,垃圾回收器不会回收。
②,软引用:在出现内存溢出之前,将会把这些对象回收。
③,弱引用:垃圾回收器工作时,无论当前内存是否足够,都会回收。
④,虚引用:为对象设置虚引用是能在对象回收时收到通知。

垃圾收集算法

①,标记清除法
②,标记整理法
③,分代收集法
④,复制收集法

二, Dalvik

Dalvik是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Google对其进行了特定的优化,使得Dalvik具有高效、简洁、节省资源的特点。从Android系统架构图知,Dalvik虚拟机运行在Android的运行时库层

2.1 特性

Dalvik作为面向Linux、为嵌入式操作系统设计的虚拟机,主要负责完成对象生命周期管理、堆栈管理、线程管理、安全和异常管理,以及垃圾回收等。Dalvik充分利用Linux进程管理的特定,对其进行了面向对象的设计,使得可以同时运行多个进程,而传统的Java程序通常只能运行一个进程,这也是为什么Android不采用JVM的原因。Dalvik为了达到优化的目的,底层的操作大多和系统内核相关,或者直接调用内核接口。另外,Dalvik早期并没有JIT编译器,直到Android2.2才加入了对JIT的技术支持。

2.2 Android虚拟机演化

  • Android 1.0,使用Dalvik作为Android虚拟机运行环境,此时的虚拟机是一个解释执行器。
  • Android 2.2,Android 虚拟机中加入了JIT编译器(Just-In-Time Compiler)。
  • Android 4.4,全新的ART虚拟机运行环境诞生,此时ART和Dalvik是共存的,用户可以在两者之间进行选择。
  • Android 5.0,ART全面取代了Dalvik成为了Android虚拟机运行环境,并使用AOT预编译技术在安装Apk时全量预编译 。
  • Android 7.0,ART虚拟机采用 JIT/AOT混合编译模式。

2.3 Dalvik与JVM的区别

  • 指令集: JVM 采用基于栈的指令集,而 Dalvik 采用基于寄存器的指令集。这使得 Dalvik
    在执行时更加高效,因为它可以直接在寄存器上操作,而不需要像 JVM 一样频繁地进行压栈和弹栈操作。
  • 内存管理: JVM 采用堆和栈的方式管理内存,而 Dalvik 则采用了一种基于寄存器和堆栈的混合型内存管理方式。Dalvik通过内存映射文件(memory-mapped file)将 Dex 文件(Dalvik所用的字节码文件)中的类和方法映射到内存中,然后使用寄存器和堆栈来管理这些对象的内存。
  • 即时编译(JIT): JVM 支持 JIT 编译技术,即将字节码实时编译成机器码来提高执行效率。而 Dalvik 最初不支持 JIT
    编译,而是采用 AOT(Ahead-Of-Time)编译技术,在应用安装时将字节码编译成机器码,以提高应用的启动速度和运行效率。后来,随着
    Android 2.2 版本的推出,Dalvik 开始支持 JIT 编译技术,可以实时编译字节码。
  • 运行环境: JVM 运行在操作系统之上,而 Dalvik 则是运行在 Android 操作系统之上。Android操作系统提供了一些针对移动设备的优化,如低功耗、省内存、多线程等。Dalvik 能够更好地适应移动设备的特性,提供更好的性能和稳定性。

2.4.JIT(Just-In-Time Compile)

Android 2.2之前,Dalvik虚拟机是通过解释器 (解释器逐条读入字节码 -> 逐条翻译成机器码 -> 执行机器码)来执行程序的,效率低。针对这个问题,引进了JIT(即时编译器)技术。它是一种优化手段。

JIT技术:将解释过的机器码缓存起来,下次再执行时到这个方法的时候,则直接从缓存里面取出机器码来执行。减少了读取字节码和翻译字节码的操作。以此来提高效率。JIT技术的引入使得Dalvik的性能提升了3~6倍。

注意: 并不是所有执行过的代码对应的机器码都会被缓存起来。而是只有被认定为热点代码(Hot Spot Code)的代码才会。这里所指的热点代码主要有两类,包括:

1.被多次调用的方法
2.被多次执行的循环体。

缺点: JIT技术的缺点:
1.每次重新启动引用都需要重新编译。
2.运行时比较耗电。

三,ART

ART虚拟机在Android 5.0开始替换Dalvik虚拟机,其处理应用程序执行的方式不同于Dalvik虚拟机,它不使用JIT而是使用了AOT(Ahead-Of-Time),也就是提前编译技术。并对垃圾收集器也进行了改进和优化。

3.1 AOT(Ahead-Of-Time)预先编译技术

AOT(提前编译技术): 简单来说就是提前将字节码转换成本地机器码,然后存储在本地磁盘上,运行时可以直接执行,避免了Dalvik时期的应用运行时再来解释字节码。运行时效率大大提高。

在Android 7.0 之前,Android系统安装Apk时,会进行一次全量预编译,将字节码预先编译成本地机器码,生成 oat文件,并存储在本地磁盘上。这样在App每次运行时就不需要重新编译,可以直接使用编译好本地机器码,运行效率大大提升。但是这也使得安装应用的时间大大增加,于是在Android7.0及之后,又重新引进了JIT技术,形成JIT/AOT混合编译模式。

混合编译的特点:

  • 应用在安装的时候,不进行AOT预编译。
  • 应用运行时直接通过解释器翻译字节码为机器码然后执行。(在应用运行期间使用了JIT技术)并同时记录热点代码信息到profile文件中。
  • 手机进入空闲或充电状态的时候,系统会扫描APP目录下的profile文件,并通过AOT对热点代码进行编译。
  • 下一次启动时,会根据profile文件来运行已编译好的机器码,避免在运行时对已经转换为机器码的方法又进行了JIT编译。
  • 应用运行期间会持续对热点代码进行记录,以方便在空闲或充电时进行AOT,以此循环。

3.2 Dalvik与ART虚拟机的区别

  • 编译方式: Dalvik 是一种基于解释器的运行时环境,它在应用程序运行时将字节码解释成机器码执行。而 ART 则是一种基于AOT(Ahead-Of-Time)编译的运行时环境,它在应用程序安装时将字节码编译成本地机器指令,并保存在设备上,以便后续的运行。
  • 内存使用: ART 比 Dalvik 更加高效地使用内存,因为它能够更好地利用设备的多核处理器和更大的内存容量。ART将所有应用程序的代码提前编译成本地机器代码,并将其存储在设备上,这样可以避免在应用程序运行时重复编译和优化,从而减少内存使用和提高性能。
  • 应用启动时间: 由于 ART 的 AOT编译方式,它在应用程序启动时需要花费更多的时间来编译和优化代码,从而导致应用程序的启动时间相对较长。但是,一旦应用程序编译完成,它的执行速度会更快。
  • 兼容性: ART 在 Android 4.4(KitKat)版本中首次推出,取代了 Dalvik。由于 ART 的引入,一些使用Dalvik 运行时环境的应用程序可能会出现兼容性问题,需要进行调整和优化。

四,安装包解析

一个android应用在安装到手机上,系统是怎么解析的呢。在我们解压apk安装包后会发现有dex文件,虚拟机是怎么解析和运行的呢。

4.1 dex文件是什么

dex是二进制文件,用于在Android虚拟机上执行。是通过把所有的class文件进行合并优化得到的。dex文件去除了class文件中的冗余信息(比如重复字符串),并且结构更加紧凑,因此在dex解析阶段可以减少I/O操作,提高类查找速度。
它与.jar文件不同,.jar文件像是一个文件夹,里面的.class是单独的文件,各个class信息里面会出现重复的信息。而dex文件,则将所有的.class里面的信息整合在一起,去除掉里面的重复数据。

在这里插入图片描述

4.2 odex文件

dex是从apk提取出dex文件并通过优化后得到的产物,它被保存到data/dalvik-cache目录下。原apk文件中的classes.dex可以保留也可以删除,甚至有时候会留下残缺的dex文件。
系统在首次启动时,需要对预置的apk进行安装,此时需要将dex从apk文件中解压出来放到data/app文件夹中。

在Dalvik虚拟机中,会通过dexopt来对dex进行优化,生成odex文件,并将其保存到手机的VM缓存文件夹data/dalvik-cache下(注意,这边生成的odex文件后缀依然是dex )。它是一个dey文件,里面仍然还是字节码。

在ART虚拟机上,同样会在首次进入系统的时候使用dexopt工具来对dex进行优化,不过此时的优化是将dex字节码翻译成本地机器码。并保存在data/dalvik-cache下。

一般情况下,在Android系统进行编译的时候,预处理提取odex文件的话,将会大大优化系统首次启动时间。

4.3 65535问题

单个DEX文件中最多可以引用的64K个方法的限制。

当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised。DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。

五,热修复,插件化,增量更新

5.1 什么是类的加载

java文件通过编译器变成了.class文件,类加载器又将这些.class文件加载到JVM中,如下图所示。
在这里插入图片描述9db15e4243a1492ed4e0710e19.png)
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

5.2 android中的类加载器

在这里插入图片描述

  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。BootClassLoader是它的内部类。
  • BootClassLoader:启动了加载器,和Java虚拟机不同,BootClassLoader是由Java代码实现,而不是C++实现。
  • BaseDexClassLoader:用于加载dex文件,PathClassLoader和DexClassLoader是它的两个实现类。
  • DexClassLoader:支持加载APK、dex、jar,也可以从SD卡加载。
  • PathClassLoader:该加载器将optomizedDirectory设置为null,默认路径为/data/dalvik-cache目录,即加载已经安装的应用。

先来了解ClassLoader中的核心方法

loadClass

       protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
            {
                synchronized (getClassLoadingLock(name)) {
                    // First, check if the class has already been loaded
                    Class c = findLoadedClass(name);//检查是否已经加载
                    if (c == null) {
                        long t0 = System.nanoTime();
                        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.
                            long t1 = System.nanoTime();
                            c = findClass(name);
        
                            // this is the defining class loader; record the stats
                            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                            sun.misc.PerfCounter.getFindClasses().increment();
                        }
                    }
                    if (resolve) {
                        resolveClass(c);
                    }
                    return c;
                }
            }

双亲委派: 首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

所谓的双亲指的就是 ClassLoader 和 BootStrapClassLoader

  • ClassLoader 是 java 生态里最顶层的类加载器。
  • BootStrapClassLoader 是 C++ 生态中的类加载器。

findClass

findClass一般由子类去实现

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

definclass

把字节码转化为Class

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

看一下BaseDexClassLoader 部分源码:

public class BaseDexClassLoader extends ClassLoader {
    // 需要加载的dex列表
    private final DexPathList pathList;
    // dexPath要加载的dex文件所在的路径,optimizedDirectory是odex将dexPath
    // 处dex优化后输出到的路径,这个路径必须是手机内部路劲,libraryPath是需要
    // 加载的C/C++库路径,parent是父类加载器对象
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        // 使用pathList对象查找name类
        Class c = pathList.findClass(name, suppressedExceptions);
        return c;
    }
}

接着看DexPathList 源码:

其中有两个核心方法makeDexElements和findClass。

/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private final ClassLoader definingContext;
    // 
    private final Element[] dexElements;
    // 本地库目录
    private final File[] nativeLibraryDirectories;

    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        // 当前类加载器的父类加载器
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // 根据输入的dexPath创建dex元素对象
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }
}
//将所有这些dex文件都加入到Element数组中
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
    ArrayList<Element> elements = new ArrayList<Element>();
    // 所有从dexPath找到的文件
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();
        // 如果是文件夹,就直接将路径添加到Element中
        if (file.isDirectory()) {
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()){
            // 如果是文件且文件名以.dex结束
            if (name.endsWith(DEX_SUFFIX)) {
                try {
                    // 直接从.dex文件生成DexFile对象
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                zip = file;

                try {
                    // 从APK/JAR文件中读取dex文件
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }

        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }

    return elements.toArray(new Element[elements.size()]);
}

// 加载名字为name的class对象
public Class findClass(String name, List<Throwable> suppressed) {
    // 遍历从dexPath查询到的dex和资源Element
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        // 如果当前的Element是dex文件元素
        if (dex != null) {
            // 使用DexFile.loadClassBinaryName加载类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

5.3 热修复

市面上流行的热修复框架主要有三个方案,类加载方案,底层替换方案和Instant Run方案。

类加载方案

了解完android中的类加载器之后,从BaseDexClassLoader 入手开始分析实现过程。

BaseDexClassLoader中有个DexPathList 类,其中保存了Element。Element内部封装了DexFile用于加载dex文件,因此每个dex文件对应一个Element。

这就是关键地方,我们可以将有bug的类test.class进行修改,然后将test.class打包dex的补丁包test.jar,通过反射放在dexElements数组的前边,这样首先找到test.dex中的test.class去替换之前存在bug的test.class,排在数组后面的dex文件根据ClassLoader的双亲委托模式就不会被加载。

类加载方案需要重启App才能生效,不能即时生效。因为在App启动之后所有类已经加载完成,在Android上是无法对类进行卸载。如果不重启,类还在虚拟机中。

底层替换方案

底层替换不同的地方是可以及时生效,直接在Native层直接修改原类,底层替换方案通过在运行时利用hook操作native指针实现“热”的特性。

底层替换所操作的指针,实际上是ArtMethod,在类被加载,类中的每个方法都会有对应的ArtMethod,它记录了方法包括所属类和内存地址信息。

由于不同的厂商对ArtMethod结构进行了修改,Sophix采用了对旧ArtMethod进行完整替换。因此Sophix采用类加载和底层替换相结合的方案。

ArtMethod 对象包含了 Java 方法的相关信息,例如方法名、参数类型、返回类型、代码实现等。ArtMethod 对象在运行时动态生成,并被存储在内存中,以便能够快速地被访问和执行。当调用一个 Java 方法时,Dalvik 虚拟机或者 Art 运行时会根据方法名、参数类型等信息,在内存中查找对应的 ArtMethod 对象,并执行其中的代码实现。ArtMethod 对象的创建和管理是由 Art 运行时负责的,其结构体定义和实现则是由 Android 系统提供的 C++ 库进行实现。ArtMethod 对象在运行时是不可变的,即一旦被创建和初始化,就不可以再修改其包含的信息。因此,如果需要对一个 Java 方法进行修改或者替换,需要重新创建一个新的 ArtMethod 对象,并替换原来的对象。

Instant Run方案

Instant Run是基于多ClassLoader的,每一个patch都有一个ClassLoader,这就意味着如果你想更新patch,它都会创建一个ClassLoader,而在java中不同ClassLoader创建的类被认为是不同的,所以会重新加载新的patch中的补丁类。

Instant Run原理:

  • 首先构造一个新的AssetManager,并通过反射调用addAssetPath方法,把这个完整的新资源包加入到AssetManager中,这样就获得了一个含有所有新资源的AssetManager。
  • 找到所有之前引用到原有AssetManager的地方,通过反射将引用处替换成AssetManager。

Sophix实现原理:

  • 构造一个package id为0x66的资源包,该包只包含修改了的资源项,采用的替换方式是直接在原有的AssetManager对象上进行析构和重构。
  • 由于补丁包的package id 为0x66,不与目前已经加载的0x7f冲突,因此直接加入到已有的AssetManager中就可以使用了。

5.3 插件化

插件化是直接把一个apk当成一个模块运行,可以减轻app的安装包大小。而热修复主要是修复已存在的代码,通过反射的方式把dex文件信息加入需要加载的列表中。

第一步:

插件化场景下,会存在同一进程中多个 ClassLoader 的场景:

  • 宿主 ClassLoader:宿主是安装应用,运行即自动创建。
  • 插件 ClassLoader:使用 new DexClassLoader 创建。

我们称这个过程叫做 ClassLoader 注入,完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载。

由于 ClassLoader 的双亲委派机制,实际上系统类不受 ClassLoader 的类隔离机制所影响。

第二步:

我们都知道 Android 组件都是由系统调用启动的,未安装的 Apk 中的组件,是未注册到 AMS 和 PMS。在startAcitity是,系统无法找到。

因此可以在宿主app中预埋一些空的 Android 组件,并在清单文件中注册。比如:这个空的Activity可以接受参数pluginName,pluginApkPath。当这个Activity启动时加载插件的ClassLoader,并通过反射找到对应的组件。

第三步:

由于app中的Resource 被打包到apk文件中,会生成R文件对应的资源id。插件中的Resource 可以通过这两个接口获取。

  • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 Apk 的PackageInfo。
  • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例。

拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,可以重写一个新的 Resources。再复写组件中#getResources,将获取到的资源替换掉。

在资源合并过程中可能存在资源id冲突问题,解决方法使不同的插件资源拥有不同的资源id。

资源id是由8位16进制数表示,表示为0xPPTTNNNN。PP段用来区分包空间,默认只区分了应用资源和系统资源,TT段为资源类型,NNNN段在同一个APK中从0000递增。

5.4 增量更新

增量更新其实就是拆分到合并的过程,安装旧版本包。新版本包通过打差分包生成一个补丁,更新时只需要下载补丁,下载到本地后再与旧版本包合并成新包。

增量更新是借助bsdiff 一个开源库实现的,这个开源库是根据文件二进制去实现文件的差分 。

增量更新麻烦在于需要维护不同版本对应的差分包。

bsdiff介绍

bsdiff是一种用于生成二进制补丁的工具,它可以通过比较两个二进制文件的差异来生成一个小巧的补丁文件,该补丁文件可以用于将旧版本的文件升级到新版本。

bsdiff的核心原理是基于一种叫做“二分查找”的算法,该算法可以在一个有序数组中高效地查找一个特定的值。在bsdiff中,这个有序数组是原始文件的字节流,而要查找的特定值是新文件与旧文件的差异。

bsdiff的生成过程分为三个步骤:

1.生成原始文件和新文件的差异数据,这里用到了类似于“比特位图”的数据结构,将新文件与原始文件的差异以二进制的形式表示。

2.使用类似于“霍夫曼编码”的算法对差异数据进行压缩,使得生成的补丁文件更小。

3.将压缩后的差异数据与原始文件的某些元数据一起写入补丁文件中,以便在升级时进行解压和应用。

通过这种方式,bsdiff可以生成一个小巧而高效的补丁文件,用于将旧版本的文件升级到新版本。

六,面试题

经过上文的分析讲解,相信在遇到下面问题的时候,已经胸有成竹了。

5.1 JVM,Dalvik,ART区别

  • JVM基于栈指令,需要更多的指令,占用内存较大。
  • Dalvik基于寄存器,需要更多的指令空间,编译时花费的时间短。
  • ART是Dalvik的升级版,采用了JIT/AOT混合编译模式。

5.2 APK安装过程

apk的安装其实就是解压的过程,把相关的文件解压到本地。

大体说清一个应用程序安装到手机上时发生了什么:

  1. 复制apk到/data/app目录下,解压并扫描安装包。
  2. 资源管理器解析apk里的资源文件。
  3. 解析AndroidManifest文件,在/data/data/目录下创建对应的应用数据目录。
  4. 对dex文件进行优化生成odex,并保存在dalvik-cache目录下。
  5. 解析AndroidManifest文件中的信息并保存到/data/sytem/packges.xml。
  6. 安装完成后,发送广播。

整个流程通过PackageManagerService实现。

5.3 热修复,插件化,增量更新区别

  • 热修复:修复线上存在的bug,利用ClassLoader通过反射的方式加载补丁包中的dex文件。
  • 插件化:动态加载apk,采用多ClassLoader机制构建宿主容器,利用PackageManager加载资源。
  • 增量更新:缓解apk包大小压力,利用bsdiff技术实现包的拆分合并。

5.4 线程保存在哪里

Java中的对象实例都是存储在堆中的,而基本类型的变量则存储在栈中。

5.5 Bitmap保存在哪里

  • android2.3.3(API level10)和更早的版本,bitmap对象和对象里对应的像素数据是分开存储的,bitmap存在虚拟机的堆里,而像素数据存储在native内存里。
  • android3.0(API level 11)到android7.1(API level25),bitmap对象及其像素数据都存储在虚拟机的堆里。
  • android8.0(API level 26)开始,bitmap对象存储在虚拟机的堆里,而对应的像素数据存储在native堆里。

5.6 系统为每个应用分配内存多大

Android系统为每个应用程序分配的内存确实有一个上限。这个上限被称为"应用程序堆大小",通常情况下是128MB,但是具体的大小也取决于Android版本和设备的硬件规格。

应用程序堆是一个应用程序可以使用的内存区域,其中包括Java堆和Native堆。

一下代码可以查看应用可使用内存上限。

        // 获取Java Runtime实例
        Runtime runtime = Runtime.getRuntime();

        // 获取最大可用内存大小,以字节为单位
        long maxMemory = runtime.maxMemory();

        // 将字节数转换为兆字节
        long maxMemoryInMb = maxMemory / (1024 * 1024);

        // 打印最大可用内存大小
        Log.d(TAG, "Max memory available: " + maxMemoryInMb + " MB");

用我们自己的APP实际测试:

  1. 华为mate8 系统8.0 内存上限:512MB。
  2. 小米note 系统7.0 内存上限:512MB。
  3. 魅族T6 系统7.0 内存上限:512MB。
  4. 华为 pad6 系统鸿蒙2.0 内存上限:512MB。

5.7 android应用使用的是同一个虚拟机吗

每个应用在 Android 系统中都在自己独立的进程中运行,每个进程都会启动一个虚拟机实例来执行应用程序的代码,因此每个应用程序都会有自己独立的虚拟机实例来运行应用程序的代码。

5.8 虚拟内存和物理内存

  • 虚拟内存是一种内存管理技术,它通过地址转换机制将虚拟内存地址映射到物理内存地址上,从而提供了统一的内存地址空间。
  • 物理内存是计算机系统中实际存在的内存,它直接由硬件提供支持。计算机系统通过物理内存来存储程序和数据,CPU可以直接访问物理内存中的数据。

5.9 android一个线程,一个进程占多大内存?

每个线程至少会占用1M的虚拟内存大小(实际大小根据设备厂商定义有区别)
早期android系统只为一个单进程的应用分配了16M的可用内存。

5.10 Binder可传输内存为何是1M?

以防止恶意应用程序利用Binder缓冲区进行攻击。

总结

本篇主要讲解了虚拟机的原理和技术点,从而延伸到开发中的一些技术实现。总结看来可以分两个大方向去记忆和理解:虚拟机部分,类加载部分。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值