Android 笔记

Android学习笔记

一、Java基础

(一)、泛型

1.为什么使用泛型?

1.适用于多种数据类型执行相同的代码

2.泛型中的类型在使用时指定,不需要强制类型转换

泛型分为:

泛型接口、泛型类、泛型方法 (带有<T>)

2.基本类型不能作为泛型类型,只有引用类型可以
3.通配符:
  1. <? extends 类 &接口> 决定继承上限, 如果有类,则类必须放在第一个位置

  2. <? super 类 &接口> 决定继承下限

通配符只能使用在方法上,不能用在类和接口上

(二)、注解

1.java中的所有注解默认实现Annotation接口,与class不同,注解使用@interface进行声明
2.元注解:注解的注解
  1. @Target:可以限制应用注解的java元素类型,可以使用多个范围
  2. @ Retention:指定标记注解的存活时间。(源文件、class文件、JVM运行时)
3.运用场景
  1. 源码级别(APT):在编译期间能够获取注解与注解声明的类,包括类中所有的成员信息,一般用于生成额外的辅助类。
  2. 字节码级别(字节码增强):在编译出class之后,通过修改class数据以实现修改代码逻辑的目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
  3. 运行时级别(反射):在程序运行期间,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判定。

应用场景:

  1. APT(注解处理器)
  • 自定义注解

  • 使用注解

  • (新建module)自定义注解处理器

  • 在main目录下新建/resources/META-INF/services/javax.annotation.processing.Processor

  • 在新建的文件中添加自定义注解处理器的全类名,如:com.qdreamer.compiler.***Processor

  • 将新建得module作为依赖添加到要使用的模块中,然后编译
    <font color=Red>注意:IDEA目前需要使用AutoService注解才能生效。</font>

<font color = Red> Androidx.annotation 提供了一个叫IntDef的元注解,用于语法检查。这个语法检查是由于IDE实现的,IDE插件。</font>

  1. class字节码级别:字节码增强技术,注解用于区分哪些地方需要加逻辑

  2. 运行时级别:注解+反射完成 findViewById

反射 :

  • aClass.getField() 获得自己和父类的成员(不包括 private)。

  • aClass.getDecleredField() 只能获得自己的成员(包括private但是不包括父类的成员)。

  • setAcessable(true)如果成员是private,则需要设置访问权限。

4.APT详解:
  1. APT程序就是javac的小插件,由javac在编译时候根据条件调起。

  2. 注解处理器的执行是有javac调起我们APT实现类的process方法。而这个方法就在round.run中调起。

  3. APT实现类返回值为true,删除它能处理的注解信息,这样其他需要处理相同注解的注解处理器就得不到执行了。

  4. APT中process方法最少执行两次,最多无数次,最后一次执行可以视为Finish结束通知,执行收尾工作。

<font color=Red>注意:APT在调起我们实现类的process方法使用的是java的SPI机制,Service Provider Interface是一种JDK内置的动态加载实现扩展点的机制,SPI机制的重点在于ServiceLoader</font>

5.反射获取泛型真实类型

当我们对一个泛型类进行反射时,需要得到泛型中的真实数据类型,来完成json反序列化的操作。此时需要通过type体系来完成。Type接口包含了一个实现类(Class)和四个实现接口,他们分别是:

  1. TypeVariable
  • 泛型类型变量,可以获得泛型上下限的等信息。
  1. ParameterizedType
  • 具体的泛型类型,可以获得元数据中泛型泛型签名类型(泛型真实类型)。
  1. GenericArrayType
  • 当需要描述的类型是泛型类的数组时,比如List[],Map[],此接口会作为Type的实现。
  1. WildcardType
  • 通配符泛型,获得上下限信息。
public class TestType <K extends Comparable & Serializable, V> {
    K key;
    V value;
    public static void main(String[] args) throws Exception {
        // 获取字段的类型
        Field fk = TestType.class.getDeclaredField("key");
        Field fv = TestType.class.getDeclaredField("value");

        TypeVariable keyType = (TypeVariable)fk.getGenericType();
        TypeVariable valueType = (TypeVariable)fv.getGenericType();
        // getName 方法
        System.out.println(keyType.getName());                 // K
        System.out.println(valueType.getName());               // V
        // getGenericDeclaration 方法
        System.out.println(keyType.getGenericDeclaration());   // class com.test.TestType
        System.out.println(valueType.getGenericDeclaration()); // class com.test.TestType
        // getBounds 方法
        System.out.println("K 的上界:");                        // 有两个
        for (Type type : keyType.getBounds()) {                // interface java.lang.Comparable
            System.out.println(type);                          // interface java.io.Serializable
        }
        System.out.println("V 的上界:");                        // 没明确声明上界的, 默认上界是 Object
        for (Type type : valueType.getBounds()) {              // class java.lang.Object
            System.out.println(type);
        }
    }
}

gson反序列化

//泛型类型或者泛型接口的实现类,会把泛型具体化
TypeRefrence<T> type = new TypeRefrence<Response<Data>>(){}.getType()
//相当于创建了一个TypeRefrence<T>的子类对象,里面记录了Response<Data>
    static class childTypeRefrence{
        Response<Data> t;
    }

(三)、Retrofit中的注解反射与动态代理

1.静态代理
  1. 通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来的不必要复杂性。
  2. 通过代理对象对访问进行控制

代理模式一般有三个角色:

  • 抽象角色:指代理角色和真实角色对外提供的公共方法,一般为一个接口。
  • 真实角色
  • 代理角色
2.动态代理

流程:

  • 创建公共接口(抽象角色)
  • 创建实现公共接口的实现类(真实角色)
  • 使用Proxy.newProxyInstance()获取到动态代理对象(代理角色)
  • 通过代理对象调用方法

原理:

Proxy.newProxyInstance()会创建一个动态代理类(这个类不是文件,而是byte[]),然后这个类会实现公共接口,并在调用接口的方法时,通过反射拿到真实角色实现的方法,以及在代理对象调用的方法时拿到的参数,一并通过InvocationHandler接口的invoke方法回调出去。

package org.example.inter;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class UserService$Proxy0 extends Proxy implements UserService {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public UserService$Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            //h就是InvocationHandler
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void addUser(String var1) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            //通过反射拿到真实角色的方法
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("org.example.inter.UserService").getMethod("addUser", Class.forName("java.lang.String"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

3.作业

利用反射、注解和动态代理实现Onclick事件的自动注入。

作业答案在Android项目 JavaStudy中的homework_3.

(四)、JVM内存管理

1.java从编译到执行的过程
java程序从编译到执行.png

Java文件经过编译后变为.class文件,字节码文件通过类加载器被搬运到JVM中。

虚拟机主要的五大块:

(线程共享)

  • 方法区

(线程独享)

  • 本地方法栈

  • 计数器

JVM调优主要就是围绕堆和栈两大块进行。

-Xms java虚拟机初始化时堆内存的大小

-Xmx java虚拟机可以使用的最大堆内存的大小

一般情况下,都是将这两个参数设为一致的,让JVM有一个稳定的堆内存。

2.JVM概述

JVM的目标是提供一种基于抽象规格描述的计算机模型,为解释程序人员提供的任何系统上运行,JVM定义了控制java代码解释执行和具体实现的五种规格:

  • JVM指令系统
  • JVM寄存器
  • JVM栈结构
  • JVM碎片回收堆
  • JVM存储区
(1).指令系统

JVM采用“big endian"的编码方式。

Java指令由操作码(8位二进制数)+ 操作数(长度根据需求不同) 组成。

当操作数大于8位时,就会采用“big endian" 来处理,即高位bits存放在低字节中。

(2).寄存器

JVM只设置了了 4 个最常用的寄存器:

  • PC程序寄存器
  • optop 操作数栈顶指针
  • frame 当前执行环境指针
  • vars 指向当前执行环境中第一个局部变量的指针

所有的寄存器都为32位。

PC用于记录程序的执行,optop、frame和vars用于记录指向Java栈区的指针。

(3).栈结构

作为基于栈结构的虚拟机,Java栈是JVM存储信息的主要方法。

Java虚拟机的栈有三个区域:

  • 局部变量区

用于存储一个类的方法中所用到的局部变量。vars寄存器指向该变量表中的第一个局部变量。

  • 运行时环境区

在运行环境中包含的信息用于动态链接,正常的方法返回以及异常的传播

  • 操作数栈区

机器指令只从操作数栈中取操作数,对他们进行操作,并把结果返回到栈中。选择栈结构的原因:在只有少量寄存器或非通用寄存器(如 Intel 486)上,也能够高效地模拟虚拟机行为。操作数栈是32位的。

(4).碎片回收堆

Java类的实例所需要的存储空间是在堆上分配的,解释器在为一个实例分配完存储空间后,便开始记录对该实例所占用空间的内存区域的使用。一旦对象使用完毕,便将其回收到堆中。对内存进行释放和回收的工作是由Java运行系统承担的。这允许Java运行系统的设计者自己决定碎片回收的方法。

(5).存储区

JVM有两类存储区:

  • 常量缓冲池

    常量缓冲池用于存储类名称,方法和字段名称及串常量

  • 方法区

    方法区则是用于存储Java方法的字节码

3.类加载器
(1).类加载器的流程
类加载顺序.png

加载、(验证、准备、解析)统称连接、初始化、使用、卸载

(2).类加载器的加载顺序
双亲委派机制.png

双亲委派机制:

只有当父类加载器都没有找到加载所需的class时,子类加载器才会自行尝试加载。

优势:加载位于rt.jar包中的类时,不管是哪个加载器加载,最终都会委托到BootStrap ClassLoader 进行加载,这样保证了java的核心类库不会被篡改。

4.运行时数据区
(1).运行时数据区的构成
JVM运行时数据区.png
  • 本地方法栈

用于存放native方法

  • 程序计数器

其实就是一个指针,它指向了我们程序下一句要执行的指令,它也是内存区域中唯一一个不会出现OutOfMemoryError的区域。(如果执行的是native方法,这个指针就不工作了)

  • 方法区

方法区主要的作用是存储类的元数据、常量和静态变量等。当它存储的信息过大时,会在无法满足内存分配时报错。

  • 虚拟机栈和虚拟机堆

栈管运行,堆管存储。栈负责运行代码,堆负责存储数据。

在JVM中的栈帧就是Java中的方法。

<font color=Red>因为堆和栈属于线程共享区域,不会因为线程的结束而回收,所以垃圾收集器所关注的都是这两部分的内存</font>

(2).JVM内存划分
JVM内存划分.jpg

JVM内存划分为堆内存和非堆内存:

  • 堆内存

永久代,在JDK1.8后被移除,被元空间(MetaSpace)替代。最大的区别就是MetaSpace不在JVM中,它使用的是本地内存。

MateSpaceSize:初始化元空间大小,控制发生GC。

MaxMateSpaceSize:限制元空间的大小,防止占用过多的物理内存。

  • 非堆内存

非堆内存被移除的原因:融合HotSpot JVM 和 JRockit VM 而做出的改变,因为JRockit 是没有永久代的,不过也间接的解决了永久代的OOM问题

5.垃圾回收算法
  • 标记清除算法

    标记扫描一遍,清除扫描一遍,回收效率低,同时会产生大量的不连续内存碎片(适用于老年代)

  • 复制算法 (适用于新生代)

  • Appel式回收

    一种更加优化的复制回收分代策略:具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To,也可以叫做 Survivor1 和Survivor2)。

    专门研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较

    小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。

    比例为 8:1:1

  • 标记整理算法

    首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端

    边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低

  • 分代收集算法

    1. jdk1.8及之前使用的垃圾回收器为:
    • Paraller Scavenge (新生代) 复制算法
    • Paraller Old (老年代) 标记整理算法
    1. jdk9开始,G1收集器成为默认的垃圾收集器:
    • G1 (新生代 + 老年代)复制算法 + 标记整理算法
6.常见的垃圾收集器
JVM中常见的垃圾收集器.png
(1).常见的垃圾收集器
  • Serial (新生代) 复制算法 单线程(串行)
  • Serial Old (老年代) 标记整理算法 单线程(串行)
  • Paraller Scavenge (新生代) 复制算法 多线程(并发)
  • Paraller Old (老年代) 标记整理算法 多线程(并发)
  • ParNew (新生代) 复制算法 多线程(并发)
  • CMS (老年代) 标记清除算法 多线程(并发)
  • G1 (跨新生代和老年代) 标记整理 + 化整为零 多线程(并发)
(2).垃圾回收算法的选择策略
  1. 在新生代中,每次垃圾回收时都发现有大批的对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就能完成回收。
  2. 老年代中因为对象存活率搞,没有额外的空间对他们进行分配担保,就必须使用“标记-整理”或者“标记-清除”算法来进行回收。
(3).Stop The World(STW)(重点)

<font color=Red> 单线程进行垃圾回收时,必须暂停所有的工作线程,直到它回收结束。这个暂停称之为“Stop The World”,但是这种 STW 带来了恶劣的用户体验,例如:应用每运行一个小时就需要暂停响应 5 分。这个也是早期 JVM 和 java 被 C/C++语言诟病性能差的一个重要原因。所以 JVM 开发团队一直努力消除或降低 STW的时间。</font>

(4).吞吐量

吞吐量就是CPU用于运行用户代码的时间与CPU总耗时的比值

(5).动态机制

动态扩容会引发GC,同时缩容JVM也要处理,所以尽量将 -Xms 和 -Xmx 设置为相同的值,获取一个稳定的堆空间。

(6).CMS垃圾回收器详解(Concurrent Mark Sweep)

CMS是基于“标记—清除”算法实现的.

  1. 回收过程

    • 初始标记-短暂,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。

    • 并发标记-和用户的应用程序同时进行,进行 GC Roots 追踪的过程,标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)

    • 重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标

      记阶段稍长一些,但远比并发标记的时间短。
      
    • 并发清除-由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

  2. 问题

    • CPU 敏感:CMS 对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大。

    • 浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。

    • 会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片。

      碎片带来了两个问题:

      1、空间分配效率较低:如果是连续的空间 JVM 可以通过使用指针碰撞的方式来分配,而对于这种有大量碎片的空闲链表则需要逐个访问空闲列表中的项来访问,查找可以存放新建对象的地址。

      2、空间利用效率变低:新生代晋升的对象大小大于了连续空间的大小,即使整个 Old 区的容量是足够的,但由于其不连续,也无法存放新对象。就是内存碎片导致的 Promotion Failed,Young GC 以为 Old 有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。

    • 垃圾回收器退化

      如果发生了,Promotion Failed,那么 CMS 会退化,单线程串行 GC 模式,一般会使用 Serial Old ,因为 Serial Old 是一个单线程,所以如果内存空间很大、

      且对象较多时,CMS 发生这样情况会很卡。

      Serial 使用使用标记整理算法,单线程全暂停的方式,对整个堆进行垃圾收集,暂停时间要长于 CMS。

  3. 总结

    CMS 问题比较多,所以现在没有一个版本默认是 CMS,只能手工指定。但是它毕竟是第一个并发垃圾回收器,对于了解并发垃圾回收具有一定意义,所

    以我们必须了解。

    为什么 CMS 采用标记-清除,在实现并发的垃圾回收时,如果采用标记整理算法,那么还涉及到对象的移动(对象的移动必定涉及到引用的变化,这个需

    要暂停业务线程来处理栈信息,这样使得并发收集的暂停时间更长),所以使用简单的标记-清除算法才可以降低 CMS 的 STW 的时间。该垃圾回收器适合回收堆空间几个 G~ 20G 左右。

7.JVM中对象创建的过程
(1).对象的创建过程
JVM对象创建过程.png
  • 指针碰撞:如果Java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所而分配的内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这样分配的方式称为指针碰撞。

  • 空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空闲内存是相互交错的,就没办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存是可用的,在分配的时候从列表中找出一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配的方式称为空闲列表。

  • CAS加载失败重试机制:

CAS机制.png
  • 分配缓冲:

另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread

Local Allocation Buffer,TLAB),JVM 在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个 Buffer,如果

需要分配内存,就在自己的 Buffer 上分配,这样就不存在竞争的情况,可以大大提升分配效率,当 Buffer 容量不够的时候,再重新从 Eden 区域申请一块

继续使用。

TLAB 的目的是在为新对象分配内存空间时,让每个 Java 应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。

TLAB 只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个 TLAB 用满(分

配指针 top 撞上分配极限 end 了),就新申请一个 TLAB。

(2).对象的内存布局
对象的内存布局.png
8.垃圾回收机制
(1).可达性分析(面试重点)

通过可达性分析来判定对象是否存活。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过ed路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,需要回收。

作为 GC Roots 的对象包括下面几种(重点是器前面4种):

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,各个线程调用方法堆中使用到的参数、局部变量、临时变量等。
  • 方法区中类静态属性引用的对象,比如字符串常量池里的引用。
  • 本地方法栈中jni引用的对象。
  • JVM的内部引用(class对象、异常对象、NullPointException、OutOfMemoryError、系统类加载器)。(非重点)
  • 所有被同步锁(synchronized)持有的对象。(非重点)
  • JVM实现中的临时性对象,跨代引用的对象。(非重点)
(2).class回收

注意class要被回收,条件比较苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制)。

  • 该类的所有实例都已经被回收,也就是堆中不存在该类的任何实例。

  • 加载该类的ClassLoader已经被回收

  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  • 参数控制:

-Xnoclassgc 禁用类的垃圾收集(GC)。这样可以节省一些GC时间,从而缩短了应用程序运行期间的中断时间。

(3).各种引用
  1. 强引用:使用 new 关键字建立的引用,只要有强引用关联还在,垃圾回收器就永远不会回收被引用的对象
  2. 软引用(SoftReference):一些有用但是非必须,用软引用关联的对象,系统将要发生内存溢出(OOM)之前,这些对象就会被回收(如果这次回收还是没有足够的空间,才会抛出OOM)
  3. 弱引用(WeakReference):一些有用(程度比软引用更低)但是并非必须。用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。
  4. 虚引用(PhantomReference):幽灵引用,最弱,随时会被回收掉。
(4).对象的分配策略
对象的分配策略.png
  1. 栈上分配对象:

    • 逃逸分析的原理:**分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用。

      比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。

      从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。

      <font color = Red>如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高 JVM 的效率。</font>

      • <font color = Greend>如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。采用了逃逸分析后,满足逃逸的对象在栈上分配</font>

      <font color = Red>不过逃逸分析的触发前提条件必须触发JIT执行.</font>

    • 解释执行与 JIT

      Java 程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种就是解释执行。

      那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码,如果按照解释执行,效率是非常低的。

      以上的这些代码称为热点代码。

      所以,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。

      完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。

      在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执

      行次数,如果执行次数超过一定的阈值就认为它是“热点方法”

      虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这

      两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。

      方法调用计数器的默认阈值在 客户端模式下是 1500 次,在服务端模式下是 10000 次(我们用的都是服务端,java –version 查询),可通过 -XX:CompileThreshold 来设定

  2. 大对象直接进入老年代

    大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。

    大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到- -群“朝生夕灭”的“短命大对象”,我们写程序

    的时候应注意避免。

    在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们。

    而当复制对象时,大对象就意味着高额的内存复制开销.

  3. 对象优先在Eden区分配

    大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC。

  4. 长期存活对象进入老年区

    HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。

    如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor区中每熬过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。

  5. 对象年龄动态判断

    JVM 通过 -XX:MaxTenuringThreshold 参数来控制晋升年龄,每经过一次 GC,年龄就会加一,达到最大年龄就可以进入 Old 区,最大值为 15(因为 JVM 中使用 4 个比特来表示对象的年龄)。设定固定的MaxTenuringThreshold 值作为晋升条件。会诞生以下问题:

    1、MaxTenuringThreshold 如果设置得过大,原本应该晋升的对象一直停留在 Survivor 区,直到 Survivor 区溢出,一旦溢出发生,Eden + Survivor 中对象将不再依据年龄全部提升到 Old 区,这样对象老化的机制就失效了。

    2、MaxTenuringThreshold 如果设置得过小,过早晋升即对象不能在 Young 区充分被回收,大量短期对象被晋升到 Old 区,Old 区空间迅速增长,引起

    频繁的 Major GC,分代回收失去了意义,严重影响 GC 性能。

    解决方案

    为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

  6. 空间分配担保

    在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历

    次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的,如果担保失败则会进行一次 Full GC;如果小

    于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。

9.垃圾回收基础知识
  1. 什么是GC

自动化的垃圾回收机制

  1. 为什么需要了解GC和内存分配策略

1、面试需要

2、GC 对应用的性能是有影响的;

3、写代码有好处

  1. 栈:栈中的生命周期是跟随线程,所以一般不需要关注

  2. 堆:堆中的对象是垃圾回收的重点

  3. 方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点

  4. 分代回收理论

当前商业虚拟机的垃圾回收器,大多遵循“分代收集”的理论来进行设计,这个理论大体上是这么描述的:

1、 绝大部分的对象都是朝生夕死。

2、 熬过多次垃圾回收的对象就越难回收。

根据以上两个理论,朝生夕死的对象放一个区域,难回收的对象放另外一个区域,这个就构成了新生代老年代

  1. GC分类

1、 新生代回收(Minor GC/Young GC):指只是进行新生代的回收。

2、 老年代回收(Major GC/Old GC):指只是进行老年代的回收。目前只有 CMS 垃圾回收器会有这个单独的回收老年代的行为。(Major GC 定义是比较混乱,有说指是老年代,有的说是做整个堆的收集,这个需要你根据别人的场景来定,没有固定的说法)

3、 整堆回收(Full GC):收集整个 Java 堆和方法区(注意包含方法区)

10.JVM调优
(1).堆空间如何设置

可以通过GC日志中Full GC 之后老年代数据大小得出,比较准确的方法是程序稳定之后,多次获取GC数据,通过平均值的方式计算活跃数据的大小。

(2).扩容新生代能够提高GC效率吗?

如果应用存在大量的短期对象,扩容新生代能够提高效率,如果存在相对较多的持久对象,老年代应该适当增大。

(3).JVM是如何避免Minor GC时扫描全堆的?

新生代 GC 和老年代的 GC 是各自分开独立进行的。

新生代对象持有老年代中对象的引用,老年代也可能持有新生代对象引用,这种情况称为“跨代引用”。

因它的存在,所以 Minor GC 时也必须扫描老年代。

JVM 是如何避免 Minor GC 时扫描全堆的?

经过统计信息显示,老年代持有新生代对象引用的情况不足 1%,根据这一特性 JVM 引入了卡表(card table)来实现这一目的。

卡表.png

卡表的具体策略是将老年代的空间分成大小为 512B 的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示,卡表 3 被标记为脏,之后 Minor GC 时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。

11.常量池
(1).Class常量池(静态常量池)

在class文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期间生成的各种字面量符号引用

  1. 字面量:给基本类型变量赋值的方式就叫作字面量或者字面值。

    如:String a = "b";这里"b"就是字符串字面量,同类推理,还有整数字面量,浮点类型字面量,字符字面量。

  2. 符号引用:符号引用以一组符号来描述所引用的目标。

    在编译期间,虚拟机并不知道所引用的类的实际地址,就用符号音用来代替,而在类的解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。

(2).运行时常量池

运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量;从编译期可知的数值字面量到必须运行期间解析后才能获得的方法或字段引用

(3).字符串常量池

以JDK1.8 为例,字符串常量池是存放在堆中,并且与java.lang.String类有很大关系。设计这块内存区域的原因在于:String对象作为java语言中重要的数据类型,是内存中占据空间最大的一个对象。高效的使用字符串,可以提升系统的整体性能。

12.String类分析
(1).String对象

String对象是对char数组进行了封装实现的对象,主要有两个成员变量:char[]、hash值。

String对象的不可变性:因为 char[] 是被 private 和 final 修饰的,所以String对象一旦创建,就不能对它进行改变。

这样做的好处:

  • 保证String对象的安全性。假设String对象是可变的,那么String对象就可能被恶意篡改。
  • 保证hash值不会频繁的变更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 Key - Value 缓存功能
  • 可以实现字符串常量池。在java中,通常有两种创建字符串对象的方式,一种是 String str = "abc" ; 另一种是字符串变量通过 new 形式的创建,String str = new String("abc");

intern 方法

String的intern方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用。

13.总结图
JVM内存模型.jpg

(五)、Android虚拟机和Android类加载机制

1.JVM与Dalvik

Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例。

Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。

Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基于寄存器的,而后者的指令集是基于堆栈的。

  • JVM -> JavaBytecode -> .class file
  • Dalvik -> Dalvik Bytecode -> .dex file
(1).基于栈的虚拟机

对于基于栈的虚拟机来说,每一个运行时的线程,都有一个独立的栈。栈中记录了方法调用的历史,每有一次方法调用,栈中便会多一个栈桢。最顶部的栈桢称作当前栈桢,其代表着当前执行的方法。基于栈的虚拟机通过操作数栈进行所有操作。

(2).基于寄存器的虚拟机

基于寄存器的虚拟机中没有操作数栈,但是有很多虚拟寄存器。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。

与JVM版相比,可以发现Dalvik版程序的指令数明显减少了,数据移动次数也明显减少了。

<font color=Red> 寄存器:CPU的组成部分。寄存器是有限存储容量的高速存储部件,它们可以用来暂存指令、数据和位址。</font>

(3).ART与Dalvik
  • Dalvik

Dalvik虚拟机执行的是dex字节码,解释执行。从Android 2.2版本开始,支持

JIT 即时编译在程序运行的过程中进行选择热点代码(经常执行的代码)进行编译或者优化。

Dalvik下应用在安装的过程,会执行一次优化,将dex字节码进行优化生成odex文件

  • ART

而ART(Android Runtime) 是在 Android 4.4 中引入的一个开发者选项,也是 Android 5.0 及更高版本的默认 Android 运行时。ART虚拟机执行的是本地机器码。Android的运行时从Dalvik虚拟机替换成ART虚拟机,并不要求开发者将自己的应用直接编译成目标机器码,APK仍然是一个包含dex字节码的文件。

Art将应用的dex字节码翻译成本地机器码的最恰当AOT时机也就发生在应用安装的时候。ART 引入了预编译机制,在安装时,ART 使用设备自带的 dex2oat 工具来编译应用,dex中的字节码将被编译成本地机器码。

ART与Dalvik.png
2.Android N(7.0) 的运作方式

ART 使用预先 (AOT) 编译,并且从 Android N混合使用AOT编译,解释和JIT。

  1. 最初安装应用时不进行任何 AOT 编译(安装又快了),运行过程中解释执行,对经常执行的方法进行JIT,经过 JIT 编译的方法将会记录到Profile配置文件中。
  2. 当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行 AOT 编译。待下次运行时直接使用。
3.Android类加载机制
  1. 某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父

    类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。

    优势:

    • 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
    • 安全性考虑,防止核心API库被随意篡改。
  2. 热修复:根据类加载原理,可以实现热修复

    Android类加载机制.png

(六)、Java IO在Android中的应用

1.IO简介

数据流是一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。

流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:

  • 字节流:数据流中最小的数据单元是字节
  • 字符流:数据流中最小的数据单元是字符

Java中的字符是Unicode编码,一个字符占用两个字节。

Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable。掌握了这些就掌握了Java I/O的精髓了。

Java I/O主要包括如下3层次:

  • 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等

  • 非流式部分——如:File类、RandomAccessFile类和FileDescriptor等类

  • 其他——文件读取部分的与安全相关的类,如:SerializablePermission安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。

IO体系.png
2.IO详细介绍
(1).字节流
字节流.png
(2).字符流
字符流.png
(3).字节输入流和字符输入流的关系
字节输入流和字符输入流.png
(4).字节输出流和字符输出流的关系
字节输出流和字符输出流.png

(七)、IO实践之dex文件加密

1.反编译

定义:利用编译程序从源语言编写的源程序产生目标程序的过程。

流程:

Zip 文件解压apk --(dex2jar)--> 将class.dex转变为jar包 --(jd-gui) --> 看class文件

2.dex加固的总体框架
dex加固总体框架.png
3.Dex文件结构
Dex文件结构.png
4.APK打包流程
APK打包流程.png

(八)、Android数据序列化和反序列化

1.概念
  • 序列化:将数据结构或对象转换成二进制的过程
  • 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或对象的过程
2.Serializable 接口

Java提供的一个空接口。

Serializable 用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream反序列化。

Serializable 的特点:

  • 可序列化类中,未实现 Serializable 的属性状态无法被序列化/反序列化。

  • 也就是说,反序列化一个类的过程中,它的非可序列化的属性将会调用无参构造函数重新创建。

  • 因此这个属性的无参构造函数必须可以访问,否者运行时会报错。

  • 一个实现序列化的类,它的子类也是可序列化的。

SerialVersionUID与兼容性:

  • serialVersionUID的作用:

    serialVersionUID 用来表明类的不同版本间的兼容性。如果你修改了此类, 要修改此值。否则以前用老版本的类序列化的类恢复时会报错: InvalidClassException。

  • 设置方式:

    在JDK中,可以利用JDK的bin目录下的serialver.exe工具产生这个serialVersionUID,对于Test.class,执行命令:serialver Test。

  • 兼容性问题

    为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入 private static finallong serialVersionUID这个属性,具体数值自己定义。这样,即使某个类在与之对应的对象 已经序列化出去后做了修改,该对象依然可以被正确反序列化。否则,如果不显式定义该属性,这个属性值将由JVM根据类的相关信息计算,而修改后的类的计算 结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。不显式定义这个属性值的另一个坏处是,不利于程序在不同的JVM之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是因为JVM不同,出现因类版本不兼容而无法正确反序列化的现象出现。

    因此 JVM 规范强烈 建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private和final的,尽量保持不变。
    如果父类没有实现Serializable接口,则需要在子类中重写则需要重写writeObject(ObjectOutputStream out)readObject(ObjectInputStream in)方法,在里面添加你需要序列化的父类属性,注意顺序。同时父类必须有一个无参的构造函数,否则会报错

public class ParentStudent {
   public String parentName;

   public Integer parentAge;

   //这个构造函数必须要有,否则会报错
   public ParentStudent(){}

   public ParentStudent(String parentName,Integer parentAge){
       this.parentAge = parentAge;
       this.parentName = parentName;
   }

   @Override
   public String toString() {
       return "name:" + parentName + ",age:" + parentAge;
   }

   public String getParentName() {
       return parentName;
   }

   public void setParentName(String parentName) {
       this.parentName = parentName;
   }

   public Integer getParentAge() {
       return parentAge;
   }

   public void setParentAge(Integer parentAge) {
       this.parentAge = parentAge;
   }
}


public class Student extends ParentStudent implements Serializable {

   private static final long serialVersionUID = 12345678919L;
   public String name;
   public String sax;
   public Integer age;

   //Course也需要实现Serializable接口
   public List<Course> courses;

   public Course course;

//    public Teacher teacher;

   //用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient 变量的值被
   //设为初始值,如 int 型的是 0,对象型的是 null)
   public transient Date createTime;

   //静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也
   //就是它的成员变量,因此序列化不会关注静态变量)
   private static SimpleDateFormat simpleDateFormat = new
           SimpleDateFormat();

   public Student(){
       System.out.println("Student:empty");
   }

   //自定义序列化过程
   //如果父类没有实现Serializable接口,则需要重写writeObject(ObjectOutputStream out)和readObject(ObjectInputStream in)
   //方法,在里面添加你需要序列化的属性,注意顺序。
   //父类必须有一个无参的构造函数,否则会报错
   private void writeObject(ObjectOutputStream out) throws IOException {
       out.defaultWriteObject();
       out.writeObject(getParentName());
       out.writeInt(getParentAge());
   }

   private void readObject(ObjectInputStream in) throws  IOException,ClassNotFoundException{
       in.defaultReadObject();
       setParentName((String)in.readObject());
       setParentAge(in.readInt());
   }

   public Student(String name,String sax,Integer age){
       System.out.println("Student: "+ name + " " + sax + " "+age);
       this.name = name;
       this.sax = sax;
       this.age = age;

       parentAge = 56;
       parentName = "Oliver";

       course = new Course();
       course.setName("AGT");
       course.setScore(3.0f);

       courses = new ArrayList<>();
       courses.add(new Course());
       courses.add(new Course());
       createTime = new Date(10L);
       createTime.getTime();
   }
}
3.Externalizable

Externalizable 接口的用法:

将要序列化的类实现Externalizable接口,在writeExternal(ObjectOutput out) 和

readExternal(ObjectInput in)方法中写上你需要序列化的属性,注意属性的读写顺序保持一致。

<font color = Red>注意:在使用Externalizable接口时,必须要有一个无参的构造函数,否则会报错. </font>

package org.example.externalizable;

import org.example.serializable.Course;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class Student implements Externalizable {
    public String name;
    public String sax;
    public Integer age;

    //Course也需要实现Serializable接口
    public List<Course> courses;

    public Course course;


    //用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient 变量的值被
    //设为初始值,如 int 型的是 0,对象型的是 null)
    public transient Date createTime;

    //静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也
    //就是它的成员变量,因此序列化不会关注静态变量)
    private static SimpleDateFormat simpleDateFormat = new
            SimpleDateFormat();

    //使用Externalizable接口时,必须要有一个无参的构造函数
    public Student(){
        System.out.println("Student:empty");
    }

    public Student(String name, String sax, Integer age){
        System.out.println("Student: "+ name + " " + sax + " "+age);
        this.name = name;
        this.sax = sax;
        this.age = age;

        course = new Course();
        course.setName("AGT");
        course.setScore(3.0f);

        courses = new ArrayList<>();
        courses.add(new Course());
        courses.add(new Course());
        createTime = new Date(10L);
        createTime.getTime();

    }

    //注意属性的顺序
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(course);
        out.writeObject(name);
        out.writeObject(sax);
        out.writeInt(age);
        out.writeObject(courses);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        course = (Course) in.readObject();
        name = (String) in.readObject();
        sax = (String) in.readObject();
        age = in.readInt();
        courses = (List<Course>) in.readObject();
    }
}

4.序列化的流程
序列化的流程.png
5.单例模式序列化

单例模式序列化会导致单例模式生效,需要在单例类中重写

 private Object readResolve(){
       return SingleHodle.INSTANCE;
   }

在readResolve()方法中返回单例对象,这样就可以解决反序列化后单例对象不一致的问题。

6.Parcelable接口
(1).Parcelable接口简介

Parcelable是Android为我们提供的序列化的接口,Parcelable相对于Serializable的使用相对复杂一些,但Parcelable的效率相对Serializable也高很多,这一直是Google工程师引以为傲的,有时间的可以看一下Parcelable和Serializable的效率对比 Parcelable vs Serializable 号称快10倍的效率

Parcelable是Android SDK提供的,它是基于内存的,由于内存读写速度高于硬盘,因此Android中的跨进程对象的传递一般使用Parcelable

(2).简单使用
public class Course implements Parcelable {
    private String name;
    private float score;
    ...
    /**
    * 描述当前 Parcelable 实例的对象类型
    * 比如说,如果对象中有文件描述符,这个方法就会返回上面的
    CONTENTS_FILE_DESCRIPTOR
    * 其他情况会返回一个位掩码
    * @return
    */
    @Override
    public int describeContents() {
        return 0;
    }
    /**
    * 将对象转换成一个 Parcel 对象
    * @param dest 表示要写入的 Parcel 对象
    * @param flags 示这个对象将如何写入
    */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.name);
        dest.writeFloat(this.score);
    }
    protected Course(Parcel in) {
        this.name = in.readString();
        this.score = in.readFloat();
    }
    /**
    * 实现类必须有一个 Creator 属性,用于反序列化,将 Parcel 对象转换为 Parcelable
    * @param <T>
    */
    public static final Parcelable.Creator<Course> CREATOR = new
    Parcelable.Creator<Course>() {
    //反序列化的方法,将Parcel还原成Java对象
        @Override
        public Course createFromParcel(Parcel source) {
            return new Course(source);
        }
    //提供给外部类反序列化这个数组使用。
    @Override
        public Course[] newArray(int size) {
            return new Course[size];
        }
    };
}
(3).Parcelable和Serializable性能比较总结

首先Parcelable的性能要强于Serializable的原因我需要简单的阐述一下

  • 在内存的使用中,前者在性能方面要强于后者

  • 后者在序列化操作的时候会产生大量的临时变量,(原因是使用了反射机制)从而导致GC的频繁调用,因此在性能上会稍微逊色

  • Parcelable是以Ibinder作为信息载体的.在内存上的开销比较小,因此在内存之间进行数据传递的时候,Android推荐使用Parcelable,既然是内存方面比价有优势,那么自然就要优先选择.

  • 在读写数据的时候,Parcelable是在内存中直接进行读写,而Serializable是通过使用IO流的形式将数据读写入在硬盘上.但是:虽然Parcelable的性能要强于Serializable,但是仍然有特殊的情况需要使用Serializable,而不去使用Parcelable,因为Parcelable无法将数据进行持久化,因此在将数据保存在磁盘的时候,仍然需要使用后者,因为前者无法很好的将数据进行持久化.(原因是在不同的Android版本当中,Parcelable可能会不同,因此数据的持久化方面仍然是使用Serializable)

7.面试题
  1. 反序列化后的对象,需要调用构造函数重新构造吗

    不需要调用构造函数重新构造。具体来说,Java的反序列化机制会通过读取对象的字节流,并根据字节流中的信息来创建对象。当对象被序列化时,Java会将其类的元数据(包括类名、字段等)一同保存在字节流中。在反序列化过程中,Java会根据保存的元数据来创建对象,并将保存的字段值赋给相应的属性。

  2. 序列化与反序列化后的对象是什么关系?

    答:在Java中,==操作符用于比较两个对象的引用是否相等,即判断两个对象是否指向同一个内存地址。而equal方法(equals)用于比较两个对象的内容是否相等,即判断两个对象的属性值是否完全一致。

    当一个对象被序列化后,然后再进行反序列化时,Java会使用反序列化机制创建一个新的对象,并将序列化时保存的属性值赋给该对象。尽管这个新对象的属性值与原始对象序列化前的属性值可能相同,但它们实际上是两个不同的对象,具有不同的内存地址。

    因此,对于序列化和反序列化后的对象,应该使用equals方法来判断它们的内容是否相等。如果重写了equals方法,可以根据自定义的逻辑进行内容比较。默认情况下,equals方法通过比较两个对象的内存地址来判断它们是否相等。

    <font color = Red>有一种特殊情况,那就是枚举类型,枚举类型在序列化和反序列化前后地址都是一样的</font>

  3. Android 为什么要设计 bundle 而不是使用 HashMap 结构?

    答:bundle 内部适用的是 ArrayMap, ArrayMap 相比 Hashmap 的优点是, 扩容方便, 每次扩容是原容量的一半, 在[百量] 级别, 通过二分法查找 key 和 value (ArrayMap 有两个数组, 一个存放 key 的 hashcode, 一个存放 key+value 的 Entry) 的效率要比 hashmap 快很多, 由于在内存中或者 Android 内部传输中一般数据量较小, 因此用 bundle 更为合适

  4. serializableVersionUID 的作用是?

    用于数据的版本控制, 如果反序列化后发现 ID 不一样, 认为不是之前序列化的对象

  5. Android 中 intent/bundle 的通信原理以及大小限制?

    答: Android 中的 bundle 实现了 parcelable 的序列化接口, 目的是为了在进程间进行通讯, 不同的进程共享一片固定大 小的内存, parcelable 利用 parcel 对象的 read/write 方法, 对需要传递的数据进行内存读写, 因此这一块共享内存不能 过大, 在利用 bundle 进行传输时, 会初始化一个 BINDER_VM_SIZE 的大小 = 1 * 1024 * 1024 - 4096 * 2, 即便通过 修改 Framework 的代码, bundle 内核的映射只有 4M, 最大只能扩展到 4M.
    更详细的看leo老师的binder课程

  6. 为何 Intent 不能直接在组件间传递对象而要通过序列化机制?

    答: 因为 Activity 启动过程是需要与 AMS 交互, AMS 与 UI 进程是不同一个的, 因此进程间需要交互数据, 就必须序列化
    更详细的看zero老师的ams课程

  7. 序列化与持久化的关系和区别?

    答: 序列化是为了进程间数据交互而设计的, 持久化是为了把数据存储下来而设计的

(九)、Java序列化之Gson源码解析

1.Json
(1).定义

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式

(2).作用

数据标记、存储、传输

(3).特点
  1. 读写速度快

  2. 解析简单

  3. 轻量级

  4. 独立于语言,平台

  5. 具有自我描叙性

(4).语法

对象是一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值’ 对”之间使用“,”(逗号)分隔。

{
    "name": "英语",
    "score": 78.3
}

数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。

"courses": [
    {
        "name": "英语",
        "score": 78.3
    }
]

值(value)可以是双引号括起来的字符串(string)、数值(number)、 true 、 false 、 null 、对象(object)或者数组(array)。这些结构可以嵌套。

{
    "url": "https://qqe2.com",
    "name": "欢迎使用JSON在线解析编辑器",
    "array": {
        "JSON校验": "http://jsonlint.qqe2.com/",
        "Cron生成": "http://cron.qqe2.com/",
        "JS加密解密": "http://edit.qqe2.com/"
    },
    "boolean": true,
    "null": null,
    "number": 123,
    "object": {
        "a": "b",
        "c": "d",
        "e": "f"
    }
}
2.json解析方式
(1).Android Studio自带org.json解析
//Android原生json解析
private static void createJson(File jsonFile) throws Exception{
    JSONObject student = new JSONObject();
    student.put("name","OrgJson");
    student.put("sax","男");
    student.put("age",23);
    JSONObject course1 = new JSONObject();
    course1.put("name","语文");
    course1.put("score",98.2f);
    JSONObject course2 = new JSONObject();
    course2.put("name","数学");
    course2.put("score",93.2f);
    //实例化一个JSON数组
    JSONArray coures = new JSONArray();
    //将course1添加到JSONArray,下标为0
    coures.put(0,course1);
    coures.put(1,course2);
    //然后将JSONArray添加到名为student的JSONObject
    student.put("courses",coures);
    FileOutputStream fos = new FileOutputStream(jsonFile);
    fos.write(student.toString().getBytes());
    fos.close();
    System.out.println("CreateJson:" + student.toString());
}

 private static void parseJson(File jsonFile) throws Exception{
        FileInputStream fis = new FileInputStream(jsonFile);
        InputStreamReader isr = new InputStreamReader(fis);
        BufferedReader br = new BufferedReader(isr);
        String line;
        StringBuffer sb = new StringBuffer();
        while ((line=br.readLine())!=null){
            sb.append(line);
        }
        fis.close();
        isr.close();
        br.close();

        Student student = new Student();
        //利用JSONObject进行解析
        JSONObject stuJsonObject = new JSONObject(sb.toString());
        //为什么不用getString?
        //optString会在得不到你想要的值时候返回空字符串"",而getString会抛出异常
        String name = stuJsonObject.optString("name","");
        student.setName(name);
        student.setSax(stuJsonObject.optString("sax",""));
        student.setAge(stuJsonObject.optInt("age",0));

        //获取数组
        JSONArray courseJson = stuJsonObject.optJSONArray("courses");
        for (int i = 0; i < courseJson.length(); i++) {
            JSONObject courseJsonObject = courseJson.getJSONObject(i);
            Course course = new Course();
            course.setName(courseJsonObject.optString("name",""));
            course.setScore(courseJsonObject.optFloat("score",0f));
            student.addCourse(course);
        }

        System.out.println("parseJson : " + student);
    }


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

推荐阅读更多精彩内容