Android面试宝典2020-持续更新

  • 一、Java基础
    • 1、java基本数据类型和引用类型
    • 2、object
      • equals和==的区别
      • equals和hashcode的关系?
    • 3、static关键字
      • 3.1.static关键字的用途
      • 3.2.static方法
      • 3.3.static变量
      • 3.4.静态变量和成员变量的区别
      • 3.5. static代码块
    • 4、final关键字
    • 5、String、StringBuffer、StringBuilder
    • 6、异常处理
    • 7、谈谈对java多态的理解?(重要)
    • 8、抽象和接口(重要)
    • 8、内部类,匿名内部类
    • 9、集合框架
      • 9.1.集合和数组的区别:
      • 9.2. 常用集合的分类:
      • 9.3. Collection集合的方法:
      • 9.4. list和set的区别:
      • 9.5. Map
    • 10、IO
    • 11、反射(非常重要)
      • 11.1.思考:Java中创建对象大概有几种方式?
      • 11.2.什么是反射?反射能干什么?反射的优缺点?
      • 11.3.反射的使用
    • 12、引用类型
    • 13、java泛型
  • 二、java进阶
    • 2.1 java多线程(非常重要)
      • 2.1.1. 线程
      • 2.1.2. 线程的状态有哪些?
      • 2.1.3. 线程的状态转换及控制
      • 2.1.4. Java如何正确停止线程
      • 2.1.5 线程池(非常重要)
      • 2.1.6. java锁机制
      • 2.1.7. Java中Volatile关键字(重要)
    • 2.2 jvm
      • 2.2.1. java内存模型
      • 2.2.1. GC机制(重要)
      • 2.2.1. 类加载过程
  • 三、Android基础
    • 3.1.Android系统架构
    • 3.2.四大组件
      • 1. Activity
      • 2. Service
      • 3. BroadcastReceiver
      • 4. ContentProvider
    • 3.3.屏幕适配
      • 3.3.1相关重要概念
      • 3.3.2.屏幕适配:目前最好的适配方案
    • 3.4.Android消息机制
    • 3.5.View的绘制流程(自定义控件步骤)
      • 3.5.1 View绘制流程
      • 3.5.2 自定义View
    • 3.6.事件分发
      • 3.6.1 了解Activity的构成
      • 3.6.2 触摸事件的类型
      • 3.6.3 事件分发流程
    • 3.7.Context
    • 3.8.序列化
    • 3.9.动画
    • 3.10.Android5.0-10.0版本变更及开发适配
  • 四、Android进阶
    • 4.1 Okhttp
    • 4.2 Retrofit
    • 4.3 Android常用图片库对比
    • 4.4 Glide原理+手写图片加载框架思路
    • 4.5 Rxjava常用操作符
    • 4.6 Android IPC机制(跨进程通信)
    • 4.7 Framwork
    • 4.8 插件化
    • 4.9 组件化
    • 4.10 热修复
  • 五、kotlin
    • 5.1.kotlin基础: From Java To Kotlin
    • 5.2.Kotlin 的延迟初始化: lateinit var 和 by lazy
    • 5.3 Kotlin Tips:怎么用 Kotlin 去提高生产力(kotlin优势)
    • 5.4 Kotlin数组和集合
      • 5.4.1 kotlin数组
      • 5.4.2 kotlin集合
    • 5.5 Kotlin集合操作符
    • 5.6 说一下Kotlin的伴生对象(关键字companion)
    • 5.7.Kotlin 顶层函数和属性
    • 5.8 协程(Coroutines=cooperation+routines)
      • 5.8.1 协程是什么
      • 5.8.2 suspend
      • 5.8.3 「挂起」的本质
      • 5.8.4 suspend 的意义?
      • 5.8.5 到底什么是「非阻塞式」挂起?协程真的更轻量级吗?
  • 六、jetpack
  • 七、网络
    • 7.1.http/https
    • 7.2 Tcp和udp
  • 八、设计模式
    • 8.1mvc/mvp/mvvm
    • 8.2 常见设计模式
      • 8.2.1.设计模式的六大原则:
      • 8.2.2.单例模式
      • 8.2.3.建造者模式
      • 8.2.4.责任链模式
      • 8.2.5.观察者模式
      • 8.2.6.代理模式
      • 8.2.7.策略模式
      • 8.2.8.工厂模式
      • 8.2.9.适配器模式
  • 九、性能优化(非常重要)
    • 9.1 性能优化分析工具学习
    • 9.2 布局优化
    • 9.3 线程优化
    • 9.4 网络优化
    • 9.5 Apk 包体优化
    • 9.6 内存优化(非常重要)
      • 9.6.1 Android 内存管理机制
      • 9.6.2 Android的内存泄漏、内存溢出、内存抖动概念
      • 9.6.3 如何避免OOM(内存泄漏优化)。
        • 1.减小对象的内存占用
        • 2.内存对象的重复利用
        • 3.避免对象的内存泄露
      • 9.6.4 常用的内存检查工具。
    • 9.7 电量优化
  • 十、算法题

一、Java基础

1、java基本数据类型和引用类型

  1. 基本数据类型:
    整型:byte,short,int,long
    浮点型:float,double
    字符型:char
    布尔型:boolean
    其中,占一个字节的是byte,short和char占两个字节,int,float占四个字节,double和long占8个字节,boolean只有true和false。
  2. 引用数据类型:
    类、 接口、 数组、 枚举、 注解

例如,String类型就是引用类型,还有Double,Byte,Long,Float,Char,Boolean,Short(注意这里和基本类型相比首字母是大写),简单来说,所有的非基本数据类型都是引用数据类型。

  1. 基本数据类型和引用数据类型区别
    3.1 存储位置
    基本变量类型:在方法中定义的非全局基本数据类型变量的具体内容是存储在栈中的
    引用变量类型:引用数据类型变量,其具体内容都是存放在堆中的,而栈中存放的是其具体内容所在内存的地址
public class Main{        public static void main(String[] args){            //基本数据类型       int i=1;       double d=1.2;              //引用数据类型       String str="helloworld";   }}


3.2 传递方式
基本数据类型:在方法中定义的非全局基本数据类型变量,调用方法时作为参数是按数值传递的
引用数据类型:调用方法时作为参数是按引用传递的

2、object

equals和==的区别

==:基本类型比较值,引用类型比较地址。
equals:对两个对象的地址值进行比较(即比较引用是否相同)

public boolean equals(Object obj) {         return (this == obj);}

equals和hashcode的关系?

equals和hashcode之间的关系:
默认情况下,equals相等,hashcode必相等,hashcode相等,equals不是必相等。hashcode基于内存地址计算得出,可能会相等,虽然几率微乎其微。

3、static关键字

在类中,用static声明的成员变量为静态成员变量,也成为类变量。static修饰的成员方法为静态方法。类变量的生命周期和类相同,在整个应用程序执行期间都有效。

这里要强调一下:

static修饰的成员变量和方法,从属于

普通变量和方法从属于对象

静态方法不能调用非静态成员,编译会报错。

3.1.static关键字的用途

一句话描述就是:方便在没有创建对象的情况下进行调用(方法/变量)。

显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

3.2.static方法

由于静态方法不依赖于任何对象就可以直接访问,因此对于静态方法来说,是没有this的,因为不依附于任何对象,既然都没有对象,就谈不上this了,并且由于此特性,在静态方法中不能访问类的非静态成员变量和非静态方法,因为非静态成员变量和非静态方法都必须依赖于具体的对象才能被调用

虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法和静态成员变量。

特别说明:static方法是属于类的,非实例对象,在JVM加载类时,就已经存在内存中,不会被虚拟机GC回收掉,这样内存负荷会很大,但是非static方法会在运行完毕后被虚拟机GC掉,减轻内存压力。

3.3.static变量

静态变量被所有对象共享,在内存中只有一个副本,在类初次加载的时候才会初始化

非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响

3.4.静态变量和成员变量的区别

(1)所属不同:静态变量属于类,为类变量;成员变量属于对象,称为对象变量
(2)内存中位置不同:静态变量位于方法区中的静态区,成员变量存储于堆内存
(3)成员变量随着实例对象创建而存在,随着实例对象被回收而消失。静态变量随着类的加载而存在,随着类的消失而消失。
(4)调用不同:静态变量可以通过类名调用,也可以通过对象名调用,成员变量只能通过对象名调用

所以,成员变量可以称为对象的特有数据,静态变量称为对象的共享数据。

3.5. static代码块

public class CodeBlock{          static{             System.out.println("静态代码块");  }      }

执行时机:静态代码块在类被加载的时候就运行了,而且只运行一次,并且优先于各种代码块以及构造函数。如果一个类中有多个静态代码块,就会按照书写的顺序执行。

静态代码块的作用:一般情况下,如果有些代码需要在项目启动的时候执行,这时就需要静态代码快,比如一个项目启动需要加载很多配置文件等资源,就可以都放在静态代码块中。

执行顺序:.静态代码块>构造代码块>构造函数

4、final关键字

可以声明成员变量、方法、类以及本地变量
final 成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误
final 变量是只读的
final 申明的方法不可以被子类的方法重写
final 类通常功能是完整的,不能被继承
final 变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销
final 关键字提高了性能,JVM 和 Java 应用都会缓存 final 变量,会对方法、变量及类进行优化
方法的内部类访问方法中的局部变量,必须用 final 修饰才能访问

5、String、StringBuffer、StringBuilder

String 为什么要设计成不可变的?

String是不可变的(修改String时,不会在原有的内存地址修改,而是重新指向一个新对象),String用final修饰,不可继承,String本质上是个final的char[]数组,所以char[]数组的内存地址不会被修改,而且String 也没有对外暴露修改char[]数组的方法。不可变性可以保证线程安全以及字符串串常量池的实现。

三者在执行速度方面的比较:StringBuilder > StringBuffer > String

String每次变化一个值就会开辟一个新的内存空间

StringBuilder:线程非安全的

StringBuffer:线程安全的

对于三者使用的总结:

1.如果要操作少量的数据用 String。

2.单线程操作字符串缓冲区下操作大量数据用 StringBuilder。

3.多线程操作字符串缓冲区下操作大量数据用 StringBuffer。

String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

6、异常处理

Java异常结构中定义有Throwable类。 Exception和Error为其子类。

Exception是程序本身可以处理的异常,这种异常分两大类:运行时异常非运行时异常,程序中应当尽可能去处理这些异常。

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等, 这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的, 程序应该从逻辑角度尽可能避免这类异常的发生。

非运行时异常:是需要显示用try-catch捕捉处理的异常,如IOException等

Error是程序无法处理的错误,比如OutOfMemoryError、StackOverflowError。这些异常发生时, Java虚拟机(JVM)一般会选择线程终止。

Java语言异常处理

  1. 对代码块用try…catch进行异常捕获处理;
finally块没有处理异常的能力。处理异常的只能是catch块。不管有没有异常,finally 中的代码都会执行当 try、catch 中有 return 时,finally 中的代码依然会继续执行
  1. 在方法体外用throws进行抛出声明
public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN{      //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。}

3.在代码块用throw手动抛出一个异常对象

public void save(User user){      if(user  == null)           throw new IllegalArgumentException("User对象为空");      //......}

异常处理的两个基本原则:

  1. 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。
  2. 不要生吞异常。

7、谈谈对java多态的理解?(重要)

多态是指父类的某个方法被子类重写时,可以产生自己的功能行为,同一个操作作用于不同对象,可以有不同的解释,产生不同的执行结果。

多态的三个必要条件:

1.继承父类。2.重写父类的方法。3.父类的引用指向子类对象。

什么是多态

面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。

多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

多态的作用:消除类型之间的耦合关系。

现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。

多态的好处:

1.可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。

2.可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。

3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。

4.灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。

5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

Java中多态的实现方式:接口实现,继承父类进行方法重写,同一个类中进行方法重载。

8、抽象和接口(重要)

抽象类的意义?

为其子类提供一个公共的类型,封装子类中的重复内容,定义抽象方法,子类虽然有不同的实现,但是定义是一致的。

接口的意义?

规范、扩展、回调。

共同点

  1. 是上层的抽象层。
  2. 都不能被实例化。
  3. 都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不提供具体的实现。

区别

  1. 在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,这样可以提高代码的复用性,这是抽象类的优势,接口中只能有抽象的方法。
  2. 多继承:一个类只能继承一个直接父类,这个父类可以是具体的类也可是抽象类,但是一个类可以实现多个接口。
  3. 抽象类可以有默认的方法实现,接口根本不存在方法的实现。
  4. 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明方法的实现。子类使用关键字implements来实现接口。它需要提供接口中所有声明方法的实现。
  5. 构造器:抽象类可以有构造器,接口不能有构造器。
  6. 和普通Java类的区别:除了你不能实例化抽象类之外,抽象类和普通Java类没有任何区别,接口是完全不同的类型。
  7. 访问修饰符:抽象方法可以有public、protected和default修饰符,接口方法默认是public abstract**** 。你不可以使用其它修饰符。接口中的所有属性默认为:public static final ****.
  8. main方法:抽象方法可以有main方法并且我们可以运行它,接口没有main方法,因此我们不能运行它。
  9. 速度:抽象类比接口速度要快,接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
  10. 添加新方法:如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。

8、内部类,匿名内部类

内部类
非静态内部类没法在外部类的静态方法中实例化。
非静态内部类的方法可以直接访问外部类的所有数据,包括私有的数据。
在静态内部类中调用外部类成员,成员也要求用 static 修饰。
创建静态内部类的对象可以直接通过外部类调用静态内部类的构造器;创建非静态的内部类的对象必须先创建外部类的对象,通过外部类的对象调用内部类的构造器。
匿名内部类
匿名内部类不能定义任何静态成员、方法
匿名内部类中的方法不能是抽象的
匿名内部类必须实现接口或抽象父类的所有抽象方法
匿名内部类不能定义构造器
匿名内部类访问的外部类成员变量或成员方法必须用 final 修饰

9、集合框架

所有的集合都在 java.util 包下,java的集合几乎是从Collection 和 map这两个接口中派生出来的,而这两个接口又有一些子类(包括子接口和实现类)

9.1.集合和数组的区别:

9.2. 常用集合的分类:

9.3. Collection集合的方法:

9.4. list和set的区别:


List:

(1)ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
(2)LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
(3)Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素

Set
HashSet
HashSet 是一个没有重复元素的集合。它是由HashMap实现的,不保证元素的顺序(这里所说的没有顺序是指:元素插入的顺序与输出的顺序不一致),而且HashSet允许使用null。但是只允许有一个null元素!

LinkedHashSet
LinkedHashSet继承自HashSet,其底层是基于LinkedHashMap来实现的,有序,非同步。(LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。)

TreeSet
TreeSet是一个有序集合,其底层是基于TreeMap实现的,非线程安全。TreeSet可以确保集合元素处于排序状态。

9.5. Map

Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。

HashMap和HashTable的比较:

TreeMap

HashMap的特点是什么?HashMap的原理?(重点)
HashMap的特点:
1.基于Map接口,存放键值对。
2.允许key/value为空。
3.非多线程安全。
4.不保证有序,也不保证使用的过程中顺序不会改变。

简单来讲,核心是数组+链表/红黑树

HashMap的原理就是存键值对的时候:

通过键的Hash值确定数组的位置。
找到以后,如果该位置无节点,直接存放。
该位置有节点即位置发生冲突,遍历该节点以及后续的节点,比较key值,相等则覆盖。
没有就新增节点,默认使用链表,相连节点数超过8的时候,在jdk 1.8中会变成红黑树。
如果Hashmap中的数组使用情况超过一定比例,就会扩容,默认扩容两倍。

当然这是存入的过程,其他过程可以自行查阅。这里需要注意的是:

key的hash值计算过程是高16位不变,低16位和高16位取抑或,让更多位参与进来,可以有效的减少碰撞的发生。
初始数组容量为16,默认不超过的比例为0.75。

10、IO

IO流的分类:

按照“流”的数据流向,可以将其化分为:输入流输出流

按照“流”中处理数据的单位,可以将其区分为:字节流字符流。在java中,字节是占1个Byte,即8位;而字符是占2个Byte,即16位。而且,需要注意的是,java的字节是有符号类型,而字符是无符号类型!

字节流的抽象基类:
InputStream,OutputStream

字符流的抽象基类:
Reader,Writer

由这四个类派生出来的子类名称都是以其父类名作为子类名的后缀,如InputStream的子类FileInputStream,Reader的子类FileReader。

字节流和字符流的区别

字节流操作的基本单元是字节;字符流是Unicode字符
字节流不使用缓冲区,字符流使用缓冲区
字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取Unicode码元;字符流通常处理文本数据,它支持写入及读取Unicode码元。

IO和NIO(new IO)区别
IO面向流,NIO面向缓冲区
IO是阻塞的,NIO是非阻塞的
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道

11、反射(非常重要)

反射是框架设计的灵魂

11.1.思考:Java中创建对象大概有几种方式?

  1. 使用new关键字:这是我们最常见的也是最简单的创建对象的方式
  2. 使用Clone的方法:无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去
  3. 使用反序列化:当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象
  4. 反射

11.2.什么是反射?反射能干什么?反射的优缺点?

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

如果给定一个类名,就可以通过反射机制来获取类的所有信息,也可以动态的创建对象和编译。一般来说反射是用来做框架的,或者说可以做一些抽象度比较高的底层代码,常用的需求场景有:动态代理、工厂模式优化、Java JDBC数据库操作等。有一句很经典的话:反射是框架设计的灵魂

优点
灵活性高。因为反射属于动态编译,即只有到运行时才动态创建 、获取对象实例。

编译方式说明: 1. 静态编译:在编译时确定类型、绑定对象。如常见的使用new关键字创建对象 2. 动态编译:运行时确定类型、绑定对象。动态编译体现了Java的灵活性、多态特性、降低类之间的藕合性

缺点
执行效率低。 因为反射的操作主要通过JVM执行,所以时间成本 高于直接执行相同操作。

因为接口的通用性,Java的invoke方法是传object和object[]数组的。基本类型参数需要装箱和拆箱,产生大量额外的对象和内存开销,频繁促发GC。编译器难以对动态调用的代码提前做优化,比如方法内联。反射需要按名检索类和方法,有一定的时间开销。

11.3.反射的使用

在使用Java反射机制时,主要步骤包括:

  1. 获取 目标类型的Class对象
  2. 通过 Class 对象分别获取Constructor类对象、Method类对象 、 Field 类对象
  3. 通过 Constructor类对象、Method类对象 、Field类对象分别获取类的构造函数、方法、属性的具体信息,并进行后续操作
    //步骤1:    //获取目标类型的class对象方式1:static method Class.forName 前提:已明确类的全路径名(最常用)    Class cls = Class.forName("com.text.Person");    //获取目标类型的class对象方式2:Object.getClass()  适合有对象示例的情况下    Person person= new Person();     Class<?> cls = person.getClass();     //获取目标类型的class对象方式3:T.class语法   说明:仅适合在编译前就已经明确要操作的 Class    Class<?> classType = Person.class;         //步骤2:// 以下方法都属于Class类的方法。<-- 1. 获取类的构造函数(传入构造函数的参数类型)->>// a. 获取指定的构造函数 (公共 / 继承)Constructor<T> getConstructor(Class<?>... parameterTypes)// b. 获取所有的构造函数(公共 / 继承) Constructor<?>[] getConstructors(); // c. 获取指定的构造函数 ( 不包括继承)Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) // d. 获取所有的构造函数( 不包括继承)Constructor<?>[] getDeclaredConstructors(); // 最终都是获得一个Constructor类对象 // 特别注意:// 1. 不带 "Declared"的方法支持取出包括继承、公有(Public)、不包括有(Private)的构造函数// 2. 带 "Declared"的方法是支持取出包括公共(Public)、保护(Protected)、默认(包)访问和私有(Private)的构造方法,但不包括继承的构造函数// 下面同理 <--  2. 获取类的属性(传入属性名) -->// a. 获取指定的属性(公共 / 继承)Field getField(String name) ;// b. 获取所有的属性(公共 / 继承)Field[] getFields() ;// c. 获取指定的所有属性 (不包括继承)Field getDeclaredField(String name)// d. 获取所有的所有属性 (不包括继承)Field[] getDeclaredFields()// 最终都是获得一个Field类对象 <-- 3. 获取类的方法(传入方法名 & 参数类型)-->// a. 获取指定的方法(公共 / 继承)Method getMethod(String name, Class<?>... parameterTypes)// b. 获取所有的方法(公共 / 继承)Method[] getMethods()// c. 获取指定的方法 ( 不包括继承)Method getDeclaredMethod(String name, Class<?>... parameterTypes)// d. 获取所有的方法( 不包括继承)Method[] getDeclaredMethods()// 最终都是获得一个Method类对象 <-- 4. Class类的其他常用方法 -->getSuperclass(); // 返回父类String getName(); // 作用:返回完整的类名(含包名,如java.lang.String ) Object newInstance(); // 作用:快速地创建一个类的实例// 具体过程:调用默认构造器(若该类无默认构造器,则抛出异常 // 注:若需要为构造器提供参数需使用java.lang.reflect.Constructor中的newInstance()步骤3:通过 Constructor类对象、Method类对象、Field类对象分别获取类的构造函数、方法、属性的具体信息进行操作// 以下方法都分别属于`Constructor`类、Method类、Field类的方法。<-- 1. 通过Constructor 类对象获取类构造函数信息 -->String getName()// 获取构造器名Class getDeclaringClass()// 获取一个用于描述类中定义的构造器的Class对象int getModifiers()// 返回整型数值,用不同的位开关描述访问修饰符的使用状况Class[] getExceptionTypes()// 获取描述方法抛出的异常类型的Class对象数组Class[] getParameterTypes()// 获取一个用于描述参数类型的Class对象数组 <-- 2. 通过Field类对象获取类属性信息 -->String getName()// 返回属性的名称Class getDeclaringClass()// 获取属性类型的Class类型对象Class getType()// 获取属性类型的Class类型对象int getModifiers()// 返回整型数值,用不同的位开关描述访问修饰符的使用状况Object get(Object obj)// 返回指定对象上 此属性的值void set(Object obj, Object value) // 设置 指定对象上此属性的值为value <-- 3. 通过Method 类对象获取类方法信息 -->String getName()// 获取方法名Class getDeclaringClass()// 获取方法的Class对象 int getModifiers()// 返回整型数值,用不同的位开关描述访问修饰符的使用状况Class[] getExceptionTypes()// 获取用于描述方法抛出的异常类型的Class对象数组Class[] getParameterTypes()// 获取一个用于描述参数类型的Class对象数组使用方法:method.invoke(Object obj,Object... args)<--额外:java.lang.reflect.Modifier类 -->// 作用:获取访问修饰符 static String toString(int modifiers)   // 获取对应modifiers位设置的修饰符的字符串表示 static boolean isXXX(int modifiers) // 检测方法名中对应的修饰符在modifiers中的值

12、引用类型

  1. 强引用(FinalReference):在内存不足时不会被回收。平常用的最多的对象,如新创建的对象。
  2. 软引用(SoftReference):在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
  3. 弱引用(WeakReferenc):无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
  4. 虚引用(PhantomReference):虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收.

13、java泛型

1. 为什么要引入泛型?

在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者实际参数类型可以预知的情况下进行的。对于强制类型换错误的情况,编译器可能不提示错误,在运行的时候出现异常,这是一个安全隐患。

2. 说一下对泛型的理解

泛型的本质是参数化类型,在不创建新的类型的情况下,通过泛型指定不同的类型来控制形参具体限制的类型。也就是说在泛型的使用中,操作的数据类型被指定为一个参数,这种参数可以被用在类、接口和方法中,分别被称为泛型类、泛型接口和泛型方法。 Java语言引入泛型的好处是安全简单.
泛型是Java中的一种语法糖,能够在代码编写的时候起到类型检测的作用,但是虚拟机是不支持这些语法的。

泛型的优点:

类型安全,避免类型的强转。提高了代码的可读性,不必要等到运行的时候才去强制转换。

3. 什么是类型擦除?

不管泛型的类型传入哪一种类型实参,对于Java来说,都会被当成同一类处理,在内存中也只占用一块空间。通俗一点来说,就是泛型只作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的信息擦除,也就是说,成功编译过后的class文件是不包含任何泛型信息的。

二、java进阶

2.1 java多线程(非常重要)

2.1.1. 线程

线程和进程的区别?

线程是CPU调度的最小单位,一个进程中可以包含多个线程,在Android中,一个进程通常是一个App,App中会有一个主线程,主线程可以用来操作界面元素,如果有耗时的操作,必须开启子线程执行,不然会出现ANR,除此以外,进程间的数据是独立的,线程间的数据可以共享。

java多线程实现方式主要有:

  1. 继承Thread
    优点 : 方便传参,可以在子类添加成员变量,通过方法设置参数或构造函数传参。
    缺点
    1.因为Java不支持多继承,所以继承了Thread类以后,就无法继承其他类。
    2.每次都要新建一个类,不支持通过线程池操作,创建和销毁线程对资源的开销比较大。
    3.从代码结构上讲,为了启动一个线程任务,都要创建一个类,耦合性太高。
    4.无法获取线程任务的返回结果。

    Thread syncTask = new Thread() {           @Override    public void run() {               // 执行耗时操作    }};syncTask.start();//启动线程
  2. 实现Runnable
    优点 : 此方式可以继承其他类。也可以使用线程池管理,节约资源。创建线程代码的耦合性较低。推荐使用此种方式创建线程。
    缺点: 不方便传参,只能使用主线程中用final修饰的变量。其次是无法获取线程任务的返回结果。

    //写法1:集成Runnable接口定义任务类public class ThreadTask implements Runnable {       @Overridepublic void run() {       while(true) {       System.out.println(Thread.currentThread().getName()+" is running...");try {                   Thread.sleep(1000);        } catch (InterruptedException e) {                   e.printStackTrace();        }}}}//在其他地方使用new Thread(new ThreadTask ()).start(); //写法2:匿名内部类写法new Thread(new Runnable() {       @Override    public void run() {               //做操作                    }}).start();
  3. 实现Callable
    此种方式创建线程底层源码也是使用实现Runnable接口的方式实现的,所以不是一种新的创建线程的方式,只是在实现Runnable接口方式创建线程的基础上,同时实现了Future接口,实现有返回值的创建线程。

    Runnable 与 Callable的区别:

    1. Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的; 2. Runnable 接口之中只提供了一个run()方法,并且没有返回值;3. Callable接口提供有call(),可以有返回值;

    扩展:

    Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞
    public class CallableImpl implements Callable<String> {            public CallableImpl(String acceptStr) {               this.acceptStr = acceptStr;    }     private String acceptStr;     @Override    public String call() throws Exception {               // 任务阻塞 1 秒        Thread.sleep(1000);        return this.acceptStr + " append some chars and return it!";    }      public static void main(String[] args) throws ExecutionException, InterruptedException {               Callable<String> callable = new CallableImpl("my callable test!");        FutureTask<String> task = new FutureTask<>(callable);        long beginTime = System.currentTimeMillis();        // 创建线程        new Thread(task).start();        // 调用get()阻塞主线程,反之,线程不会阻塞        String result = task.get();        long endTime = System.currentTimeMillis();        System.out.println("hello : " + result);        System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!");    }} //执行结果 hello : my callable test! append some chars and return it!cast : 1 second!

    总结

    根据Oracle提供的JAVA官方文档的说明,Java创建线程的方法只有两种方式,即继承Thread类和实现Runnable接口。其他所有创建线程的方式,底层都是使用这两种方式中的一种实现的,比如通过线程池、通过匿名类、通过lambda表达式、通过Callable接口等等,全是通过这两种方式中的一种实现的。所以我们在掌握线程创建的时候,必须要掌握的只有这两种,通过文章中优缺点的分析,这两种方法中,最为推荐的就是实现Runnable接口的方式去创建线程。

2.1.2. 线程的状态有哪些?

Java中定义线程的状态有6种,可以查看Thread类的State枚举:

public static enum State  {         NEW,  RUNNABLE,  BLOCKED,  WAITING,  TIMED_WAITING,  TERMINATED;        private State() {     }  }
初始(NEW):新创建了一个线程对象,还没调用start方法;运行(RUNNABLE):java线程中将就绪(ready)和运行中(running)统称为运行(RUNNABLE)。线程创建后调用了该对象的start方法,此时处于就绪状态,当获得CPU时间片后变为运行中状态;阻塞(BLOCKED):表现线程阻塞于锁;等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定时间后自行返回;终止(TERMINATED):表示该线程已经执行完毕。

状态详细说明

  1. 初始状态(NEW)
    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

  2. 就绪状态(RUNNABLE之READY)
    就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
    调用线程的start()方法,此线程进入就绪状态。
    当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    锁池里的线程拿到对象锁后,进入就绪状态。

    运行中状态(RUNNABLE之RUNNING)
    线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

  3. 阻塞状态(BLOCKED)
    阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

  4. 等待(WAITING)
    处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

  5. 超时等待(TIMED_WAITING)
    处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  6. 终止状态(TERMINATED)
    当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
    在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

2.1.3. 线程的状态转换及控制

主要由这几个方法来控制:sleep、join、yield、wait、notify以及notifyAll。

wait() / notify() / notifyAll()

wait(),notify(),notifyAll() 是定义在Object类的实例方法,用于控制线程状态,三个方法都必须在synchronized 同步关键字所限定的作用域中调用(只能在同步控制方法或者同步控制块中使用),否则会报错 java.lang.IllegalMonitorStateException。

join() / sleep() / yield()

join()

如果线程A调用了线程B的join方法,线程A将被阻塞,等待线程B执行完毕后线程A才会被执行。这里需要注意一点的是,join方法必须在线程B的start方法调用之后调用才有意义。join方法的主要作用就是实现线程间的同步,它可以使线程之间的并行执行变为串行执行。

sleep()

当线程A调用了 sleep方法,则线程A将被阻塞,直到指定睡眠的时间到达后,线程A才会重新被唤起,进入就绪状态。

public class Test {          public static void main(String[] args) {             for (int i = 0; i < 10; i++) {                 System.out.println(Thread.currentThread().getName() + "---" + i);            try {                     Thread.sleep(1000);        // 阻塞当前线程1s            } catch (Exception e) {                     e.printStackTrace();            }        }    }}

yield()  当线程A调用了yield方法,它可以暂时放弃处理器,但是线程A不会被阻塞,而是进入就绪状态。执行了yield方法的线程什么时候会继续运行由线程调度器来决定。

public class YieldThread extends Thread {         @Override    public void run() {             for (int i = 0; i < 10; i++) {                 System.out.println(Thread.currentThread().getName() + "---" + i);            // 主动放弃            Thread.yield();        }    }}

sleep方法和wait方法的区别是什么?

wait方法既释放cpu,又释放锁。 sleep方法只释放cpu,但是不释放锁。

sleep 方法是Thread类的一个静态方法,其作用是使运行中的线程暂时停止指定的毫秒数,从而该线程进入阻塞状态并让出处理器,将执行的机会让给其他线程。但是这个过程中监控状态始终保持,当sleep的时间到了之后线程会自动恢复。

wait 方法是Object类的方法,它是用来实现线程同步的。当调用某个对象的wait方法后,当前线程会被阻塞并释放同步锁,直到其他线程调用了该对象的 notify 方法或者 notifyAll 方法来唤醒该线程。所以 wait 方法和 notify(或notifyAll)应当成对出现以保证线程间的协调运行。

2.1.4. Java如何正确停止线程

注意

Java中线程的stop()、suspend()、resume()三个方法都已经被弃用,所以不再使用stop()方法停止线程。

如何停止线程

我们只能调用线程的interrupt()方法通知系统停止线程,并不能强制停止线程。线程能否停止,何时停止,取决于系统。

2.1.5 线程池(非常重要)

线程池的地位十分重要,基本上涉及到跨线程的框架都使用到了线程池,比如说OkHttp、RxJava、LiveData以及协程等。

与新建一个线程相比,线程池的特点?

节省开销: 线程池中的线程可以重复利用。速度快:任务来了就能开始,省去创建线程的时间。线程可控:线程数量可空和任务可控。功能强大:可以定时和重复执行任务。

ExecutorService简介

通常来说我们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。

线程池(ThreadPoolExecutor)中的几个参数是什么意思?

  public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler) {     //...}

参数解释如下(重要):

corePoolSize:核心线程数量,不会释放。

maximumPoolSize:允许使用的最大线程池数量,非核心线程数量,闲置时会释放。

keepAliveTime:闲置线程允许的最大闲置时间。它起作用必须在一个前提下,就是当线程池中的线程数量超过了corePoolSize时,它表示多余的空闲线程的存活时间,即:多余的空闲线程在超过keepAliveTime时间内没有任务的话则被销毁。而这个主要应用在缓存线程池中

unit:闲置时间的单位。

workQueue:阻塞队列,用来存储已经提交但未被执行的任务,不同的阻塞队列有不同的特性。

threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即可

handler:通常叫做拒绝策略,1、在线程池已经关闭的情况下 2、任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务 。在上面两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个RejectedExecutionException异常

上面的参数理解起来都比较简单,不过workQueue这个任务队列却要再次说明一下,它是一个BlockingQueue<Runnable>对象,而泛型则限定它是用来存放Runnable对象的,刚刚上面讲了,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现了,细心的会发现在刚刚的用来创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样,五种线程池分别用的是什么BlockingQueue:

1、newFixedThreadPool()—>LinkedBlockingQueue  无界的队列2、newSingleThreadExecutor()—>LinkedBlockingQueue  无界的队列3、newCachedThreadPool()—>SynchronousQueue  直接提交的队列4、newScheduledThreadPool()—>DelayedWorkQueue  等待队列5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue  等待队列

实现了BlockingQueue接口的队列还有:ArrayBlockingQueue(有界的队列)、PriorityBlockingQueue(优先级队列)。这些队列的详细作用就不多介绍了。

线程池的种类有哪些:五种功能不一样的线程池

这样创建线程池的话,我们需要配置一堆东西,非常麻烦。所以,官方也不推荐使用这种方法来创建线程池,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池(但底层实现还是通过ThreadPoolExecutor),从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:

newCachedThreadPool() :返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。

newFixedThreadPool() :线程池只能存放指定数量的线程池,线程不会释放,可重复利用。

newSingleThreadExecutor() :单线程的线程池。即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。

newScheduledThreadPool() :可定时和重复执行的线程池。

newSingleThreadScheduledExecutor():同上。和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。

通过Executors的工厂方法来获取

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();ExecutorService cachedThreadPool = Executors.newCachedThreadPool();ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();

通过Executors的工厂方法来创建线程池极其简便,其实它的内部还是通过new ThreadPoolExecutor(…)的方式创建线程池的,我们看一下这些工厂方法的内部实现:

  public static ExecutorService newFixedThreadPool(int nThreads) {             return new ThreadPoolExecutor(nThreads, nThreads,                                      0L, TimeUnit.MILLISECONDS,                                      new LinkedBlockingQueue<Runnable>());    }    public static ExecutorService newSingleThreadExecutor() {             return new FinalizableDelegatedExecutorService            (new ThreadPoolExecutor(1, 1,                                    0L, TimeUnit.MILLISECONDS,                                    new LinkedBlockingQueue<Runnable>()));    }    public static ExecutorService newCachedThreadPool() {             return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                      60L, TimeUnit.SECONDS,                                      new SynchronousQueue<Runnable>());    }

线程池ThreadPoolExecutor的使用

使用线程池,其中涉及到一个极其重要的方法,即:

execute(Runnable command) 

该方法意为执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由ThreadPoolExecutor的实现决定。

五种线程池使用举例

  1. newFixedThreadPool 创建一个固定线程数量的线程池,示例为:
    创建了一个线程数为3的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是3,而我模拟了10个任务让它处理,执行的情况则是首先执行前三个任务,后面7个则依次进入任务队列进行等待,执行完前三个任务后,再通过FIFO的方式从任务队列中取任务执行,直到最后任务都执行完毕。

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);    for (int i = 1; i <= 10; i++) {               final int index = i;        fixedThreadPool.execute(new Runnable() {                    @Override             public void run() {                        String threadName = Thread.currentThread().getName();                 Log.v("zxy", "线程:"+threadName+",正在执行第" + index + "个任务");                 try {                               Thread.sleep(2000);                 } catch (InterruptedException e) {                               e.printStackTrace();                 }             }         });     }

  2. newSingleThreadExecutor
    创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务,示例为:

     ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();        for (int i = 1; i <= 10; i++) {                   final int index = i;            singleThreadPool.execute(new Runnable() {                       @Override                public void run() {                           String threadName = Thread.currentThread().getName();                    Log.v("zxy", "线程:"+threadName+",正在执行第" + index + "个任务");                    try {                               Thread.sleep(1000);                    } catch (InterruptedException e) {                               e.printStackTrace();                    }                }            });        }


    其实我们通过newSingleThreadExecutor()和newFixedThreadPool()的方法发现,创建一个singleThreadExecutorPool实际上就是创建一个核心线程数和最大线程数都为1的fixedThreadPool。

  3. newCachedThreadPool
    创建一个可以根据实际情况调整线程池中线程的数量的线程池,为了体现该线程池可以自动根据实现情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的,示例为

     ExecutorService cachedThreadPool = Executors.newCachedThreadPool();        for (int i = 1; i <= 10; i++) {                   final int index = i;            try {                       Thread.sleep(1000);            } catch (InterruptedException e) {                       e.printStackTrace();            }            cachedThreadPool.execute(new Runnable() {                       @Override                public void run() {                           String threadName = Thread.currentThread().getName();                    Log.v("zxy", "线程:" + threadName + ",正在执行第" + index + "个任务");                    try {                               long time = index * 500;                        Thread.sleep(time);                    } catch (InterruptedException e) {                               e.printStackTrace();                    }                }            });        }

  4. newScheduledThreadPool
    创建一个可以定时或者周期性执行任务的线程池,示例为:

        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);        //延迟2秒后执行该任务        scheduledThreadPool.schedule(new Runnable() {                   @Override            public void run() {                   }        }, 2, TimeUnit.SECONDS);        //延迟1秒后,每隔2秒执行一次该任务        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {                   @Override            public void run() {                   }        }, 1, 2, TimeUnit.SECONDS);
  5. newSingleThreadScheduledExecutor
    创建一个可以定时或者周期性执行任务的线程池,该线程池的线程数为1,示例为

       ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();        //延迟1秒后,每隔2秒执行一次该任务        singleThreadScheduledPool.scheduleAtFixedRate(new Runnable() {                   @Override            public void run() {                       String threadName = Thread.currentThread().getName();                Log.v("zxy", "线程:" + threadName + ",正在执行");            }        },1,2,TimeUnit.SECONDS);

    这个和上面的没什么太大区别,只不过是线程池内线程数量的不同,效果为:每隔2秒就会执行一次该任务

自定义线程池ThreadPoolExecutor(自行了解)

线程池的停止
关于线程池的停止,ExecutorService为我们提供了两个方法:shutdown和shutdownNow,这两个方法各有不同,可以根据实际需求方便的运用,如下:

1、shutdown()  平滑的关闭线程池。(如果还有未执行完的任务,就等待它们执行完)。2、shutdownNow() 简单粗暴的关闭线程池。(没有执行完的任务也直接关闭)。

线程池的工作流程

简单说:

任务来了,优先考虑核心线程。核心线程满了,进入阻塞队列。阻塞队列满了,考虑非核心线程。非核心线程满了,再触发拒绝任务。

详细说明:

1 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务。

2 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列。

3 如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。

4 如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理。

注意:当池中线程数大于coolPoolSize,超过keepAliveTime时间的闲置线程会被回收掉。回收的是非核心线程,核心线程一般是不会回收的。如果设置allowCoreThreadTimeOut(true),则核心线程在闲置keepAliveTime时间后也会被回收。

任务队列是一个阻塞队列,线程执行完任务后会去队列取任务来执行,如果队列为空,线程就会阻塞,直到取到任务。

2.1.6. java锁机制

在java中,解决同步问题,很多时候都会使用到synchronizedLock,这两者都是在多线程并发时候常使用的锁机制。在JDK1.6后,对synchronized进行了很多优化,如偏向锁、轻量级锁等,synchronized的性能已经与Reentrantlock大致相同,除非要使用Reentrantlock的一些高级功能(实现公平锁、中断锁等),一般推荐使用synchronized关键字来实现加锁机制

Synchronized 是Java 并发编程中很重要的关键字,另外一个很重要的是 volatile。Syncronized 一次只允许一个线程进入由他修饰的代码段,从而允许他们进行自我保护。进入由Synchronized 保护的代码区首先需要获取 Synchronized 这把锁,其他线程想要执行必须进行等待。Synchronized 锁住的代码区域执行完成后需要把锁归还,也就是释放锁,这样才能够让其他线程使用。

Lock 是 Java并发编程中很重要的一个接口,它要比 Synchronized 关键字更能直译"锁"的概念,Lock需要手动加锁和手动解锁,一般通过 lock.lock() 方法来进行加锁, 通过 lock.unlock() 方法进行解锁。与 Lock 关联密切的锁有 ReetrantLockReadWriteLock

ReetrantLock 实现了Lock接口,它是一个可重入锁,内部定义了公平锁与非公平锁。

ReadWriteLock 一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。ReentrantReadWirteLock实现了ReadWirteLock接口,并未实现Lock接口。

Synchronized 的使用

修饰一个方法:即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候。

实例方法:锁住的是该类的实例对象静态方法:锁住的是该类的类对象。
public synchronized void goHOme(){     }
public static synchronized void goHOme(){     }

修饰代码块:表示只能有一个线程进入某个代码段

public void numDecrease(Object num){       synchronized (num){         number++;  }}

修饰一个类:作用的对象是这个类的所有对象,只要是这个类型的class不管有几个对象都会起作用。

class Person {         public void method() {         //锁住的是该类的类对象,如果换成this或其他object,则锁住的是该类的实例对象       synchronized(Person.class) {               // todo       }    } }

获取对象锁

synchronized(this|object) {}修饰非静态方法

获取类锁

synchronized(类.class) {}修饰静态方法

Lock 的使用

public interface Lock {         void lock();    void lockInterruptibly() throws InterruptedException;      boolean tryLock();      boolean tryLock(long time, TimeUnit unit) throws InterruptedException;      void unlock();      Condition newCondition();}


使用示例:

Lock lock = ...;lock.lock();try{         //处理任务}catch(Exception ex){          }finally{         lock.unlock();   //释放锁}
Lock lock = ...;if(lock.tryLock()) {          try{              //处理任务     }catch(Exception ex){                   }finally{              lock.unlock();   //释放锁     }}else {         //如果不能获取锁,则直接做其他事情}
public void method() throws InterruptedException {         lock.lockInterruptibly();    try {          //.....    }    finally {             lock.unlock();    }}

一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

synchronized和Lock的区别?

主要区别:

synchronized是Java中的关键字,是Java的内置实现;Lock是Java中的接口。synchronized遇到异常会释放锁;Lock需要在发生异常的时候调用成员方法Lock#unlock()方法。synchronized是不可以中断的,Lock可中断。synchronized不能去尝试获得锁,没有获得锁就会被阻塞; Lock可以去尝试获得锁,如果未获得可以尝试处理其他逻辑。synchronized多线程效率不如Lock,不过Java在1.6以后已经对synchronized进行大量的优化,所以性能上来讲,其实差不了多少。

死锁

所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

死锁触发的四大条件?

互斥锁请求与保持不可剥夺循环的请求与等待

简单死锁代码示例:

public class DeadLock {         public static String obj1 = "obj1";    public static String obj2 = "obj2";    public static void main(String[] args){             Thread a = new Thread(new Lock1());        Thread b = new Thread(new Lock2());        a.start();        b.start();    }    }class Lock1 implements Runnable{         @Override    public void run(){             try{                 System.out.println("Lock1 running");            while(true){                     synchronized(DeadLock.obj1){                         System.out.println("Lock1 lock obj1");                    Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2                    synchronized(DeadLock.obj2){                             System.out.println("Lock1 lock obj2");                    }                }            }        }catch(Exception e){                 e.printStackTrace();        }    }}class Lock2 implements Runnable{         @Override    public void run(){             try{                 System.out.println("Lock2 running");            while(true){                     synchronized(DeadLock.obj2){                         System.out.println("Lock2 lock obj2");                    Thread.sleep(3000);                    synchronized(DeadLock.obj1){                             System.out.println("Lock2 lock obj1");                    }                }            }        }catch(Exception e){                 e.printStackTrace();        }    }}


可以看到,Lock1获取obj1,Lock2获取obj2,但是它们都没有办法再获取另外一个obj,因为它们都在等待对方先释放锁,这时就是死锁。

2.1.7. Java中Volatile关键字(重要)

基本概念:Java 内存模型中的可见性原子性有序性

原子性:(原子是世界上的最小单位,具有不可分割性)原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1;

非原子性:

也就是整个过程中会出现线程调度器中断操作的现象

类似"a ++"这样的操作不具有原子性,因为它可能要经过以下两个步骤:

(1)取出 a 的值

(2)计算 a+1

如果有两个线程t1,t2都在进行这样的操作。t1在第一步做完之后还没来得及加1操作就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1开始执行第二步(此时t1中a的值可能还是旧值,不是一定的,只有线程t2中a的值没有及时更新到t1中才会出现)。这个时候就出现了错误,t2的操作相当于被忽略了

类似于a += 1这样的操作都不具有原子性。还有一种特殊情况,就是long跟double类型某些情况也不具有原子性

只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

举例:请分析以下哪些操作是原子性操作:

x = 10;        //语句1y = x;         //语句2x++;           //语句3x = x + 1;     //语句4

其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

同样的,x++和 x = x+1包括2个操作:读取x的值,进行加1操作,写入新的值。

如何保证原子性?

synchronized、Lock、cas原子类工具

由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。其次cas原子类工具。

共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这个几个线程的共享变量

可见性:一个线程对共享变量值的修改,能够及时的被其它线程看到。也就是一个线程对共享变量修改的结果,另一个线程马上就能看到修改的值。

如何保证可见性?

volatile、synchronized、Lock

要想实现变量的一定可见,可以使用volatile。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。(其实还有final,但是它初始化后,值不可更改,所以一般不用它实现可见性)。

指令重排:CPU在执行代码时,其实并不一定会严格按照我们编写的顺序去执行,而是可能会考虑一些效率方面的原因,对那些先后顺序无关紧要的代码进行重新排序,这个操作就被称为指令重排。指令重排在单线程情况下没有什么影响,但是在多线程就不一定了。

有序性:程序执行的顺序按照代码先后的顺序执行。

如何保证有序性?

volatile、synchronized、Lock

volatile:

volatile原理:Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache 中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

当一个变量定义为 volatile 之后,将具备两种特性:

1.保证此变量对所有的线程的可见性。当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主内存是不确定的,当其他线程去读取时,此时主内存中可能还是原来的旧值,因此无法保证可见性。

2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

volatile 性能:
  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
  
volatile为什么不能保证原子性?
简单的说,修改volatile变量分为四步:

1)读取volatile变量到local2)修改变量值3)local值写回4)插入内存屏障,即lock指令,让其他线程可见

这样就很容易看出来,前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。(或者可以理解为线程安全需要锁来保证)。这也就是为什么,volatile只用来保证变量可见性和有序性,但不保证原子性。

2.2 jvm

2.2.1. java内存模型

  1. Jvm内存区域(运行时数据区)划分:

    程序计数器:当前线程的字节码执行位置的指示器,线程私有。

    Java虚拟机栈:描述的Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧,存储着局部变量、操作数栈、动态链接和方法出口等,线程私有。

    本地方法栈:本地方法执行的内存模型,线程私有。

    Java堆:所有对象实例分配的区域。

    方法区:所有已经被虚拟机加载的类的信息、常量、静态变量和即时编辑器编译后的代码数据

详细说明:

程序计数器
程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
如果线程正在执行的是一个 Native 方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java 虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等消息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

其中 64 位长度的 long 和 double 类型的数据会占用两个局部变量空间(Slot),其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在 Java 虚拟机规范中,对这个区域规定了两种异常状态:

如果线程请求的栈深度大于虚拟机所允许的的深度,将抛出 StackOverflowError 异常。
如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈
本地方法栈(Native Method Stack) 与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(例如:Sun HotSpot虚拟机)直接就把虚拟机栈和本地方法栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java 堆
对于大多数应用来说,Java 堆(Java Heap) 是 Java 虚拟机所管理的的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器基本采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。

从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池(Runtime Constant Pool) 是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时就会抛出 OutOfMemoryError 异常。

扩展String s1 = "abc"和String s2 = new String(“abc”)的区别,生成对象的情况

指向方法区:"abc"是常量,所以它会在方法区中分配内存,如果方法区已经给"abc"分配过内存,则s1会直接指向这块内存区域。

指向Java堆:new String(“abc”)是重新生成了一个Java实例,它会在Java堆中分配一块内存。

2.2.1. GC机制(重要)

GC 是 garbage collection 的缩写, 垃圾回收的意思. 也可以是 Garbage Collector, 也就是垃圾回收器.
Java的内存分配与回收全部由JVM垃圾回收进程自动完成。

面试题:“你能不能谈谈,java GC”

1、哪些对象可以被回收。2、何时回收这些对象。3、采用什么样的方式回收。

问题1:哪些对象可以被回收?

对象存活判断(如何判断对象可回收/垃圾搜集)
判断一个对象可以回收通常采用的算法是引用计数算法可达性分析算法。由于互相引用导致的计数不好判断,Java采用的可达性算法。

  1. 引用计数算法
    每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,效率很高,但是主流的JVM并没有选用这种算法来判定可回收对象,因为它有一个致命的缺陷,那就是它无法解决对象之间相互循环引用的的问题对于循环引用的对象它无法进行回收。例:

    public class Object {           public Object instance;    public static void main(String[] args) {               // 1        Object objectA = new Object();        Object objectB = new Object();                // 2        objectA.instance = objectB;        objectB.instance = objectA;                // 3        objectA = null;        objectB = null;    }

    程序启动后,objectA和objectB两个对象被创建并在堆中分配内存,这两个对象都相互持有对方的引用,除此之外,这两个对象再无任何其他引用,实际上这两个对象已经不可能再被访问(引用被置空,无法访问),但是它们因为相互引用着对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC收集器回收它们。

    实际上,当第1步执行时,两个对象的引用计数器值都为1;当第2步执行时,两个对象的引用计数器都为2;当第3步执行时,二者都清为空值,引用计数器值都变为1。根据引用计数算法的思想,值不为0的对象被认为是存活的,不会被回收;而事实上这两个对象已经不可能再被访问了,应该被回收。

  2. 可达性分析算法(根搜索算法)
    在主流的JVM实现中,都是通过可达性分析算法来判定对象是否存活的。可达性分析算法的基本思想是:通过一系列被称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots对象没有任何引用链相连,就认为GC Roots到这个对象是不可达的,判定此对象为不可用对象,可以被回收。

    在上图中,objectA、objectB、objectC是可达的,不会被回收;objectD、objectE虽然有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

在Java中,可作为GC Roots的对象包括下面几种:

   1、java虚拟机栈中引用的对象;   2、方法区中类静态属性引用的对象;   3、方法区中常量引用的对象;   4、本地方法栈中Native(JNI)方法引用的对象。

第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。

问题3:采用什么样的方式回收

GC常用算法

可达性分析算法只是知道了哪些对象可以回收,不过垃圾收集显然还需要解决后两个问题,什么时候回收以及如何回收,在根搜索算法的基础上,现代虚拟机的实现当中,垃圾搜集的算法主要有三种,分别是标记-清除算法、复制算法、标记-整理算法,这三种算法都扩充了根搜索算法,不过它们理解起来还是非常好理解的。

标记 -清除算法

就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高(递归与全堆对象遍历);另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,内存的布局自然会乱七八糟。空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法
“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

标记-整理算法
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

分代搜集算法(重要)
GC 分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

“分代搜集”算法,把Java堆分为新生代老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

新生代GC(minor GC):指发生在新生代的垃圾回收动作,因为Java对象大多都具备朝生夕灭的特点,所以minor GC发生得非常频繁,一般回收速度也比较块。老年代GC(Major GC/Full GC):指发生在老年代的GC,它的速度会比minor GC慢很多。

问题2:何时回收这些对象?

回收的时机
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC),它们所针对的区域如下。普通GC(minor GC):只针对新生代区域的GC。全局GC(major GC or Full GC):针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC。由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC。

内存模型与回收策略

Java 堆(Java Heap)是JVM所管理的内存中最大的一块,堆又是垃圾收集器管理的主要区域,Java 堆主要分为2个区域-新生代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。

Eden 区
大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。 通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。

Survivor 区
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

Old 区
老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记——整理算法。

java垃圾收集器:(共7种,着重了解CMS和G1)

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

G1收集器

与CMS收集器相比G1收集器有以下特点:

1、空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

2、可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。

使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region 的集合。

G1的新生代收集跟 ParNew 类似,当新生代占用达到一定比例的时候,开始出发收集。和 CMS 类似,G1 收集器收集老年代对象会有短暂停顿。

2.2.1. 类加载过程

类加载的过程?

  1. 加载:将类的全限定名转化为二进制流,再将二进制流转化为方法区中的类型信息,从而生成一个Class对象。

  2. 验证:对类的验证,包括格式、字节码、属性等。

  3. 准备:为类变量分配内存并设置初始值。

  4. 解析:将常量池的符号引用转化为直接引用。

  5. 初始化:执行类中定义的Java程序代码,包括类变量的赋值动作和构造函数的赋值。

  6. 使用

  7. 卸载

只有加载、验证、准备、初始化和卸载的这个五个阶段的顺序是确定的。

类加载的机制,以及为什么要这样设计?

类加载的机制是双亲委派模型。大部分Java程序需要使用的类加载器包括:

启动类加载器:由C++语言实现,负责加载Java中的核心类。扩展类加载器:负责加载Java扩展的核心类之外的类。应用程序类加载器:负责加载用户类路径上指定的类库

双亲委派模型如下:

双亲委派模型要求出了顶层的启动类加载器之外,其他的类加载器都有自己的父加载器,通过组合实现。

双亲委派模型的工作流程:
当一个类加载的任务来临的时候,先交给父类加载器完成,父类加载器交给父父类加载器完成,知道传递给启动类加载器,如果完成不了的情况下,再依次往下传递类加载的任务。

这样设计的原因:
双亲委派模型能够保证Java程序的稳定运行,不同层次的类加载器具有不同优先级,所有的对象的父类Object,无论哪一个类加载器加载,最后都会交给启动类加载器,保证安全。

三、Android基础

3.1.Android系统架构

  1. 应用层
  2. 应用框架层(Framwork)
  3. 系统运行库层
  4. Linux内核层

3.2.四大组件

1. Activity

  1. 生命周期

    Activity A 启动另一个Activity B,回调如下:
    Activity A 的onPause() → Activity B的onCreate() → onStart() → onResume() → Activity A的onStop();如果B是透明主题又或则是个DialogActivity,则不会回调A的onStop;

  2. activity四种启动模式

    standard:标准模式,每次都会在活动栈中生成一个新的Activity实例。通常我们使用的活动都是标准模式。

    singleTop:栈顶复用,如果Activity实例已经存在栈顶,那么就不会在活动栈中创建新的实例。比较常见的场景就是给通知跳转的Activity设置,因为你肯定不想前台Activity已经是该Activity的情况下,点击通知,又给你再创建一个同样的Activity。

    singleTask:栈内复用,如果Activity实例在当前栈中已经存在,就会将当前Activity实例上面的其他Activity实例都移除栈。常见于跳转到主界面。

    singleInstance:单实例模式,创建一个新的任务栈,这个活动实例独自处在这个活动栈中。

2. Service

  1. 启动Service的2种方式

    第一种方式:Context.startService():
    service会一直无限期运行下去,只有外部调用了stopService()或stopSelf()方法时,该Service才会停止运行并销毁。多次startService不会重复执行onCreate回调,但每次都会执行onStartCommand回调。

    两种方式创建:
    a.继承Service类
    请务必在Service中开启线程来执行耗时操作,因为Service运行在主线程中

    b.继承IntentService类
    IntentService继承于Service,若Service不需要同时处理多个请求,那么使用IntentService将是最好选择:只需要重写onHandleIntent()方法,该方法接收一个回传的Intent参数,可以在方法内进行耗时操作,因为它默认开启了一个子线程,操作执行完成后也无需手动调用stopSelf()方法,onHandleIntent()会自动调用该方法。

    第二种方式:Context.bindService()

  2. Service的销毁
    stopService/unbindService
    在Service的启动这一部分,我们已经简单介绍了销毁Service的方法。

    startService—>stopService(stopSelf)
    bindService—>unbindService

  3. 生命周期

  4. 启动前台Service
    Service几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是Service的系统优先级还是比较低的,当系统出现内存不足情况时,就有可能会回收掉正在后台运行的Service。如果你希望Service可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台Service。

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    Notification notification = new Notification(icon, text, System.currentTimeMillis());Intent notificationIntent = new Intent(this, ExampleActivity.class);PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);notification.setLatestEventInfo(this, title, mmessage, pendingIntent);startForeground(ONGOING_NOTIFICATION_ID, notification);

3. BroadcastReceiver

target 26 之后,无法在 AndroidManifest 显示声明大部分广播,除了一部分必要的广播,如:

ACTION_BOOT_COMPLETED
ACTION_TIME_SET
ACTION_LOCALE_CHANGED

LocalBroadcastManager.getInstance(MainActivity.this).registerReceiver(receiver, filter);

静态注册和动态注册

4. ContentProvider

作用:进程间进行数据交互、共享,即跨进程通信
原理:ContentProvider的底层原理 = Android中的Binder机制
ContentProvider 管理对结构化数据集的访问。它们封装数据,并提供用于定义数据安全性的机制。 内容提供程序是连接一个进程中的数据与另一个进程中运行的代码的标准界面。

ContentProvider 无法被用户感知,对于一个 ContentProvider 组件来说,它的内部需要实现增删该查这四种操作,它的内部维持着一份数据集合,这个数据集合既可以是数据库实现,也可以是其他任何类型,如 List 和 Map,内部的 insert、delete、update、query 方法需要处理好线程同步,因为这几个方法是在 Binder 线程池中被调用的。

ContentProvider 通过 Binder 向其他组件乃至其他应用提供数据。当 ContentProvider 所在的进程启动时,ContentProvider 会同时启动并发布到 AMS 中,需要注意的是,这个时候 ContentProvider 的 onCreate 要先于 Application 的 onCreate 而执行。

ContentProvider 和 sql 在实现上有什么区别?

ContentProvider 屏蔽了数据存储的细节,内部实现透明化,用户只需关心 uri 即可(是否匹配)ContentProvider 能实现不同 app 的数据共享,sql 只能是自己程序才能访问Contentprovider 还能增删本地的文件,xml等信息

3.3.屏幕适配

3.3.1相关重要概念

屏幕尺寸
含义:手机对角线的物理尺寸
单位:英寸(inch),1英寸=2.54cm
Android手机常见的尺寸有5寸、5.5寸、6寸等等

屏幕分辨率
含义:屏幕像素点数总和
例子:1080x1920,即宽度方向上有1080个像素点,在高度方向上有1920个像素点
单位:px(pixel),1px=1像素点(UI设计师的设计图会以px作为统一的计量单位)

Android手机常见的分辨率:320x480、480x800、720x1280、1080x1920

屏幕像素密度
含义:每英寸的像素点数
单位:dpi(dots per ich)

假设设备内每英寸有160个像素,那么该设备的屏幕像素密度=160dpi

安卓手机对于每类手机屏幕大小都有一个相应的屏幕像素密度:

屏幕尺寸、屏幕分辨率、屏幕像素密度三者关系

密度无关像素
含义:density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关
单位:dp,可以保证在不同屏幕像素密度的设备上显示相同的效果

Android开发时用dp而不是px单位设置图片大小,是Android特有的单位场景:假如同样都是画一条长度是屏幕一半的线,如果使用px作为计量单位,那么在480x800分辨率手机上设置应为240px;在320x480的手机上应设置为160px,二者设置就不同了;如果使用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度。

dp与px的转换
因为ui设计师给你的设计图是以px为单位的,Android开发则是使用dp作为单位的,那么我们需要进行转换:

在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

独立比例像素
含义:scale-independent pixel,叫sp或sip
单位:sp

Android开发时用此单位设置文字大小,可根据字体大小首选项进行缩放推荐使用12sp、14sp、18sp、22sp作为字体设置的大小,不推荐使用奇数和小数,容易造成精度的丢失问题;小于12sp的字体会太小导致用户看不清

3.3.2.屏幕适配:目前最好的适配方案

  1. SmallestWidth适配(sw限定符适配)
    实现原理:Android会识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。

    sw限定符适配宽高限定符适配类似,区别在于,前者有很好的容错机制,如果没有value-sw360dp文件夹,系统会向下寻找,比如离360dp最近的只有value-sw350dp,那么Android就会选择value-sw350dp文件夹下面的资源文件。这个特性就完美的解决了宽高限定符的容错问题。
      
    优点:1.非常稳定,极低概率出现意外
       2.不会有任何性能的损耗
       3.适配范围可自由控制,不会影响其他三方库
      
    缺点:就是多个dimens文件可能导致apk变大,几百k(影响很小)。

    附件:生成sw文件的工具

  2. 今日头条适配方案
    实现原理修改系统的density值(核心)

    今日头条适配是以设计图的宽或高进行适配的,适配最终是改变系统density实现的。

    优点:使用成本低,侵入性低,修改一次项目所有地方都会适配,无性能损耗
    缺点:
    1.只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的。
    2.这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图完全一样。

  3. AutoSize
    AndroidAutoSize 是基于今日头条适配方案,该开源库已经很大程度上解决了今日头条适配方案的两个缺点,可以对activity,fragment进行取消适配。也是目前我的项目中所使用的适配方案。

    使用也非常简单只需两步:
    (1)引入:

     implementation 'me.jessyan:autosize:1.1.2'

    (2)在 AndroidManifest 中填写全局设计图尺寸 (单位 dp),如果使用副单位,则可以直接填写像素尺寸,不需要再将像素转化为 dp,详情请查看 demo-subunits

    <manifest>    <application>                    <meta-data            android:name="design_width_in_dp"            android:value="360"/>        <meta-data            android:name="design_height_in_dp"            android:value="640"/>                </application>           </manifest>

3.4.Android消息机制

子线程更新UI的方式

1、调用activity的runOnUIThread()

// 因为runOnUiThread是Activity中的方法,Context是它的父类,所以要转换成Activity对象才能使用((Activity) context).runOnUiThread(new Runnable() {         @Override    public void run() {             // 在这里执行你要想的操作 比如直接在这里更新ui或者调用回调在 在回调中更新ui    }});

2、Handle发送Runnable

new Handler(mContext.getMainLooper()).post(new Runnable() {         @Override        public void run() {                 // 在这里执行你要想的操作 比如直接在这里更新ui或者调用回调在 在回调中更新ui        }});

3、Handler发送Message

private Handler handler = new Handler() {      //处理消息    @Override    public void handleMessage(Message msg) {             super.handleMessage(msg);        switch (msg.what) {             case XXX:        XXX XXX= msg.obj;             break;        }    }};//发送消息Message msg = Message.obtain();msg.what = XXX;msg.obj = XXX;mHandler.sendMessage(msg);

Handler推送Message和Runnable的区别

其实,Message里面有一个叫callback的Runnable属性,无论你send一个Message,还是post一个Runnnable,最终都是send一个Message。Runnable最终也是转化为一个Message。

为了更方便开发者根据不同需要进行调用。当我们需要传输很多数据时,我们可以使用sendMessage来实现,因为通过给Message的不同成员变量赋值可以封装成数据非常丰富的对象,从而进行传输;当我们只需要进行一个动作时,直接使用Runnable,在run方法中实现动作内容即可。

Handler原理:(非常重要)

消息机制指Handler、Looper、MessageQueue、Message之间如何工作的。

handler是用来处理消息和接收消息的中间者。在handler的构造函数中,会绑定其中的looper和MessageQueue。handler依赖于looper,looper依赖于MessageQueue,所以在子线程中使用handler抛出异常是因为子线程中没有初始化looper对象(子线程中使用handler需要手动调用Looper.prepare()),而主线程中looper是在ActivityThread中已经初始化过了,所以能直接在主线程中能拿到Handler。

Looper是用来轮询消息,通过loop方法实现死循环,有消息的时候,通过MessageQueue.next方法取出message,没有消息的时候,线程处于阻塞的状态。在有消息的时候获取到消息,将消息交给了handler,handler会根据消息中有没有callback,如果有callback会直接callback,否则通过handleMessage处理。

MessageQueue是一个单链表结构来存储Message,每次通过next方法取出Message消息后,取完之后将message.next给当前的message,再将message.next=null,实际上就是移除当前的message。但是在looper里面每次在next取出message后,放到了message的sPool里面,缓存起来方便使用。

Message就没什么好说的,主要存储平常经常用的obj和what信息,以及我们不用关心的target和callback等。

创建 Message 实例的最佳方式

由于 Handler 极为常用,所以为了节省开销,Android 给 Message 设计了回收机制,所以我们在使用的时候尽量复用 Message ,减少内存消耗。

通过 Message 的静态方法 Message.obtain();  通过 Handler 的公有方法 handler.obtainMessage(); 。

一个线程会有几个Looper,几个Handler,以及Looper会存在线程哪里?

一个线程一个Looper,可以有多个Handler,Looper会存在线程的ThreadLocal对象里,该对象是线程的缓存区

ThreadLocal:它是和线程一一对应的,从Thread类可以看出来,ThreadLocal是作为Thread变量来使用。
ThreadLocal只是ThreadLocalMap的一个包装类,实现了get和set方法,而ThreadLocalMap实际是一个由Entry内部类组成的数组,Entry是继承自弱应用,弱引用里面放的就是ThreadLocal当前对象,Entry的value存的是当前线程要存储的对象,value作为Entry的成员变量。 ThreadLocal经常会问到内存泄漏的问题,从上面分析可以发现ThreadLocalMap里面的Entry对象存储的ThreadLocal弱引用,而value直接作为Entry的强引用,因此在用到了ThreadLocal的地方,防止内存泄漏,手动调用remove方法。

Looper在主线程中死循环为什么没有导致界面的卡死?

  1. 导致卡死的是在Ui线程中执行耗时操作导致界面出现掉帧,甚至ANR,Looper.loop()这个操作本身不会导致这个情况。

  2. 有人可能会说,我在点击事件中设置死循环会导致界面卡死,同样都是死循环,不都一样的吗?Looper会在没有消息的时候阻塞当前线程,释放CPU资源,等到有消息到来的时候,再唤醒主线程。

  3. App进程中是需要死循环的,如果循环结束的话,App进程就结束了。

详细可以阅读:《Android中为什么主线程不会因为Looper.loop()里的死循环卡死?》

扩展阅读:Handler 都没搞懂,拿什么去跳槽啊?

3.5.View的绘制流程(自定义控件步骤)

3.5.1 View绘制流程

activity界面显示流程:activity启动后,不会立马去显示界面上的view,而是等到onResume的时候才会真正显示view的时机,首先会触发windowManager.addView方法,在该方法中触发代理对象WindowManagerGlobal的addView方法,代理对象的addView方法中创建了viewRootImpl,将setContentView中创建的decorView通过viewRootImpl的setView方法放到了viewRootImpl中,最终经过viewRootImpl一系列的方法最终调用performTraversals方法。

  1. onMeasure:
    View: onMeasure 方法会计算自己的尺寸并通过 setMeasureDimension 保存。
    ViewGroup : onMeasure 方法会调用所有子 view的onMeasure 方法进行自我测量并保存。然后通过子View的尺寸和位置计算出自己的尺寸并保存。

  2. onLayout:
    View :因为是没有子 View 的,所以View的onLayout里面什么都不做。
    ViewGroup: ViewGroup 中的 onLayout 方法会调用所有子 View 的 onLayout 方法,把尺寸和位置传给他们,让他们完成自我的内部布局。

  3. onDraw:
    绘制背景:对应 drawBackground(Canvas)方法。
    绘制主体:对应 onDraw(Canvas)方法。
    绘制子View: 对应 dispatchDraw(Canvas)方法。
    绘制滑动相关和前景: 对应 onDrawForeground(Canvas)。

3.5.2 自定义View

重写方法:onMeasure、 onLayout、onDraw、onTouchEvent

3.6.事件分发

3.6.1 了解Activity的构成

一个Activity包含了一个Window对象,这个对象是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域:一个是TitleView,另一个是ContentView,而我们平时所写的就是展示在ContentView中的

3.6.2 触摸事件的类型

ACTION_DOWN
ACTION_MOVE(移动的距离超过一定的阈值会被判定为ACTION_MOVE操作)
ACTION_UP

View事件分发本质就是对MotionEvent事件分发的过程。即当一个MotionEvent发生后,系统将这个点击事件传递到一个具体的View上。

3.6.3 事件分发流程

  1. 注意:在Android系统中,拥有事件传递处理能力的类有以下三种:

    Activity:拥有分发和消费两个方法。ViewGroup:拥有分发、拦截和消费三个方法。View:拥有分发、消费两个方法。

    即只有ViewGroup有拦截事件

  2. 图解(只适用于理解事件分发机制)

  1. 事件分发的传递规则

    点击事件产生后会由默认会先走Activity的分发,传递给PhoneView,再传递给DecorView,最后传递给顶层的ViewGroup。接着走ViewGroup的分发,然后到ViewGroup的拦截,后面再到View的分发事件,最后会传到View的消费事件,如果View不消费,紧接着回传到ViewGroup的消费事件,如果ViewGroup也不消费,最后回到Activity的消费事件。

    整个事件分发构成了一个U型结构,下面总结了分发的细节流程:

    1. ViewGroupdispatchTouchEvent返回true或false,touch事件不会往子view中传递。只有在返回super.dispatchTouchEvent时候touch事件才会传递到子view。

      ture:当前View消耗所有事件

      false:停止分发,交由上层控件的onTouchEvent方法进行消费,如果本层控件是Activity,则事件将被系统消费。false的时候只会触发action_down。

    2. ViewGrouponInterceptTouchEvent返回false或者super.onInterceptTouchEvent时,touch事件会传递到子view。

      返回true事件不会向下传递,交给自己的onTouchEvent处理。

    3. ViewdispatchTouchEvent返回true或false,touch事件不会传给自己的ontouchEvent事件,返回false,只会触发action_down,move和up不会触发;返回true,才会触发move和up。返回super.dispatchTouchEvent,touch事件才会交给自己的onTouchEvent处理。

    4. ViewonTouchEvent返回false,只会有action_down事件,touch事件交给上一层处理,如果返回true才会消费,事件不会向上传递,如果返回super.ontouchEvent,得看clickable是不是返回true。

  2. ACTION_CANCEL什么时候触发,触摸button然后滑动到外部抬起会触发点击事件吗,再滑动回去抬起会么?

    一般ACTION_CANCEL和ACTION_UP都作为View一段事件处理的结束。如果在父View中拦截ACTION_UP或ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件。

    如果触摸某个控件,但是又不是在这个控件的区域上抬起(移动到别的地方了),就会出现action_cancel。

  3. 点击事件被拦截,但是想传到下面的View,如何操作?
    重写子类的requestDisallowInterceptTouchEvent()方法返回true就不会执行父类的onInterceptTouchEvent(),即可将点击事件传到下面的View。

  4. 如何解决View的事件冲突?举个开发中遇到的例子?
    常见开发中事件冲突的有ScrollView与RecyclerView的滑动冲突、RecyclerView内嵌同时滑动同一方向。

    滑动冲突的处理规则:

    对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
    对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部View拦截事件,何时由内部View拦截事件。
    对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。
    滑动冲突的实现方法:

    外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。
    内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合requestDisallowInterceptTouchEvent方法。

3.7.Context

Context
翻译为上下文,也可以理解为环境,是提供一些程序的运行环境基础信息。
是一个抽象类。继承结构如下:

ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。

ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity,所以Activity和Service以及Application的Context是不一样的,只有Activity需要主题,Service不需要主题。

Context一共有三种类型,分别是Application、Activity和Service。在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

Application中的Context和Activity中的Context的区别

Activity.this取的是当前Activity的Context,它的生命周期则只能存活于当前Activity
getApplicationContext() 生命周期是整个应用,当应用程序摧毁的时候,它才会摧毁

详细可以阅读:
郭霖:Android Context完全解析,你所不知道的Context的各种细节
鸿洋:Android Context 上下文 你必须知道的一切

3.8.序列化

Serialiable与Parcelable的区别

在使用内存的时候,Parcelable 类比Serializable 性能高,首选使用Parcelable 类

Serializable 在序列化的时候会产生大量的临时变量,从而引起频繁的GC

数据持久化,Parcelable 不能使用在要将数据存储在磁盘上的情况。尽管Serializable 效率低点,但在这种情况下,还是建议你用Serializable

3.9.动画

Android中动画大致分为3类:帧动画补间动画(View Animation)、属性动画(Object Animation)。

帧动画:通过xml配置一组图片,动态播放。很少会使用。

补间动画(View Animation):大致分为旋转、透明、缩放、位移四类操作。很少会使用。

属性动画(Object Animation):属性动画是现在使用的最多的一种动画,它比补间动画更加强大。属性动画大致分为两种使用类型,分别是 ViewPropertyAnimator 和 ObjectAnimator。前者适合一些通用的动画,比如旋转、位移、缩放和透明,使用方式也很简单通过 View.animate() 即可得到 ViewPropertyAnimator,之后进行相应的动画操作即可。后者适合用于为我们的自定义控件添加动画,当然首先我们应该在自定义 View 中添加相应的 getXXX() 和 setXXX() 相应属性的 getter 和 setter 方法,这里需要注意的是在 setter 方法内改变了自定义 View 中的属性后要调用 invalidate() 来刷新View的绘制。之后调用 ObjectAnimator.of 属性类型()返回一个 ObjectAnimator,调用 start() 方法启动动画即可。

补间动画与属性动画的区别:

补间动画是父容器不断的绘制 view,看起来像移动了效果,其实 view 没有变化,还在原地。属性动画是通过不断改变 view 内部的属性值,真正的改变 view。

3.10.Android5.0-10.0版本变更及开发适配

  1. Android 5.0

    Material Design

    ART虚拟机:ART模式在用户安装App时进行预编译AOT(Ahead-of-time),将android5.X的运行速度提高了3倍左右。

  2. Android 6.0

    应用权限管理
    运行时权限机制->危险权限需要动态申请权限
    官方指纹支持
    Doze电量管理

  3. Android 7.0

    FileProvider:在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。要应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider类。

    APK signature scheme v2:Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。

    只勾选V1签名就是传统方案签署,但是在 Android 7.0 上不会使用V2安全的验证方式。只勾选V2签名7.0以下会显示未安装,Android 7.0 上则会使用了V2安全的验证方式。同时勾选V1和V2则所有版本都没问题。

    org.apache不支持问题

    build.gradle里面加上这句话

    defaultConfig {           useLibrary 'org.apache.http.legacy'}

    或者在AndroidManifest.xml添加下面的配置

    <uses-library    android:name="org.apache.http.legacy"    android:required="false" />

    SharedPreferences闪退:

    // MODE_WORLD_READABLE:Android 7.0以后不能使用这个获取,会闪退// 应修改成MODE_PRIVATESharedPreferences read = getSharedPreferences(RELEASE_POOL_DATA, MODE_WORLD_READABLE);
  4. Android 8.0
    Notification(通知权限):Android 8.0之后通知权限默认都是关闭的,无法默认开启以及通过程序去主动开启,需要程序员读取权限开启情况,然后提示用户去开启。
    判断权限是否开启:

    /** * 判断通知权限是否开启 * @param context 上下文 */public static boolean isNotificationEnabled(Context context){           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {               return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).areNotificationsEnabled();    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {               AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);        ApplicationInfo appInfo = context.getApplicationInfo();        String pkg = context.getApplicationContext().getPackageName();        int uid = appInfo.uid;        try {                   Class<?> appOpsClass = Class.forName(AppOpsManager.class.getName());            Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class);            Field opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION");            int value = (Integer) opPostNotificationValue.get(Integer.class);            return (Integer) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg) == 0;        } catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException | ClassNotFoundException ignored) {                   return true;        }    } else {               return true;    }

    前往设置开启权限:

    /** * 打开设置页面打开权限 * * @param activity activity * @param requestCode 这里的requestCode和onActivityResult中requestCode要一致 */public static void startSettingActivity(@NonNull Activity activity, int requestCode) {           try {               Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + activity.getPackageName()));        intent.addCategory(Intent.CATEGORY_DEFAULT);        activity.startActivityForResult(intent, requestCode);    } catch (Exception e) {               e.printStackTrace();    }}

自适应启动图标:从Android 8.0系统开始,应用程序的图标被分为了两层:前景层和背景层。也就是说,我们在设计应用图标的时候,需要将前景和背景部分分离,前景用来展示应用图标的Logo,背景用来衬托应用图标的Logo。需要注意的是,背景层在设计的时候只允许定义颜色和纹理,但是不能定义形状。注意图标图层的大小,两层的尺寸必须为108x108dp,前景图层中间的72x72dp图层就是在手机界面上展示的应用图标范围。这样系统在四面各留出18dp以产生有趣的视觉效果,如视差或脉冲(动画视觉效果由受支持的启动器生成,视觉效果可能因发射器而异)。

<?xml version="1.0" encoding="utf-8"?><adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">    <background android:drawable="@drawable/ic_launcher_background" />    <foreground android:drawable="@drawable/ic_launcher_foreground" /></adaptive-icon>

安装APK:
Android 8.0去除了“允许未知来源”选项,如果我们的App具备安装App的功能,那么AndroidManifest文件需要包含REQUEST_INSTALL_PACKAGES权限,未声明此权限的应用将无法安装其他应用。当然,如果你不想添加这个权限,也可以通过getPackageManager().canRequestPackageInstalls()查询是否有此权限,没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES这个action将用户引导至安装未知应用权限界面去授权。

静态广播无法正常接收:
Google官方声明:从android 8.0(API26)开始,对清单文件中静态注册广播接收者增加了限制,建议大家不要在清单文件中静态注册广播接收者,改为动态注册。当然,如果你还是想用静态注册的方式也是有方法的,Intent里添加Component参数可实现。

发送静态广播的特殊处理

Intent intent = new Intent( "广播的action" );intent.setComponent( new ComponentName( "包名(如:com.yhd.rocket)","接收器的完整路径(如:com.yhd.rocket.receiver.RoReceiver)" ) );sendBroadcast(intent);
  1. Android 9.0
    刘海屏API支持:
    Android 9 支持最新的全面屏,其中包含为摄像头和扬声器预留空间的屏幕缺口。 通过 DisplayCutout类可确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些屏幕缺口区域是否存在及其位置,使用 getDisplayCutout() 函数。

    CLEARTEXT communication to http://xxx not permitted by network security policy:
    问题原因: Android P 限制了明文流量的网络请求,非加密的流量请求(http)都会被系统禁止掉。解决方案:
    方案一:将http请求改为https
    方案二:添加usesCleartextTraffic属性

    <application    android:usesCleartextTraffic="true"></application>    

    方案三:添加资源文件(复杂)
    1、在资源文件新建xml目录,新建文件network_security_config.xml

    <?xml version="1.0" encoding="utf-8"?><network-security-config>    <base-config cleartextTrafficPermitted="true" /></network-security-config>

    2、清单文件配置:

    <application    android:networkSecurityConfig="@xml/network_security_config"></application>

    全面限制静态广播的接收
    升级安卓9.0之后,隐式广播将会被全面禁止,在AndroidManifest中注册的Receiver将不能够生效,你需要在应用中进行动态注册。

  2. Android 10

    暗黑模式
    分区存储
    隐私增强(后台能否访问定位)
    限制程序访问剪贴板
    应用黑盒
    权限细分需兼容
    后台定位单独权限需兼容

    设备唯一标示符需兼容:从Android10开始普通应用不再允许请求权限android.permission.READ_PHONE_STATE。而且,无论你的App是否适配过Android Q(既targetSdkVersion是否大于等于29),均无法再获取到设备IMEI等设备信息。

    //受影响的APIBuild.getSerial();TelephonyManager.getImei();TelephonyManager.getMeid()TelephonyManager.getDeviceId();TelephonyManager.getSubscriberId();TelephonyManager.getSimSerialNumber();

    targetSdkVersion<29 的应用,其在获取设备ID时,会直接返回null
    targetSdkVersion>=29 的应用,其在获取设备ID时,会直接抛出异常SecurityException
    如果您的App希望在Android 10以下的设备中仍然获取设备IMEI等信息,可按以下方式进行适配:

    <uses-permission android:name="android.permission.READ_PHONE_STATE"        android:maxSdkVersion="28"/>

    后台打开Activity 需兼容
    非 SDK 接口限制 需兼容

四、Android进阶

4.1 Okhttp

okhttp主要实现了异步、同步的网络操作,创建了不同的call对象,这里的call对象是一个个的runnable对象,由于我们的任务是很多的,因此这里有Dispatcher包装了线程池来处理不同的call,其中该类中创建了三种队列,分别用于存放正在执行的异步任务,同步队列,以及准备的队列。最后在执行每个任务的时候,采用队列的先进先出原则,处理每一个任务,都是交给了后面的各种拦截器来处理,有请求准备的拦截器、缓存拦截器、网络连接的拦截器,每一个拦截器组成了一个责任链的形式。到最后返回response信息。
OkHttp的底层是通过Java的Socket发送HTTP请求与接受响应的(这也好理解,HTTP就是基于TCP协议的),但是OkHttp实现了连接池的概念,即对于同一主机的多个请求,其实可以公用一个Socket连接,而不是每次发送完HTTP请求就关闭底层的Socket,这样就实现了连接池的概念。而OkHttp对Socket的读写操作使用的OkIo库进行了一层封装。

责任链模式?

interceptors和networkInterceptors的区别?

4.2 Retrofit

retrofit基于okHttp封装成RESTFUL网络请求框架,通过工厂模式配置各种参数,通过动态代理、注解实现网络请求。retrofit利用了工厂模式,将分为生产网络请求执行器(callFactory)、回调方法执行器(callbackExecutor)、网络请求适配器(CallAdapterFactory)、数据转换器(converterFactory)等几种工厂。
callFactory负责生产okHttp的call,大家都知道okHttp通过生成call对象完成同步和异步的http请求。
callbackExecutor通过判断不同的平台,生成对应平台的数据回调执行器。其中android端的回调执行器是通过handler回调数据。
CallAdapterFactory是数据解析工厂,一般我们配置json的数据解析适配器就行。
converterFactory是数据转换的工厂,一般我们配置Rxjava的数据转换就行。
retrofit通过动态代理模式实现接口类配置的注解、参数解析成HTTP对象,最后通过okHttp实现网络请求。

Retrofit常见问题:

设计模式和封层解耦的理念

动态代理

4.3 Android常用图片库对比

主要对比Glide跟Picasso,然后了解Fresco

Glide:

图片+媒体缓存:Glide不仅是图片缓存,还适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
生命周期集成:根据Activity或者Fragment的生命周期管理图片加载请求。
高效处理Bitmap:使用Bitmap Pool使bitmap复用,主动调用recycle回收需要回收的Bitmap,减少系统回收压力。
高效的缓存策略灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格:比如imageVIew大小是200200,原图是400400,glide会缓存200200规格的,而Picasso只会缓存400400规格的图),加载速度快内存开销小(默认Bitmap格式的不同,Glide默认的Bitmap格式是RGB_565,Picasso是ARGB_8888,使得内存开销是Picasso的一半)。
还有一点, Picasso 是加载了全尺寸的图片到内存,下次在任何ImageView中加载图片的时候,全尺寸的图片将从缓存中取出,重新调整大小,然后加载。而 Glide 是按 ImageView 的大小来缓存的,它会为每种大小的ImageView缓存一次。尽管一张图片已经缓存了一次,但是假如你要在另外一个地方再次以不同尺寸显示,需要重新下载,调整成新尺寸的大小,然后将这个尺寸的也缓存起来。具体说来就是:假如在第一个页面有一个200x200的ImageView,在第二个页面有一个100x100的ImageView,这两个ImageView本来是要显示同一张图片,却需要下载两次,使用的使用不需要调整大小直接读取缓存加载。结论:Glide的这种方式优点是加载显示非常快,但同时也需要更大的空间来缓存。

Picasso
Picasso库的大小大概100k,而Glide的大小大概500k。单纯这个大小还好,更重要的是Picasso和Glide的方法个数分别是840和2678个,这个差距还是很大的,对于DEX文件65535个方法的限制来说,2678是一个相当大的数字了,建议在使用Glide的时候开启ProGuard。

Picasso与Square 公司的其他开源库如 Retrofit 或者 OkHttp搭配使用兼容性会更好些,占用体积也会少些。

Glide能做到Picasso所能做到的一切,只是图像质量可能比Picasso低,xu两者的区别是 Picasso 比 Glide 体积小很多且图像质量比 Glide 高,但Glide 的速度比 Picasso 更快,Glide 的长处是处理大型的图片流,如 gif、video,如果要制作视频类应用,Glide 当为首选。F

Fresco:
最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统,Fresco将图片放到一个特别的内存区域(Ashmem区)
大大减少OOM(在更底层的Native层对OOM进行处理,图片将不再占用App的内存)
适用于需要高性能加载大量图片的场景
对于一般App来说,Glide完全够用,而对于图片需求比较大的App,为了防止加载大量图片导致OOM,Fresco 会更合适一些。并不是说用Glide会导致OOM,Glide默认用的内存缓存是LruCache,内存不会一直往上涨。

4.4 Glide原理+手写图片加载框架思路

Glide考察的频率挺高的,常见的问题有:
Glide和其他图片加载框架的比较?
如何设计一个图片加载框架?
Glide缓存实现机制?
Glide如何处理生命周期?

建议阅读:1.郭霖:Glide最全解析

建议阅读:.面试官:简历上最好不要写Glide,不是问源码那么简单

Glide基本使用:

Glide.with(this).load(url).into(imageView);

with()方法可以接收Context、Activity或者Fragment类型的参数。注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。

除了加载字符串网址外,还能

// 加载本地图片File file = new File(getExternalCacheDir() + "/image.jpg");Glide.with(this).load(file).into(imageView);// 加载应用资源int resource = R.drawable.image;Glide.with(this).load(resource).into(imageView);// 加载二进制流byte[] image = getImageBytes();Glide.with(this).load(image).into(imageView);// 加载Uri对象Uri imageUri = getImageUri();Glide.with(this).load(imageUri).into(imageView);

加载占位图:
.placeholder(R.drawable.loading)
异常占位图:
.error(R.drawable.error)

假如让你自己写个图片加载框架,你会考虑哪些问题?
首先,梳理一下必要的图片加载框架的需求:

异步加载:线程池切换线程:Handler,没有争议吧缓存:LruCache、DiskLruCache防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置内存泄露:注意ImageView的正确引用,生命周期管理列表滑动加载的问题:加载错乱、队满任务过多问题当然,还有一些不是必要的需求,例如加载动画等。

异步加载:
线程池,多少个?

缓存一般有三级,内存缓存、硬盘、网络

由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。

读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。

Glide 必然也需要多个线程池,看下源码是不是这样

public final class GlideBuilder {       ...  private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载  private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池  ...  private GlideExecutor animationExecutor; //动画线程池

Glide使用了三个线程池,不考虑动画的话就是两个。

4.5 Rxjava常用操作符

操作符:

map 转换事件,返回普通事件

flatMap 转换事件,返回 Observable

conactMap concatMap 与 FlatMap 的唯一区别就是 concatMap 保证了顺序

subscribeOn 规定被观察者所在的线程

observeOn 规定下面要执行的消费者所在的线程

interval 每隔一定时间执行一些任务

timer 定时任务,多少时间以后发送事件

Zip 专用于合并事件,该合并不是连接(连接操作符后面会说),而是两两配对,也就意味着,最终配对出的 Observable 发射事件数目只和少的那个相同。不影响Observable的发射,Observable 被观察者会一直发射,不会停,只是Observer 接收不到

merge 多个 Observable 发射的数据随机发射,不保证先后顺序

Concat 多个 Observable 组合以后按照顺序发射,保证了先后顺序,不过最多能组合4个 Observable ,多的可以使用 contactArray

如何解决 RxJava 内存泄漏
订阅的时候拿到 Disposable ,主动调用 dispose
使用 RxLifeCycle
使用AutoDispose

map和 flatmap 的区别
Map和flatMap的区别 前者是严格按照1.2.3.4.5顺序发的,经过map以后还是按照这个顺序,后者是1.2.3.4.5发送完到 flatMap 里面,然后经过flatmap进行组装以后再发出来,顺序可能会打乱,使用 contactMap 可以保证转换后的事件发射顺序

4.6 Android IPC机制(跨进程通信)

IPC在不同操作系统中有不同的实现,常见的有:
Windows:剪贴板、管道、邮槽
Linux:命名管道、共享内存、信号量
Android是一种基于Linux内核的移动操作系统,但是Android的IPC机制并不能完全继承自Linux,相反,它有自己的IPC机制:Binder和Socket。

Android如何开启多进程?
通过给四大组件指定android:process属性可以开启多进程模式,在内存允许的条件下可以开启N个进程。(还有一种非常规的多进程方法,通过JNI在native层fork一个新的进程,这种方法暂不讨论)

多进程通信可能会出现的问题?

静态成员和单例模式完全失效:独立的虚拟机造成。线程同步机制完全失效:独立的虚拟机造成。SharedPreferences的可靠性下降:这是因为Sp不支持两个进程并发进行读写,有一定几率导致数据丢失。Application会多次创建:Android系统在创建新的进程时会分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,自然也会创建新的Application。

IPC基础概念
Serializable接口、Parcelable接口以及Binder,只有熟悉这三方面的内容后,我们才能更好地理解跨进程通信的各种方式。Serializable和Parcelable接口可以完成对象的序列化过程,当我们需要通过Intent和Binder传输数据时就需要使用Parcelable或者Serializable。

Android中IPC方式、各种方式优缺点?

总结:

当仅仅是跨进程的四大组件间的传递数据时,使用Bundle就可以 ,简单方便  当要共享一个应用程序的内部数据的时候,使用ContentProvider实现比较方便  当并发程度不高,也就是偶尔访问一次那种进程间通信,用Messenger就可以  当设计网络数据的共享时,使用socket 当需求比较复杂 ,高并发,并且还要求实时通信,而且有RPC需求时,就得使用AIDL了 文件共享的方法用于一些缓存共享之类的功能

AIDL(非常重要)

AIDL是一个缩写,全称是Android Interface Definition Language,也就是Android接口定义语言。

4.7 Framwork

4.8 插件化

4.9 组件化

4.10 热修复

tinker

五、kotlin

5.1.kotlin基础: From Java To Kotlin

常量与变量

//javaString name = "niubi";final String name = "niubi";//kotlinvar name = "niubi"val name = "niubi"

null 声明

//javaString name= null;//kotlinvar name:String?=null

空判断

//Javaif (text != null) {      int length = text.length();}//Kotlintext?.let {      val length = text.length}// or simply val length = text?.length

字符串拼接

//JavaString firstName = "firstName ";String lastName = "lastName ";String message = "My name is: "+ firstName + " " + lastName;//kotlinval firstName = "firstName "val lastName = "lastName "val message = "My name is: $firstName $lastName"

换行

//JavaString text = "First Line\n" +             "Second Line\n" +             "Third Line";//Kotlinval text = """        |First Line        |Second Line        |Third Line        """.trimMargin()

三元表达式

//JavaString text = x > 5 ? "x > 5" : "x <= 5";//Kotlinval text = if (x > 5)              "x > 5"           else "x <= 5"

操作符

//javafinal int andResult  = a & b;final int orResult   = a | b;final int xorResult  = a ^ b;final int rightShift = a >> 2;final int leftShift  = a << 2;//Kotlinval andResult  = a and bval orResult   = a or bval xorResult  = a xor bval rightShift = a shr 2val leftShift  = a shl 2

。。。。等等

5.2.Kotlin 的延迟初始化: lateinit var 和 by lazy

private var name0: String //报错private var name1: String = "xiaoming"//不报错 private var name2: String? = null //不报错

可是有的时候,我并不想声明一个类型可空的对象,而且我也没办法在对象一声明的时候就为它初始化,那么这时就需要用到 Kotlin 提供的延迟初始化。Kotlin 中有两种延迟初始化的方式:
一种是 lateinit var,一种是 by lazy。

  1. lateinit var
    lateinit var 只能用来修饰类属性,不能用来修饰局部变量,并且只能用来修饰对象,不能用来修饰基本类型(因为基本类型的属性在类加载后的准备阶段都会被初始化为默认值)。

    lateinit var 的作用:让编译期在检查时不要因为属性变量未被初始化而报错

    Kotlin 相信当开发者显式使用 lateinit var 关键字的时候,他一定也会在后面
    某个合理的时机将该属性对象初始化的(然而,谁知道呢,也许他用完才想起还
    没初始化)。

  2. by lazy
    by lazy的作用:真正做到了声明的同时也指定了延迟初始化时的行为,在属性被第一次被使用的时候能自动初始化。
    by lazy 本身是一种属性委托。属性委托的关键字是 by。by lazy 的写法如下:
    //用于属性延迟初始化

    val name: Int by lazy {        1 }

    //用于局部变量延迟初始化

     public fun foo() {        val bar:String by lazy {        "hello" } println(bar)}

    by lazy 要求属性声明为 val,即不可变变量,在 java 中相当于被 final 修饰。这意味着该变量一旦初始化后就不允许再被修改值了(基本类型是值不能被修改,对象类型是引用不能被修改)。{}内的操作就是返回唯一一次初始化的结果。
    by lazy 可以使用于类属性或者局部变量。
    实现原理:(了解附加属性)
    当一个属性 name 需要 by lazy 时,具体是怎么实现的:

    1. 生成一个该属性的附加属性:name$$delegate;
    2. 在构造器中,将使用 lazy(()->T)创建的 Lazy 实例对象赋值给 name$$delegate;
    3. 当该属性被调用,即其 getter 方法被调用时返回 name$delegate.getVaule(),而
      name$delegate.getVaule()方法的返回结果是对象name$delegate内部的_value属性
      值,在 getVaule()第一次被调用时会将_value 进行初始化,往后都是直接将_value 的
      值返回,从而实现属性值的唯一一次初始化。

5.3 Kotlin Tips:怎么用 Kotlin 去提高生产力(kotlin优势)

Tip1- 更简洁的字符串
kotlin除了有单个双引号的字符串,还对字符串加强,引入了三个引号,"""中可以包含换行、反斜杠等等特殊字符;同时,Kotlin中引入了字符串模版,方便字符串的拼接,可以用$符号拼接变量和表达式。注意,在kotlin中,美元符号$是特殊字符,在字符串中不能直接显示,必须经过转义,方法1是用反斜杠,方法二是${‘$’}

Tip2- Kotlin中大多数控制结构都是表达式

首先,需要弄清楚一个概念语句表达式,然后会介绍控制结构表达式的优点:简洁

语句和表达式是什么?

表达式有值,并且能作为另一个表达式的一部分使用语句总是包围着它的代码块中的顶层元素,并且没有自己的值

Kotlin与Java的区别

Java中,所有的控制结构都是语句,也就是控制结构都没有值Kotlin中,除了循环(for、do和do/while)以外,大多数控制结构都是表达式(if/when等)

Example1:if语句

java中,if 是语句,没有值,必须显式的return

public int max(int a, int b) {         if (a > b) {             return a;    } else {             return b;    }}

kotlin中,if 是表达式,不是语句,因为表达式有值,可以作为值return出去,类似于java中的三目运算符a > b ? a : b

fun max(a: Int, b: Int): Int {         return if (a > b) a else b}

上面的if中的分支最后一行语句就是该分支的值,会作为函数的返回值。这其实跟java中的三元运算符类似,

public int max2(int a, int b) {         return a > b ? a : b;}

上面是java中的三元运算符,kotlin中if是表达式有值,完全可以替代,故kotlin中已没有三元运算符了,用if来替代。

上面的max函数还可以简化成下面的形式

fun max2(a: Int, b: Int) = if (a > b) a else b

Example2:when语句
Kotlin中的when非常强大,完全可以取代Java中的switch和if/else,同时,when也是表达式,when的每个分支的最后一行为当前分支的值

// java中的switchpublic String getPoint(char grade) {         switch (grade) {             case 'A':            return "GOOD";        default:            return "UN_KNOW";    }}

java中的switch有太多限制,我们再看看Kotlin怎样去简化的

fun getPoint(grade: Char) = when (grade) {         'A' -> "GOOD"    else -> "UN_KNOW"}

同样的,when语句还可以取代java中的if/else if

Tip3- 更好调用的函数:显式参数名(命名参数)/默认参数值
Kotlin的函数更加好调用,主要是表现在两个方面:
1,显式的标示参数名,可以方便代码阅读;
2,函数可以有默认参数值,可以大大减少Java中的函数重载。

@JvmOverloads
在java与kotlin的混合项目中,会发现用kotlin实现的带默认参数的函数,在java中去调用的化就不能利用这个特性了。这时候可以在kotlin的函数前添加注解@JvmOverloads,添加注解后翻译为class的时候kotlin会帮你去生成多个函数实现函数重载。

Tip4-扩展函数和属性

以项目中StringExt.kt中部分代码示例:

//扩展属性:获取String最后一个字符val String.lastChar: Char    get() = get(length - 1)//扩展函数:字符串保留2位小数fun String.keep2Decimal(): String {         val format = DecimalFormat()    format.maximumFractionDigits = 2    format.isGroupingUsed = false  //不对数字串进行分组    return format.format(this.toDouble())}
var name:String="123.321"//只需import完了就跟使用自己的属性一样方便了。//使用扩展属性val lastChar = name.lastChar//使用扩展函数val keep2Decimal = name.keep2Decimal()

Kotlin为什么能实现扩展函数和属性这样的特性?

在Kotlin中要理解一些语法,只要认识到Kotlin语言最后需要编译为class字节码,Java也是编译为class执行,也就是可以大致理解为Kotlin需要转成Java一样的语法结构,Kotlin就是一种强大的语法糖而已,Java不具备的功能Kotlin也不能越界的

上面的扩展函数转成Java后的代码

/** 扩展函数会转化为一个静态的函数,同时这个静态函数的第一个参数就是该类的实例对象* *//** 获取的扩展属性会转化为一个静态的get函数,同时这个静态函数的第一个参数就是该类的实例对象* */public static final char getLastChar(@NotNull StringBuilder $receiver) {         Intrinsics.checkParameterIsNotNull($receiver, "$receiver");    return $receiver.charAt($receiver.length() - 1);}

对于扩展函数,转化为Java的时候其实就是一个静态的函数
对于扩展属性也类似,获取的扩展属性会转化为一个静态的get函数

可以看到扩展函数和扩展属性适用的地方和缺陷,有两点:

扩展函数和扩展属性内只能访问到类的公有方法和属性,私有的和protected是访问不了的扩展函数不能被override,因为Java中它是静态的函数

Tip5- 懒初始化by lazy 和 延迟初始化lateinit
上面已有详细介绍。

Tip6- 不用再手写findViewById

利用kotlin-android-extensions插件,activity中import对应的布局即可。插件会自动根据布局的id生成对应的View成员。直接拿id用即可。如: tip6Tv.text = “XXXX”

原理是什么?插件帮我们做了什么?

在编译阶段,插件会帮我们生成视图缓存,视图由一个Hashmap结构的_$_findViewCache变量缓存,会根据对应的id先从缓存里查找,缓存没命中再去真正调用findViewById查找出来,再存在HashMap中。另外在onDestroyView会清掉缓存

Fragment需要注意,不能在onCreateView方法里用view,不然会出现空指针异常,需要在onViewCreate里,原理是插件用了getView来findViewById.故在onViewCreated中getView还是空的,原理就好理解了。

Tip7- 利用局部函数抽取重复代码

Kotlin中提供了函数的嵌套,在函数内部还可以定义新的函数。这样我们可以在函数中嵌套这些提前的函数,来抽取重复代码。

Java写法 private void checkIsBlank(){          if (TextUtils.isEmpty(textviewOne.getText())){              throw new IllegalArgumentException("");     }     if (TextUtils.isEmpty(textviewTwo.getText())){              throw new IllegalArgumentException("");     }     if (TextUtils.isEmpty(editText.getText())){              throw new IllegalArgumentException("");     } }Java优化后代码 private void checkIsBlank(){          checkTextView(textviewOne);     checkTextView(textviewTwo);     checkTextView(editText); } private void checkTextView(TextView view){          if (TextUtils.isEmpty(view.getText())){              throw new IllegalArgumentException("");     } }Kotlin写法fun checkIsBlank(){         fun checkTextView(view: TextView){             if (view.text.isNullOrBlank())            throw IllegalArgumentException("")    }    checkTextView(textviewOne)    checkTextView(textviewTwo)    checkTextView(editText)}

Kotlin的写法和Java优化后代码相比,代码量并没有减少,那为什么我们推荐使用局部函数,而不推荐把重复代码提取成一个独立的函数呢?那是因为,在当前代码文件中,我们只有checkIsBlank一个函数使用到了这段重复的代码,别的函数并没有任何相关逻辑代码,所以使用局部函数的话,不仅让重复代码的用途和用处更明确了,函数相关性也大大提高了

Tip8- 使用数据类来快速实现model类

//Kotlin会为类的参数自动实现get set方法class User(val name: String, val age: Int, val gender: Int, var address: String)//用data关键词来声明一个数据类,除了会自动实现get set,还会自动生成equals hashcode toStringdata class User2(val name: String, val age: Int, val gender: Int, var address: String)

Tip9- 用类委托来快速实现装饰器模式
通过继承的实现容易导致脆弱性,例如如果需要修改其他类的一些行为,这时候Java中的一种策略是采用装饰器模式:创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个成员变量。
与原始类拥有相同行为的方法不用修改,只需要直接转发给原始类的实例。如下所示:

* 常见的装饰器模式,为了修改部分的函数,却需要实现所有的接口函数* */class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> {         var objectAdded = 0        override val size: Int        get() = innerSet.size    /*    * 需要修改的方法    * */    override fun add(element: T): Boolean {             objectAdded++        return innerSet.add(element)    }    /*    * 需要修改的方法    * */    override fun addAll(elements: Collection<T>): Boolean {             objectAdded += elements.size        return innerSet.addAll(elements)    }    override fun contains(element: T): Boolean {             return innerSet.contains(element)    }    override fun containsAll(elements: Collection<T>): Boolean {             return innerSet.containsAll(elements)    }    override fun isEmpty(): Boolean {             return innerSet.isEmpty()    }    override fun clear() {             innerSet.clear()    }    override fun iterator(): MutableIterator<T> {             return innerSet.iterator()    }    override fun remove(element: T): Boolean {             return innerSet.remove(element)    }    override fun removeAll(elements: Collection<T>): Boolean {             return innerSet.removeAll(elements)    }    override fun retainAll(elements: Collection<T>): Boolean {             return innerSet.retainAll(elements)    }}`

如上所示,想要修改HashSet的某些行为函数add和addAll,需要实现MutableCollection接口的所有方法,将这些方法转发给innerSet去具体的实现。虽然只需要修改其中的两个方法,其他代码都是模版代码。
只要是重复的模版代码,Kotlin这种全新的语法糖就会想办法将它放在编译阶段再去生成。
这时候可以用到类委托by关键字,如下所示:

/** 通过by关键字将接口的实现委托给innerSet成员变量,需要修改的函数再去override就可以了* */class CountingSet2<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerSet {         var objectAdded = 0    override fun add(element: T): Boolean {             objectAdded++        return innerSet.add(element)    }    override fun addAll(elements: Collection<T>): Boolean {             objectAdded += elements.size        return innerSet.addAll(elements)    }}

通过by关键字将接口的实现委托给innerSet成员变量,需要修改的函数再去override就可以了,通过类委托将10行代码就可以实现上面接近100行的功能,简洁明了,去掉了模版代码。

Tip10- Lambda表达式简化OnClickListener

Tip11- kotlin常见内联扩展函数来简化代码

内联扩展函数之let
let扩展函数的实际上是一个作用域函数
场景一: 针对一个可null的对象统一做判空处理。
场景二: 明确一个变量所处特定的作用域范围内。

内联函数之with
适用于调用同一个类的多个方法时,可以省去类名重复,直接调用类的方法即可。经常用于Android中RecyclerView中onBinderViewHolder中,数据model的属性映射到UI上
使用前:

使用后:

内联扩展函数之run
适用于let,with函数任何场景因为run函数是let,with两个函数结合体,准确来说它弥补了let函数在函数体内必须使用it参数替代对象,在run函数中可以像with函数一样可以省略,直接访问实例的公有属性和方法,另一方面它弥补了with函数传入对象判空问题,在run函数中可以像let函数一样做判空处理
借助上个例子,
使用前:

使用后:

内联扩展函数之apply

整体作用功能和run函数很像,唯一不同点就是它返回的值是对象本身,而run函数是一个闭包形式返回,返回的是最后一行的值。正是基于这一点差异它的适用场景稍微与run函数有点不一样。
apply一般用于一个对象实例初始化的时候,需要对对象中的属性进行赋值。或者动态inflate出一个XML的View的时候需要给View绑定数据也会用到,这种情景非常常见。特别是在我们开发中会有一些数据model向View model转化实例化的过程中需要用到。

 menuAdapter = HomeMenuAdapter().apply {                 bindToRecyclerView(recyclerView_menu)            setOnItemClickListener {      _, _, position ->                when (position) {                         3 -> ActivityManager.start(ActivateActivity::class.java)                }            }        }

内联扩展函数之also
also函数的结构实际上和let很像唯一的区别就是返回值的不一样,let是以闭包的形式返回,返回函数体内最后一行的值,如果最后一行为空就返回一个Unit类型的默认值。而also函数返回的则是传入对象的本身
适用于let函数的任何场景,also函数和let很像,只是唯一的不同点就是let函数最后的返回值是最后一行的返回值而also函数的返回值是返回当前的这个对象。一般可用于多个扩展函数链式调用

总结:(extension指是否为扩展函数)

Tip11- 高阶函数简化代码
高阶函数:以另一个函数作为参数或者返回值的函数

5.4 Kotlin数组和集合

5.4.1 kotlin数组

kotlin为数组增加了一个Array类,为元素是基本类型的数组增加了xxArray类(其中xx也就是Byte,Short, Int等基本类型)

Kotlin创建数组大致有如下两种方式:

1.使用arrayOf(), arrayOfNulls(),emptyArray()工具函数。

 var model1=CommonModel()    var model2=CommonModel()    var model3=CommonModel()    //创建包含指定元素的数组,相当于java数组的静态初始化    var a= arrayOf(model1,model2,model3)    var b= intArrayOf(1,2,3)    //创建指定长度为3,元素为null的数组,相当于java数组动态初始化    var c= arrayOfNulls<Int>(3)

2.使用Array(size: Int, init:(Int) -> T)
第一个参数就是数组的大小。第二个参数是一个函数, init:(Int) -> T 代表这这个方法返回的类型是T只能有一个参数类型是Int型。Int就是该array的所对应的索引。

private fun arrInit(): (Int) -> Int = {      it * 2 }    var array1 = Array<Int>(5, arrInit())    var array2 = Array<Int>(5, {     it})

5.4.2 kotlin集合

kotlin集合类同样有两个接口派生:CollectionMap。但Kotlin的结合被分成两个大类,可变集合和不可变集合。只有可变集合才可以添加修改,删除等处理操作。不可变集合只能读取元素。

kotlin只提供了HashSet,HashMap, LinkedHashSet, LinkedHashMap, ArrayList这5个集合实现类,而且他们都是可变集合,那么说好的不可变集合呢。kotlin的不可变集合类并没有暴露出来,我们只能通过函数来创建不可变集合。

list集合

创建一个不可变的list

val mList = listOf<Int>(1, 2, 3)

创建一个可变的list

val mList = mutableListOf<Int>(1, 2, 3)

emptyList()——创建一个空集合
listOfNotNull ()—— 创建的集合中不能插入null值

Map
创建一个不可变的Map

val mList = mapOf(Pair("key1", 1), Pair("key2", 2))

或者

//推荐val mList = mapOf("key1" to 1, "key2" to 2)

创建一个可变的Map

val mList = mutableMapOf("key1" to 1, "key2" to 2)

此外还有

emptyMap()——创建一个空map
hashMapOf()——创建一个hashMap
linkedMapOf()——创建一个linkedMap
sortedMapOf()——创建一个sortedMap

5.5 Kotlin集合操作符

Kotlin中关于集合的操作符有六类:

总数操作符过滤操作符映射操作符顺序操作符生产操作符元素操作符
  1. 总数操作符
    any —— 判断集合中 是否有满足条件 的元素;
    all —— 判断集合中的元素 是否都满足条件;
    none —— 判断集合中是否 都不满足条件,是则返回true;
    count —— 查询集合中 满足条件 的 元素个数;
    reduce —— 从 第一项到最后一项进行累计 ;
    reduceRight —— 从 最后一下到第一项进行累计;
    fold —— 与reduce类似,不过有初始值,而不是从0开始累计;
    foldRight —— 和reduceRight类似,有初始值,不是从0开始累计;
    forEach —— 循环遍历元素,元素是it,可对每个元素进行相关操作;
    forEachIndexed —— 循环遍历元素,同时得到元素index(下标);
    max —— 查询最大的元素,如果没有则返回null;
    maxBy —— 获取方法处理后返回结果最大值对应的那个元素的初始值,如果没有则返回null;
    min —— 查询最小的元素,如果没有则返回null;
    minBy —— 获取方法处理后返回结果最小值对应那个元素的初始值,如果没有则返回null;
    sumBy —— 获取 方法处理后返回结果值 的 总和;
    dropWhile —— 返回从第一项起,去掉满足条件的元素,直到不满足条件的一项为止

  2. 过滤操作符
    过滤后会返回一个处理后的列表结果,但不会改变原列表!!!
    filter —— 过滤 掉所有 满足条件 的元素
    filterNot —— 过滤所有不满足条件的元素
    filterNotNull —— 过滤NULL
    take —— 返回从第一个开始的n个元素
    takeLast —— 返回从最后一个开始的n个元素
    takeWhile —— 返回不满足条件的下标前面的所有元素的集合
    drop —— 返回 去掉前N个元素后 的列表
    dropLastWhile —— 返回从最后一项起,去掉满足条件的元素,直到不满足条件的一项为止
    slice —— 过滤掉 非指定下标 的元素,即保留下标对应的元素过滤list中
    指定下标的元素(比如这里只保留下标为1,3,4的元素)

  3. 映射操作符
    map —— 将集合中的元素通过某个 方法转换 后的结果存到一个集合中;
    mapIndexed —— 除了得到 转换后的结果 ,还可以拿到Index(下标);
    mapNotNull —— 执行方法 转换前过滤掉 为 NULL 的元素
    flatMap —— 合并两个集合,可以在合并的时候做些小动作;
    groupBy —— 将集合中的元素按照某个条件分组,返回Map;

  4. 顺序操作符
    reversed —— 相反顺序
    sorted —— 自然排序(升序)
    sortedBy —— 根据方法处理结果进行自然(升序)排序
    sortedDescending —— 降序排序
    sortedByDescending —— 根据方法处理结果进行降序排序

  5. 生产操作符
    zip —— 两个集合按照下标组合成一个个的Pair塞到集合中返回
    partition —— 根据判断条件是否成立,拆分成两个 Pair
    plus —— 合并两个List,可以用"+"替代
    unzip —— 将包含多个Pair的List 转换成 含List的Pair

  6. 元素操作符
    contains —— 判断集合中是否有指定元素,有返回true
    elementAt —— 查找下标对应的元素,如果下标越界会抛IndexOutOfBoundsException
    elementAtOrElse —— 查找下标对应元素,如果越界会根据方法返回默认值(最大下标经方法后的值)
    elementAtOrNull —— 查找下标对应元素,越界会返回Null
    first —— 返回符合条件的第一个元素,没有 抛NoSuchElementException
    firstOrNull —— 返回符合条件的第一个元素,没有 返回null
    indexOf —— 返回指定下标的元素,没有 返回-1
    indexOfFirst —— 返回第一个符合条件的元素下标,没有 返回-1
    indexOfLast —— 返回最后一个符合条件的元素下标,没有 返回-1
    last —— 返回符合条件的最后一个元素,没有 抛NoSuchElementException
    lastIndexOf —— 返回符合条件的最后一个元素,没有 返回-1
    lastOrNull —— 返回符合条件的最后一个元素,没有 返回null
    single —— 返回符合条件的单个元素,如有没有符合或超过一个,抛异常
    singleOrNull —— 返回符合条件的单个元素,如有没有符合或超过一个,返回null

5.6 说一下Kotlin的伴生对象(关键字companion)

在Java中可以通过static关键字声明静态的属性或方法。但是在Kotlin中并没有延续这个关键字,而是使用伴生对象实现,在class内部声明一个companion object代码块,其内部的成员变量和方法都将被编译为静态的。

class TestStatic {         //伴生对象    companion object Factory {             val str: String = ""        fun create(): TestStatic {                 println(this)            return TestStatic()        }    }}

Factory为最终生成的静态内部类类名,通常来说Factory名字可以省略,如果省略,类名为默认的Companion。

Kotlin中的也可写静态代码块,只需在companion object中嵌套一个init代码块。

companion object {         //静态代码块    init {             val a = "adc"    }}

注意事项

一个类中最多只能有一个companion object代码块。伴生对象本质上就是一个静态内部类,所以它还能继承其他类。

5.7.Kotlin 顶层函数和属性

创建一个文件写需要的属性或方法。在其它地方直接,import 包名.函数名来导入我们将要使用的函数,然后就可以直接使用了。Kotlin中通过使用顶层函数和顶层属性帮助我们消除了Java中常见的静态工具类,使我们的代码更加整洁,值得一试。

5.8 协程(Coroutines=cooperation+routines)

5.8.1 协程是什么

Kotlin 官方文档说「本质上,协程是轻量级的线程」。

进程,线程,协程的抽象概念

  1. 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。
  2. 线程是操作系统能够进行调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程是在进程下,所以同一进程下的多个线程是能共享资源的。
  3. 协程是单线程下实现多任务,它通过 yield 关键字来实现,能有效地减少多线程之间切换的开销。它是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

协程基本使用

// 方法一,使用 runBlocking 顶层函数// 通常适用于单元测试的场景,而业务开发中不会用到这种方法runBlocking {     getImage(imageId)}// 方法二,使用 GlobalScope 单例对象//和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和app一致,且不能取消GlobalScope.launch {     getImage(imageId)}// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象//推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的context和Android里的不是一个东西,是一个更通用的概念,会有一个Android平台的封装来配合使用)。val coroutineScope = CoroutineScope(context)coroutineScope.launch {     getImage(imageId)}

协程最常用的功能是并发,而并发的典型场景就是多线程。可以使用 Dispatchers.IO 参数把任务切到 IO 线程执行:

coroutineScope.launch(Dispatchers.IO) {      ...}

也可以使用 Dispatchers.Main 参数切换到主线程:

coroutineScope.launch(Dispatchers.Main) {      ...}

异步请求的例子完整写出来是这样的:

coroutineScope.launch(Dispatchers.Main) {      // 在主线程开启协程val user = api.getUser() // IO 线程执行网络请求nameTv.text = user.name // 主线程更新 UI}

而通常用java来写,是少不了回调方法的。

协程的「1 到 0」

多层网络请求:

coroutineScope.launch(Dispatchers.Main) {      // 开始协程:主线程val token = api.getToken() // 网络请求:IO 线程val user = api.getUser(token) // 网络请求:IO 线程nameTv.text = user.name // 更新 UI:主线程}

如果遇到的场景是多个网络请求需要等待所有请求结束之后再对 UI 进行更新。
比如以下两个请求:

api.getAvatar(user, callback)api.getCompanyLogo(user, callback)

如果使用回调式的写法,本来能够并行处理的请求被强制通过串行的方式去实现,可能会导致等待时间长了一倍,也就是性能差了一倍:

api.getAvatar(user) {      avatar ->api.getCompanyLogo(user) {      logo ->show(merge(avatar, logo)) }}

而如果使用协程,可以直接把两个并行请求写成上下两行,最后再把结果进行合
并即可:

coroutineScope.launch(Dispatchers.Main) {     val avatar = async {      api.getAvatar(user) } // 获取用户头像val logo = async {      api.getCompanyLogo(user) } // 获取用户所在公司的 logoval merged = suspendingMerge(avatar, logo) // 合并结果show(merged) // 更新 UI}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码。需要注意的是 suspendingMerge 并不是协程 API 中提供的方法,而是我们自定义的一个可「挂起」的结果合并方法。至于挂起具体是什么,可以看后面。

让复杂的并发代码,写起来变得简单且清晰,是协程的优势。
这里,两个没有相关性的后台任务,因为用了协程,被安排得明明白白,互相之间配合得很好,也就是我们之前说的「协作式任务」。本来需要回调,现在直接没有回调了,这种从 1 到 0 的设计思想真的妙哉。

5.8.2 suspend

suspend 是 Kotlin 协程最核心的关键字。代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。
创建协程的函数:

• launch• runBlocking• async

runBlocking 通常适用于单元测试的场景,而业务开发中不会用到这个函数,因为它是线程阻塞的。

接下来我们主要来对比 launch 与 async 这两个函数。

• 相同点:它们都可以用来启动一个协程,返回的都是 Coroutine,我们这里不需要纠结具体是返回哪个类。• 不同点:async 返回的 Coroutine 多实现了 Deferred 接口。

Deferred的意思就是延迟,也就是结果稍后才能拿到。
我们调用 Deferred.await() 就可以得到结果了。

看看 async 是如何使用的:

coroutineScope.launch(Dispatchers.Main) {     val avatar: Deferred = async {      api.getAvatar(user) } // 获取用户头像val logo: Deferred = async {      api.getCompanyLogo(user) } // 获取用户所在公司的 logoshow(avatar.await(), logo.await()) // 更新 UI }

可以看到 avatar 和 logo 的类型可以声明为 Deferred ,通过 await 获取结果并且更新到 UI 上显示。

await 函数签名:

public suspend fun await(): T

前面有个关键字是—— suspend

5.8.3 「挂起」的本质

协程中「挂起」的对象到底是什么?挂起线程,还是挂起函数?
都不对,我们挂起的对象是协程

协程可以使用 launch 或者 async 函数,协程其实就是这两个函数中闭包的代码块。launch ,async 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被「suspend」,也就是被挂起。

那此时又是从哪里挂起?从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。

注意,不是这个协程停下来了!是脱离,当前线程不再管这个协程要去做什么了。

suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的suspend 函数的时候,暂时不继续执行协程代码了。

互相脱离的线程和协程接下来将会发生什么事情:

举例:获取一个图片,然后显示出来:

// 主线程中GlobalScope.launch(Dispatchers.Main) {     val image = suspendingGetImage(imageId) // 获取图片avatarIv.setImageBitmap(image) // 显示出来} suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {      ...

这段执行在主线程的协程,它实质上会往你的主线程 post 一个 Runnable,这个 Runnable 就是你的协程代码:

handler.post {     val image = suspendingGetImage(imageId)avatarIv.setImageBitmap(image)}

线程:
如果它是一个后台线程:
• 要么无事可做,被系统回收
• 要么继续执行别的后台任务
跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。
如果它是 Android 的主线程,那它接下来就会继续回去工作:也就是
一秒钟 60 次的界面刷新任务。

协程:

线程的代码在到达 suspend 函数的时候被掐断,接下来协程会从这个 suspend 函数开始继续往下执行,不过是在指定的线程。
谁指定的?是 suspend 函数指定的,比如我们这个例子中,函数内部的 withContext 传入的 Dispatchers.IO 所指定的 IO 线程。

Dispatchers:调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行,关于 Dispatchers 这里先不展开了。
常用的 Dispatchers ,有以下三种:

• Dispatchers.Main:Android 中的主线程• Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求• Dispatchers.Default:适合 CPU 密集型的任务,比如计算

回到我们的协程,它从 suspend 函数开始脱离启动它的线程,继续执行在 Dispatchers 所指定的 IO 线程。在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。

结论:
协程在执行到有 suspend 标记的函数的时候,会被suspend也就是被挂起,就是切个线程;不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程。再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作

5.8.4 suspend 的意义?

随便写一个自定义的 suspend 函数:

suspend fun suspendingPrint() {     println("Thread: ${Thread.currentThread().name}") } System.out: Thread: main

输出的结果还是在主线程。
为什么没切换线程?因为它不知道往哪切,需要我们告诉它。

对比之前例子中 suspendingGetImage 函数代码:

suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {      ... }

通过 withContext 源码可以知道,它本身就是一个挂起函数,它接收一个 Dispatcher 参数,依赖这个 Dispatcher 参数的指示,你的协程被挂起,然后切到别的线程。所以这个 suspend,其实并不是起到把任何把协程挂起,或者说切换线程的作用。真正挂起协程这件事,是 Kotlin 的协程框架帮我们做的。所以我们想要自己写一个挂起函数,仅仅只加上 suspend 关键字是不行的,还需要函数内部直接或间接地调用到 Kotlin 协程框架自带的 suspend 函数才行。

这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?

它其实是一个提醒。

函数的创建者对函数的使用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。为什么 suspend 关键字并没有实际去操作挂起,但 Kotlin 却把它提供出来?因为它本来就不是用来操作挂起的。挂起的操作 —— 也就是切线程,依赖的是挂起函数里面的实际代码,而不是这个关键字。

所以suspend关键字,只是一个提醒。

你创建一个 suspend 函数但它内部不包含真正的挂起逻辑,编译器会给你一个提醒:redundant suspend modifier,告诉你这个 suspend 是多余的。所以,创建一个 suspend 函数,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 函数,你的这个 suspend 才是有意义的。

5.8.5 到底什么是「非阻塞式」挂起?协程真的更轻量级吗?

什么是「非阻塞式挂起」?

线程中的阻塞式:在单线程情况下,在单线程下执行耗时操作是会阻塞线程的,如果在多线程情况下,那么此时的线程也是非阻塞式的。

非阻塞式是相对阻塞式而言的。 Kotlin 协程在单协程的情况下也是非阻塞式 的,因为它可以利用挂起函数来切换线程。(阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。)即:协程可以用看起来阻塞的代码写出非阻塞式的操作

阻塞的本质?
首先,所有的代码本质上都是阻塞式的,而只有比较耗时的代码才会导致人类可感知的等待,比如在主线程上做一个耗时 50 ms 的操作会导致界面卡掉几帧,这种是我们人眼能观察出来的,而这就是我们通常意义所说的「阻塞」。举个例子,当你开发的 app 在性能好的手机上很流畅,在性能差的老手机上会卡顿,就是在说同一行代码执行的时间不一样。视频中讲了一个网络 IO 的例子,IO 阻塞更多是反映在「等」这件事情上,它的性能瓶颈是和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。而这跟协程半毛钱关系没有,切线程解决不了的事情,协程也解决不了。

总结:

• 协程就是切线程;• 挂起就是可以自动切回来的切线程;• 挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作,就这么简单。

六、jetpack

Android Jetpack 组件是库的集合,这些库是为协同工作而构建的,不过也可以单独采用。
包含:
1.Foundation(基础组件)

AppCompat   Android KTX MutidexTest

基础组件提供了跨领域的功能,如向后兼容性、测试和Kotlin语言支持
2.Architecture(架构组件)(非常重要)
架构组件这个比较关键,是我们要学习的重点包括:

Data Binding 数据绑定Lifecycle 管理Activity和Fragment生命周期LiveData 感知数据变化更新uiNavigation 字意为导航。多Fragment转场,栈管理Paging 分页处理Room 数据库管理ViewModel ui界面的数据管理WorkManager 后台工作管理

3.Behavior(行为组件)
行为组件可帮助开发者的应用与标准 Android 服务(如通知、权限、分享和 Google 助理)相集成。

CameraX:帮助开发者简化相机应用的开发工作。它提供一致且易于使用的 API 界面,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)DownloadManager(下载管理器):可处理长时间运行的HTTP下载,并在出现故障或在连接更改和系统重新启动后重试下载。Media & playback(媒体&播放):用于媒体播放和路由(包括 Google Cast)的向后兼容 APINotifications(通知):提供向后兼容的通知 API,支持 Wear 和 Auto。Permissions(权限):用于检查和请求应用权限的兼容性 API。Preferences(偏好设置):提供了用户能够改变应用的功能和行为能力。Sharing(共享):提供适合应用操作栏的共享操作。Slices(切片):创建可在应用外部显示应用数据的灵活界面元素。

4.UI(界面组件)
界面组件可提供各类view和辅助程序,让应用不仅简单易用,还能带来愉悦体验。它包含如下组件库:

Animation & Transitions(动画&过度):提供各类内置动画,也可以自定义动画效果。Emoji(表情符号):使用户在未更新系统版本的情况下也可以使用表情符号。Fragment:组件化界面的基本单位。Layout(布局):xml书写的界面布局或者使用Compose完成的界面。Palette(调色板):从调色板中提取出有用的信息。

所以,重点是AAC架构(Adroid Architecture Components,即Android系统架构组件

Lifecycle :能够帮我们轻松的管理 Activity/Fragment 的生命周期问题,能够让我们以一种更加解耦的方式处理生命周期的变化问题,以及轻松避免内存泄露;
原理:
1.Lifecycle 库通过在 SupportActivity 的 onCreate 中注入 ReportFragment 来感知发生命周期;
2.Lifecycle 抽象类,是 Lifecycle 库的核心类之一,它是对生命周期的抽象,定义了生命周期事件以及状态,通过它我们可以获取当前的生命周期状态,同时它也奠定了观察者模式的基调;

LiveData :基于观察者模式,并且感知生命周期、可观察的数据持有类,它被设计成 ViewModel 的一个成员变量;可以以一个 更解耦 的方式来共享数据。
1.LiveData 的实现基于观察者模式;
2.LiveData 跟 LifecycleOwner 绑定,能感知生命周期变化,并且只会在 LifecycleOwner 处于 Active 状态(started/resumed)下通知数据改变(避免不必要的数据刷新);
3.LiveData 会自动在 destroyed的状态下移除 Observer ,取消订阅,所以不用担心内存泄露;

ViewModel:管理跟UI相关的数据, 并且能够感知生命周期;另外 ViewModel 能够在配置改变的情况下让数据得以保留。ViewModel 重在以感知生命周期的方式 管理界面相关的数据。
我们知道类似旋转屏幕等配置项改变会导致我们的 Activity 被销毁并重建,此时 Activity 持有的数据就会跟随着丢失,而ViewModel 则并不会被销毁,从而能够帮助我们在这个过程中保存数据,而不是在 Activity 重建后重新去获取。并且 ViewModel 能够让我们不必去担心潜在的内存泄露问题,同时 ViewModel 相比于用onSaveInstanceState() 方法更有优势,比如存储相对大的数据,并且不需要序列化以及反序列化。
ViewModel 原理总结
通过注入一个 retainInstance 为true 的 HolderFragment ,利用 Fragment 的特性来保证在 Activity 配置改变后依然能够存活下来,并且保证了 HolderFragment 内部的 ViewModelStore 的存活,最终保证了 ViewModelStore 内部储存的 ViewModel 缓存存活,从而实现 ViewModel 在 Activity 配置改变的情况下不销毁的功能。
ViewModel 的使用注意事项
不要持有 Activity :ViewModel 不会因为 Activity 配置改变而被销毁,所以绝对不要持有那些跟 Activity 相关的类,比如Activity 里的某个 View,让 ViewModel 持有 Activity 会导致内存泄露,还要注意的是连 Lifecycle 也不行;
不能访问 UI :ViewModel 应该只负责管理数据,不能去访问 UI,更不能持有它;

七、网络

7.1.http/https

http与https的区别?https是如何工作的?

http 是超文本传输协议,而 https 可以简单理解为安全的 http 协议。https 通过在 http 协议下添加了一层 ssl 协议对数据进行加密从而保证了安全。https 的作用主要有两点:建立安全的信息传输通道,保证数据传输安全;确认网站的真实性。

http 与 https 的区别主要如下:

  1. https 需要到 CA 申请证书,很少免费,因而需要一定的费用
  2. http 是明文传输,安全性低;而 https 在 http 的基础上通过 ssl 加密,安全性高
  3. http 使用的默认端口是80;https使用的默认端口是 443

7.2 Tcp和udp

1. 基于连接与无连接;2. 对系统资源的要求(TCP较多,UDP少);3. UDP程序结构较简单;4. 流模式与数据报模式 ;5. TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。

TCP概述:TCP把连接作为最基本的对象,每一条TCP连接都有两个端点,这种断点我们叫作套接字(socket),它的定义为端口号拼接到IP地址即构成了套接字,例如,若IP地址为192.3.4.16 而端口号为80,那么得到的套接字为192.3.4.16:80。

为什么建立连接是三次握手,关闭连接确是四次挥手呢?

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

详细可以阅读两张动图-彻底明白TCP的三次握手与四次挥手
建议阅读:网络面试题

八、设计模式

8.1mvc/mvp/mvvm

MVC:Model-View-Controller,是一种分层解偶的框架,Model层提供本地数据和网络请求,View层处理视图,Controller处理逻辑,存在问题是Controller层和View层的划分不明显,Model层和View层的存在耦合。

MVP:Model-View-Presenter,是对MVC的升级,Model层和View层与MVC的意思一致,但Model层和View层不再存在耦合,而是通过Presenter层这个桥梁进行交流。

MVVM:Model-View-ViewModel,不同于上面的两个框架,ViewModel持有数据状态,当数据状态改变的时候,会自动通知View层进行更新。

MVC和MVP的区别是什么?

MVP是MVC的进一步解耦,简单来讲,在MVC中,View层既可以和Controller层交互,又可以和Model层交互;而在MVP中,View层只能和Presenter层交互,Model层也只能和Presenter层交互,减少了View层和Model层的耦合,更容易定位错误的来源。

MVVM和MVP的最大区别在哪?

MVP中的每个方法都需要你去主动调用,它其实是被动的,而MVVM中有数据驱动这个概念,当你的持有的数据状态发生变更的时候,你的View你可以监听到这个变化,从而主动去更新,这其实是主动的。

严格来说这三种都不是设计模式,只能算是框架,或者一种思想。每种模式也没有严格定义,不同的人有不同的理解

8.2 常见设计模式

8.2.1.设计模式的六大原则:

单一职责:合理分配类和函数的职责
开闭原则:开放扩展,关闭修改
里式替换:继承
依赖倒置:面向接口
接口隔离:控制接口的粒度
迪米特:一个类应该对其他的类了解最少

8.2.2.单例模式

关键点:某个类只能有一个实例,提供一个全局的访问点。

恶汉式:

public class SingleInstance {     //有一个静态属性instance,在JVM虚拟机装载类信息的时候,会new SingleInstance()对其进行初始化。//线程安全,但是没有延迟加载,浪费资源    private SingleInstance() {     }//构造函数私有化    private final static SingleInstance instance=new SingleInstance();    public static  SingleInstance getInstance() {             return instance;    }}

懒汉式

public class SingleInstance {         private SingleInstance() {     }    private  static SingleInstance instance;    public static SingleInstance getInstance() {         //假如2个线程同时进入if 语句,会创建两个对象,线程不安全.        if (instance==null){                 instance=new SingleInstance();        }        return instance;    }}

改进:

public class SingleInstance {         private SingleInstance() {     }    private  static SingleInstance instance;    public static synchronized SingleInstance getInstance() {             if (instance==null){                 instance=new SingleInstance();        }        return instance;    }}//或者public class SingleInstance {         private SingleInstance() {     }    private  static SingleInstance instance;    public static  SingleInstance getInstance() {             synchronized(SingleInstance.class){                 if (instance==null){                     instance=new SingleInstance();            }        }        return instance;    }}

synchronized 是比较耗费性能的,我们每次调用这个 getInstance()方法的时候,都会进入 synchronized 包裹的代码块内,即使这个时候单例对象已经生成,不再需要创建对象也会进入 synchronized 内部,造成不必要的同步开销。

双重锁定(DCL模式)(重要)

public class SingleInstance {         private SingleInstance() {     }    private  static SingleInstance instance;    public static  SingleInstance getInstance() {             if (instance==null){                 synchronized(SingleInstance.class){                     if (instance==null){                         instance=new SingleInstance();                }            }        }        return instance;    }}

看起来已经能够实现懒加载和线程安全了,但是还存在一个问题,那就是没有考虑到 JVM 编译器的指令重排序.

DCL模式会有什么问题?

对象生成实例的过程中,大概会经过以下过程:

  1. 为对象分配内存空间。
  2. 初始化对象中的成员变量。
  3. 将对象指向分配的内存空间(此时对象就不为null)。

由于Jvm会优化指令顺序,也就是说2和3的顺序是不能保证的。在多线程高并发的情况下,当一个线程完成了1、3过程后,当前线程的时间片已用完,这个时候会切换到另一个线程,另一个线程调用这个单例,会使用这个还没初始化完成的实例

解决方法是使用volatile关键字:

优化后的DCL模式(非常重要)

public class SingleInstance {     private static volatile SingleInstance instance;//volatile 可以禁止指令重排序private SingleInstance() {     }public static SingleInstance getInstance() {     if(instance == null) {     synchronized (SingleInstance.class) {     if(instance == null) {     instance = new SingleInstance();}}}return instance;}}

上面的分析,会发现懒加载和线程安全是我们自己通过加锁和 volatile 关键字实现的,那么有没有让 JVM 帮我们实现线程安全和懒加载呢?

静态内部类单例(非常重要)

public class SingleInstance {     private SingleInstance() {     }public static SingleInstance getInstance() {     return SingleHolder.instance;}private static class SingleHolder{     //静态初始化器,由JVM来保证线程安全private static final SingleInstance instance = new SingleInstance();}}

首先在 JVM 进行类加载的时候,只是加载了 SingleInstance 类,并不会去执行其中的静态方法,也不会去加载 SingleInstance 内的静态内部类 SingleHolder。所以也就是并不会在初次类加载的时候创建单例对象。
在我们使用getInstance()的时候,我们使用 SingleHolder的静态属性,这个时候会对 SingleHolder 这个静态内部类进行加载,这个时候,就回到了第一种写法 饿汉式中的原理,在类加载的初始化阶段,会对创建单例对象,并且赋值给 INSTANCE 属性。同样,这些操作是发生在类加载阶段的,由 JVM 保证了线程安全,并且是在使用的时候进行加载的,也实现了懒加载。

缺点就是:初始化的时候没法传值给单例类。这个时候就可以使用上面优化后的DCL模式。

枚举单例

public enum SingletonEnum {     INSTANCE}

// 获取单例对象

SingletonEnum .INSTANCE// 假如枚举类中有一个方法 getString(),就可以这样调用SingletonEnum .INSTANCE.getString()

缺点:

不能懒加载运行时占用内存比非枚举的大很多

8.2.3.建造者模式

关键点:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示,适用于初始化的对象比较复杂且参数较多的情况。

Retrofit 和 OkHttp 等开源库,大多都是采用的构造者模式去实现的。

这里以AlertDialog举例:

public class AlertDialog extends Dialog implements DialogInterface {         ···    public static class Builder {             private final AlertController.AlertParams P;        ···        public Builder(Context context) {                 this(context, resolveDialogTheme(context, ResourceId.ID_NULL));        }        ···        public Builder setTitle(CharSequence title) {                 P.mTitle = title;            return this;        }        ···        public Builder setMessage(CharSequence message) {                 P.mMessage = message;            return this;        }        ···        public AlertDialog create() {                 // Context has already been wrapped with the appropriate theme.            final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);            P.apply(dialog.mAlert);            ···            return dialog;        }        ···    }}
AlertDialog.Builder builder = new AlertDialog.Builder(this)        .setTitle("Title")        .setMessage("Message");AlertDialog dialog = builder.create();dialog.show();

8.2.4.责任链模式

关键点:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
举例:
1.View的事件分发

a). 事件收集之后最先传递给 Activity, 然后依次向下传递,大致如下:

Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View

b). 如果没有任何View消费掉事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃:

Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View

这是一个非常经典的责任链模式,如果我能处理就拦截下来自己干,如果自己不能处理或者不确定就交给责任链中下一个对象。

2.Okhttp源码中的责任链模式

OkHttp 的拦截器就是基于责任链模式,每个节点有自己的职责,同时可以选择是否把任务传递给下一个环节

2.1 Inteceptor
主要方法 Intercept。会传递一个 Chain 对象过来,可以在 Chain 在执行 proceed 的前后添加代码。

2.2 Chain
主要方法 proceed。OkHttp 的唯一实现类是 RealInterceptorChain。内部维护了所有要执行的拦截器列表,在 proceed 内部会唤醒下一个 Interceptor ,调用 intercept 来进行下一步:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpStream httpStream,    Connection connection) throws IOException {       ...  RealInterceptorChain next = new RealInterceptorChain(      interceptors, streamAllocation, httpStream, connection, index + 1, request);  Interceptor interceptor = interceptors.get(index);  Response response = interceptor.intercept(next);  ...  return response;}

可以看到,RealInterceptorChain的process方法中,会生成一个RealInterceptorChain对象,且注意到index+1,即生成下一个Chain对象,并且同时获取拦截器集合里的下一个拦截器,调用它的intercept,将下一个Chain(next)作为参数传给他去处理,回顾到刚才上面说的,拦截器的interpect里面调用了chain的process,也就是说,每一个拦截器都会持有下一个拦截器的chain对象,并通过chain的process方法,触发RealInterceptorChain里的index下标再+1,从而串联起整个拦截链。

8.2.5.观察者模式

关键点:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。(被观察者可以添加一堆观察者,被观察着发生变化,观察者做出响应)

Android中我们遇到的最常用的观察者模式

  1. 各种控件的监听,如下:

     //注册观察者 button.setOnClickListener(new View.OnClickListener() {               //观察者实现        @Override        public void onClick(View arg0) {                   Log.d("test", "Click button ");        }    });

    button就是被观察者;new出来的View.OnClickListenerd对象就是具体的观察者。在这里OnClickListener是个接口,也就是抽象观察者;通过setOnClickListener把观察者注册到被观察者中。

    一旦button捕获的点击事件就会通过回调注册的OnClickListener观察者的onClick方法会来通知观察者。

  2. Adapter的notifyDataSetChanged()方法
    当我们使用ListView时,需要更新数据时我们就会调用Adapter的notifyDataSetChanged()方法,那么我们来看看notifyDataSetChanged()的实现原理,这个方法是定义在BaseAdaper中,具体代码如下:

    public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {            //数据集被观察者    private final DataSetObservable mDataSetObservable = new DataSetObservable();    //注册观察者    public void registerDataSetObserver(DataSetObserver observer) {               mDataSetObservable.registerObserver(observer);    }    //注销观察者    public void unregisterDataSetObserver(DataSetObserver observer) {               mDataSetObservable.unregisterObserver(observer);    }    //数据集改变时,通知所有观察者    public void notifyDataSetChanged() {               mDataSetObservable.notifyChanged();    }}    //其他代码略

    上面的代码可以看出BaseAdapter实际上就是使用了观察者模式,BaseAdapter就是具体的被观察者。接下来看看 mDataSetObservable.notifyChanged()的实现:
    //数据集被观察者

    public class DataSetObservable extends Observable<DataSetObserver> {              public void notifyChanged() {               synchronized(mObservers) {                   //遍历所有观察者,并调用他们的onChanged()方法            for (int i = mObservers.size() - 1; i >= 0; i--) {                       mObservers.get(i).onChanged();            }        }    }    //其他代码略}

    AdapterDataSetObserver类中的onChanged()方法没看出啥,继续看他父类的onChanged()方法:

    class AdapterDataSetObserver extends DataSetObserver {               private Parcelable mInstanceState = null;        //观察者的核心实现        @Override        public void onChanged() {                   mDataChanged = true;            mOldItemCount = mItemCount;            mItemCount = getAdapter().getCount();//获取Adapter中的数据的数量            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null                    && mOldItemCount == 0 && mItemCount > 0) {                       AdapterView.this.onRestoreInstanceState(mInstanceState);                mInstanceState = null;            } else {                       rememberSyncState();            }            checkFocus();            //重新布局            requestLayout();        }       //其他代码略    }

    最终就是在AdapterDataSetObserver这个类里面的**onChanged()**方法中实现了布局的更新。

    简单总结

    当ListView的数据发生变化时,我们调用Adapter的notifyDataSetChanged()方法,这个方法又会调用所有观察者(AdapterDataSetObserver)的onChanged()方法,onChanged()方法又会调requestLayout()方法来重新进行布局。

  3. BroadcastReceiver

  4. RxJava、RxAndroid、EventBus、otto等等,也是使用了观察者模式。

8.2.6.代理模式

为其他的对象提供一种代理以控制对这个对象的访问。适用于当无法或不想直接访问某个对象时通过一个代理对象来间接访问,为了保证客户端使用的透明性,委托对象与代理对象需要实现相同的接口。

静态代理:
静态代理很好理解就是我们需要编写一个代理类。实现我们需要代理的所有方法。所以称之为静态代理。

动态代理:
在java的动态代理机制中,有两个重要的类或接口

一个是 InvocationHandler(Interface),另一个则是Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的

Proxy这个类的 newProxyInstance 这个方法:

public static Object newProxyInstance(ClassLoader loader,                                          Class<?>[] interfaces,                                          InvocationHandler h)

oader:一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上。

InvocationHandler:

public interface InvocationHandler {         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;}
proxy:指代生成的代理对象;method:指代的是我们所要调用真实对象的某个方法的Method对象;args:指代的是调用真实对象某个方法时接受的参数;每一个代理实例类的InvocationHandler 都要实现InvocationHandler这个接口。并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的invoke 方法来进行调用

示例:

// 定义相关接口public interface BaseInterface {         void doSomething();}// 接口的相关实现类public class BaseImpl implements BaseInterface {         @Override    public void doSomething() {             System.out.println("doSomething");    }}public static void main(String args[]) {         BaseImpl base = new BaseImpl();    // Proxy 动态代理实现    BaseInterface proxyInstance = (BaseInterface) Proxy.newProxyInstance(base.getClass().getClassLoader(), base.getClass().getInterfaces(), new InvocationHandler() {             @Override        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {                 if (method.getName().equals("doSomething")) {                     method.invoke(base, args);                System.out.println("do more");            }            return null;        }    });    proxyInstance.doSomething();}

8.2.7.策略模式

关键点:定义了一系列的算法,并封装起来,提供针对同一类型问题的多种处理方式。
应用场景

同一个问题具有不同算法时,即仅仅是具体的实现细节不同时,如各种排序算法等等。对客户隐藏具体策略(算法)的实现细节,彼此完全独立;提高算法的保密性与安全性。一个类拥有很多行为,而又需要使用if-else或者switch语句来选择具体行为时。使用策略模式把这些行为独立到具体的策略类中,可以避免多重选择的结构。

Android中的源码分析:用的ListView时都需要设置一个Adapter,而这个Adapter根据我们实际的需求可以用ArrayAdapter、SimpleAdapter等等,这里就运用到策略模式

   listView = (ListView)findViewById(R.id.list_view);        //使用ArrayAdapter    listView.setAdapter(new ArrayAdapter<String>(this,R.id.item,    new String[] {     "one","two"}));         //使用BaseAdapter    listView.setAdapter(new BaseAdapter() {             @Override        public int getCount() {                 return 0;        }        @Override        public Object getItem(int position) {                 return null;        }        .....    });

相关源码:

 public class ListView extends AbsListView {     //相当于环境类        @Override        public void setAdapter(ListAdapter adapter) {     //设置策略,即adapter            //其他代码略        }    }    public interface ListAdapter extends Adapter {     //抽象策略接口            }    //具体策略类BaseAdapter,实现ListAdapter接口    public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {                 }    //具体策略类ArrayAdapter,继承BaseAdapter,即实现ListAdapter接口    public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {                 }

通过设置不同的Adapter(即不同的策略),我们就可以写出符合我们需求的ListView布局。
另外,动画中的插值器(ValueAnimator 的 setInterpolator 方法)也是有运用到策略模式。

8.2.8.工厂模式

工厂模式分为三种 :简单工厂模式 、工厂方法模式 、抽象工厂模式

简单工厂模式:

//抽象产品类     public abstract class Product {             public abstract void show();    }
  //具体产品类A     public class ProductA extends Product {             @Override        public void show() {                 System.out.println("product A");        }    }    //具体产品类B    public class ProductB extends Product {             @Override        public void show() {                 System.out.println("product B");        }    }
//创建工厂类,创建具体的产品:public class Factory {         public static Product create(String productName) {             Product product = null;        //通过switch语句控制生产哪种商品        switch (productName) {                 case "A":                product = new ProductA();                break;            case "B":                product = new ProductB();                break;        }        return product;    }}
 public void test() {             Factory.create("A").show();//生产ProductA        Factory.create("B").show();//生产ProductB        try {                 Factory.create("C").show();//生产ProductC        } catch (NullPointerException e) {                 System.out.println("没有ProductC");//没有ProductC,会报错        }    }

缺点

  1. 违背开放封闭原则,若需添加新产品则必须修改工厂类逻辑,会造成工厂逻辑过于复杂。
  2. 简单工厂模式使用了静态工厂方法,因此静态方法不能被继承和重写。
  3. 工厂类包含了所有实例(产品)的创建逻辑,若工厂类出错,则会造成整个系统都会会受到影响。

工厂方法模式与简单工厂模式比较

  1. 工厂方法模式有抽象工厂类,简单工厂模式没有抽象工厂类且其工厂类的工厂方法是静态的。
  2. 工厂方法模式新增产品时只需新建一个工厂类即可,符合开放封闭原则;而简单工厂模式需要直接修改工厂类,违反了开放封闭原则。

工厂方法模式

创建抽象产品类,定义公共接口:

  //抽象产品类    public abstract class Product {             public abstract void show();    }

创建具体产品类,继承Product类:

//具体产品类A public class ProductA extends Product {         @Override    public void show() {             System.out.println("product A");    }}//具体产品类Bpublic class ProductB extends Product {         @Override    public void show() {             System.out.println("product B");    }}

创建抽象工厂类,定义公共接口:

//抽象工厂类public abstract class Factory {         public abstract Product create();}

创建具体工厂类,继承抽象工厂类,实现创建具体的产品:

  //具体工厂类A    public class FactoryA extends Factory {             @Override        public Product create() {                 return new ProductA();//创建ProductA        }    }    //具体工厂类B    public class FactoryB extends Factory {             @Override        public Product create() {                 return new ProductB();//创建ProductB        }    }
 public void test() {             //产品A        Factory factoryA = new FactoryA();        Product productA = factoryA.create();        productA.show();        //产品B        Factory factoryB = new FactoryB();        Product productB = factoryB.create();        productB.show();    }

应用场景
生成复杂对象时,无需知道具体类名,只需知道相应的工厂方法即可。
优点

符合开放封闭原则。新增产品时,只需增加相应的具体产品类和相应的工厂子类即可。符合单一职责原则。每个具体工厂类只负责创建对应的产品。

缺点

一个具体工厂只能创建一种具体产品。增加新产品时,还需增加相应的工厂类,系统类的个数将成对增加,增加了系统的复杂度和性能开销。引入的抽象类也会导致类结构的复杂化。

Android中的ThreadFactory就是使用了工厂方法模式来生成线程的,线程就是ThreadFactory的产品。

ThreadFactory相关源码分析

   //抽象产品:Runnable    public interface Runnable {             public abstract void run();    }        //具体产品:Thread    public class Thread implements Runnable {             //构造方法        public Thread(Runnable target, String name) {                 init(null, target, name, 0);        }                @Override        //实现抽象产品的抽象方法        public void run() {                 if (target != null) {                     target.run();            }        }                //其他代码略    }            //抽象工厂:ThreadFactory    public interface ThreadFactory {             Thread newThread(Runnable r);    }        //具体工厂:AsyncTask中的实现    private static final ThreadFactory sThreadFactory = new ThreadFactory() {             private final AtomicInteger mCount = new AtomicInteger(1);                //实现抽象工厂的抽象方法        public Thread newThread(Runnable r) {                 return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());//返回Thread这个产品        }    };
通过ThreadFactory,我们可以创建出不同的Thread来。同样,我们可以创建另外类似的工厂,生产某种专门的线程,非常容易扩展。

抽象工厂模式

创建抽象产品类

//抽象产品类-- CPUpublic abstract class CPU {         public abstract void showCPU();}//抽象产品类-- 内存public abstract class Memory {         public abstract void showMemory();}//抽象产品类-- 硬盘public abstract class HD {         public abstract void showHD();}

创建具体产品类

    //具体产品类-- Intet CPU    public class IntelCPU extends CPU {             @Override        public void showCPU() {                 System.out.println("Intet CPU");        }    }        //具体产品类-- AMD CPU    public class AmdCPU extends CPU {             @Override        public void showCPU() {                 System.out.println("AMD CPU");        }    }    //具体产品类-- 三星 内存    public class SamsungMemory extends Memory {             @Override        public void showMemory() {                 System.out.println("三星 内存");        }    }        //具体产品类-- 金士顿 内存    public class KingstonMemory extends Memory {             @Override        public void showMemory() {                 System.out.println("金士顿 内存");        }    }    //具体产品类-- 希捷 硬盘    public class SeagateHD extends HD {             @Override        public void showHD() {                 System.out.println("希捷 硬盘");        }    }    //具体产品类-- 西部数据 硬盘    public class WdHD extends HD {             @Override        public void showHD() {                 System.out.println("西部数据 硬盘");        }    }

创建抽象工厂类:定义工厂中用来创建不同产品的方法:

//抽象工厂类,电脑工厂类public abstract class ComputerFactory {         public abstract CPU createCPU();    public abstract Memory createMemory();    public abstract HD createHD();}

创建具体工厂类:继承ComputerFactory 类:

    //具体工厂类--联想电脑    public class LenovoComputerFactory extends ComputerFactory {             @Override        public CPU createCPU() {                 return new IntelCPU();        }        @Override        public Memory createMemory() {                 return new SamsungMemory();        }        @Override        public HD createHD() {                 return new SeagateHD();        }    }        //具体工厂类--华硕电脑    public class AsusComputerFactory extends ComputerFactory {             @Override        public CPU createCPU() {                 return new AmdCPU();        }        @Override        public Memory createMemory() {                 return new KingstonMemory();        }        @Override        public HD createHD() {                 return new WdHD();        }    }        //具体工厂类--惠普电脑    public class HpComputerFactory extends ComputerFactory {             @Override        public CPU createCPU() {                 return new IntelCPU();        }        @Override        public Memory createMemory() {                 return new KingstonMemory();        }        @Override        public HD createHD() {                 return new WdHD();        }    }
 public void test() {             System.out.println("--------------------生产联想电脑-----------------------");        ComputerFactory lenovoComputerFactory = new LenovoComputerFactory();        lenovoComputerFactory.createCPU().showCPU();        lenovoComputerFactory.createMemory().showMemory();        lenovoComputerFactory.createHD().showHD();        System.out.println("--------------------生产华硕电脑-----------------------");        ComputerFactory asusComputerFactory = new AsusComputerFactory();        asusComputerFactory.createCPU().showCPU();        asusComputerFactory.createMemory().showMemory();        asusComputerFactory.createHD().showHD();                System.out.println("--------------------生产惠普电脑-----------------------");        ComputerFactory hpComputerFactory = new HpComputerFactory();        hpComputerFactory.createCPU().showCPU();        hpComputerFactory.createMemory().showMemory();        hpComputerFactory.createHD().showHD();    }
--------------------生产联想电脑-----------------------Intet CPU三星 内存希捷 硬盘--------------------生产华硕电脑-----------------------AMD CPU金士顿 内存西部数据 硬盘--------------------生产惠普电脑-----------------------Intet CPU金士顿 内存西部数据 硬盘

应用场景

生产多个产品组合的对象时。

优点

代码解耦,创建实例的工作与使用实例的工作分开,使用者不必关心类对象如何创建。

缺点

如果增加新的产品,则修改抽象工厂和所有的具体工厂,违反了开放封闭原则

工厂方法模式与抽象工厂模式比较

在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法具有唯一性。抽象工厂模式则可以提供多个产品对象,而不是单一的产品对象。

8.2.9.适配器模式

九、性能优化(非常重要)

9.1 性能优化分析工具学习

System Trace
Hierarchy Viewer
TraceView

9.2 布局优化

布局优化相对比较容易,优化可以先从布局来展开。使用 Hierarchy Viewer 和开发者模
式中关于布局绘制的选项,可以查到一些问题然后进行修改。

布局嵌套过深:层级嵌套过深的话,深度遍历各个节点会非常消耗时间,这也是布局优化余地最大的一个点了。很多过深的层级是不必要的。如果布局真的很复杂,不深度嵌套没法实现想要的效果。可以尝试约束布局 Constraintlayout 。

使用合适的布局: 三种常见的 ViewGroup 的绘制速度:FrameLayout >LinerLayout > RelativeLayout。当然,如果用 RelativeLayout 可以避免布局嵌套的话是值得的。可以根据这些去决定选用什么样的布局。

使用 include 标签: 在布局文件中,标签可以指定插入一段布局文件到当前布局。这样的话既提高了布局复用,也减少了我们的代码书写。另外,merge标签可以和include的标签一起使用从而减少布局层级。

ViewStub 延时加载: 有些布局,比如网络出错的布局,没必要在所有时候都加载出来。使用 ViewStub 可以实现按需加载。ViewStub 本身没有宽高,加载起来几乎不消耗什么资源。当对他setVisibility(View.VISIBLE)的时候会调用它引用的真实布局填充到当前位置,从而实现了延时加载,节省了正常加载的时间。

移除 Activity 默认背景 只要我们不需要 Activity 的默认背景,就可以移除掉,以减少 Activity 启动时的渲染时间,提升启动效率。移动方法见下:

<style name="Theme.NoBackground" parent="Theme.AppCompat.Light.NoActionBar">    <item name="android:windowNoTitle">true</item>    <item name="android:windowBackground">@null</item></style>

9.3 线程优化

线程调度原理

  1. 任意时刻,只有一个线程占用CPU,处于运行状态
  2. 多线程并发,轮流获取CPU使用权
  3. JVM 负责线程的调度:按照特定的机制分配CPU使用权

线程的创建和销毁会带来比较大的性能开销。因此线程优化也很有必要。查看项目中是否存在随意 new thread,线程缺乏管理的情况。使用 AsyncTask 或者线程池对线程进行管理,可以提升 APP 的性能。另外,我比较推荐使用 Rxjava 来实现异步操作,既方便又优雅。

9.4 网络优化

  1. 连接复用:节省连接建立时间,如开启 keep-alive。于Android来说默认情况下HttpURLConnection和HttpClient都开启了keep-alive。

  2. 请求合并:即将多个请求合并为一个进行请求,比较常见的就是网页中的CSS Image Sprites。同一个页面数据尽量放到一个接口中去处理。

  3. 减少请求数据的大小:对于post请求,body可以做gzip压缩的,header也可以做数据压缩(不过只支持http 2.0)。 返回数据的body也可以做gzip压缩,body数据体积可以缩小到原来的30%左右(也可以考虑压缩返回的json数据的key数据的体积,尤其是针对返回数据格式变化不大的情况,支付宝聊天返回的数据用到了)。

  4. 根据用户的当前的网络质量来判断下载什么质量的图片(电商用的比较多)

  5. 使用HttpDNS优化DNS:DNS存在解析慢和DNS劫持等问题,DNS 不仅支持 UDP,它还支持 TCP,但是大部分标准的 DNS 都是基于 UDP 与 DNS 服务器的 53 端口进行交互。HTTPDNS 则不同,顾名思义它是利用 HTTP 协议与 DNS 服务器的 80 端口进行交互。不走传统的 DNS 解析,从而绕过运营商的 LocalDNS 服务器,有效的防止了域名劫持,提高域名解析的效率。

  6. 大量数据的加载采用分页的方式;上传图片时,在必要的时候压缩图片

9.5 Apk 包体优化

Apk 组成结构:

  1. 整体优化

    分离应用的独立模块,以插件的形式加载
    解压APK,重新用 7zip 进行压缩
    用 apksigner 签名工具 替代 java 提供的 jarsigner 签名工具

  2. 资源优化
    可以只用一套资源图片,一般采用 xhdpi 下的资源图片
    通过扫描文件的 MD5 值,找出名字不同,内容相同的图片并删除
    通过 Lint 工具扫描工程资源,移除无用资源
    通过 Gradle 参数配置 shrinkResources=true(资源缩减)
    对 png 图片压缩;图片资源考虑采用 WebP 格式
    避免使用帧动画,可使用 Lottie 动画库
    优先考虑能否用 shape 代码、.9 图、svg 矢量图、VectorDrawable 类来替换传统的图片

  3. 代码优化
    启用混淆以移除无用代码(minifyEnabled true )
    开启代码压缩( minifyEnabled true //打开代码压缩)
    剔除 R 文件
    用注解替代枚举

  4. .arsc文件优化
    移除未使用的备用资源来优化 .arsc 文件

android {         defaultConfig {             ...        resConfigs "zh", "zh_CN", "zh_HK", "en"    }}
  1. so库打包优化
    只提供对主流架构的支持,比如 arm,对于 mips 和 x86 架构可以考虑不提供支持
    android {
    defaultConfig {             ...        ndk {                 abiFilters  "armeabi-v7a"        }    }}

9.6 内存优化(非常重要)

首先需要了解ava 内存回收机制——GC机制,Java 对象引用方式 —— 强引用、软引用、弱引用和虚引用。
基础知识:

9.6.1 Android 内存管理机制

1.针对进程的内存策略

进程的内存分配策略为:由 ActivityManagerService 集中管理所有进程的内存分配

进程的内存回收策略为:首先Application Framework 决定回收的类型,当进程的内存空间紧张时会按照进程优先级由低到高的顺序自动回收进程及内存。

Android将进程分为5个优先级,具体如下:

真正执行回收进程的操作的是 Linux 内核。

梳理一下整体流程:

  1. ActivityManagerService 对所有进程进行评分。

  2. 更新评分到 Linux 内核。

  3. 由 Linux 内核完成真正的内存回收。

2.针对对象、变量的内存策略

Android的对于对象、变量的内存策略同 Java

内存管理 = 对象 / 变量的内存分配 + 内存释放

  1. 内存分配策略
    对象,变量的内存分配有系统负责,共有三种:静态分配、栈式分配、堆式分配,分别面向静态变量,动态变量和对象实例。

  2. 内存释放策略
    对象,变量的内存释放由Java的垃圾回收器GC负责。

内存分配注意:(非常重要)

  1. 成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)—因为他们属于类,类对象最终还是要被new出来的。

  2. 局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。—–因为他们属于方法当中的变量,生命周期会随着方法一起结束。

public class Sample {             // 该类的实例对象的成员变量s1、mSample1及指向的对象都存放在堆内存中    int s1 = 0;    Sample mSample1 = new Sample();     // 方法中的局部变量s2、mSample2存放在 栈内存 // 变量mSample2所指向的对象实例存放在 堆内存    public void method() {                     int s2 = 0;        Sample mSample2 = new Sample();    }}// 变量mSample3的引用存放在栈内存中    // 变量mSample3所指向的对象实例存放在堆内存// 该实例的成员变量s1、mSample1也存放在堆内存中    Sample mSample3 = new Sample();

9.6.2 Android的内存泄漏、内存溢出、内存抖动概念

  1. 内存泄露
    即 ML (Memory Leak),指 程序在申请内存后,当该内存不需再使用但却无法被释放,归还给 程序的现象。对应用程序的影响:容易使得应用程序发生内存溢出,即OOM(out of Memory)

    发生内存泄露的本质原因
    本质原因:持有引用者的生命周期>被引用者的生命周期
    解释:本该回收的对象(该对象已经不再被使用),由于某些原因(如被另一个正在使用的对象引用)不能被回收。

  2. 内存抖动

    优化方案
    尽量避免频繁创建大量、临时的小对象

9.6.3 如何避免OOM(内存泄漏优化)。

1.减小对象的内存占用

1)使用更加轻量的数据结构
例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构,下图演示了HashMap的简要工作原理,相比起Android系统专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。

可以参考Android性能优化典范 - 第3季

2)避免在Android里面使用Enum
3)减小Bitmap对象的内存占用
Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用是很重要的,通常来说有下面2个措施:
inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
4)使用更小的图片
在设计给到资源图片的时候,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用一张更小的图片。尽量使用更小的图片不仅仅可以减少内存的使用,还可以避免出现大量的InflationException。

2.内存对象的重复利用

1)复用系统自带的资源
Android系统本身内置了很多的资源,例如字符串/颜色/图片/动画/样式以及简单布局等等,这些资源都可以在应用程序中直接引用。这样做不仅仅可以减少应用程序的自身负重,减小APK的大小,另外还可以一定程度上减少内存的开销,复用性更好。但是也有必要留意Android系统的版本差异性,对那些不同系统版本上表现存在很大差异,不符合需求的情况,还是需要应用程序自身内置进去。

2)注意在ListView/GridView等出现大量重复子组件的视图里面对ConvertView的复用
3)Bitmap对象的复用
在ListView与GridView等显示大量图片的控件里面需要使用LRU的机制来缓存处理好的Bitmap。
4)避免在onDraw方法里面执行对象的创建
类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。
5)StringBuilder
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。

3.避免对象的内存泄露

内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现OOM。显然,这还使得每级Generation的内存区域可用空间变小,gc就会更容易被触发,容易出现内存抖动,从而引起性能问题。(LeakCanary开源控件,可以很好的帮助我们发现内存泄露的情况)
1)注意Activity的泄漏
通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,我们需要特别注意以下两种情况导致的Activity泄漏:

内部类引用导致Activity的泄漏:非静态(匿名)内部类会默认持有外部类引用。

  1. Handler导致的Activity泄漏(最经典的场景),如果Handler中有延迟的任务或者是等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏。此时的引用关系链是Looper -> MessageQueue -> Message -> Handler -> Activity。为了解决这个问题,可以在UI退出之前,执行remove Handler消息队列中的消息与runnable对象(removeCallbacksAndMessages(null)–同时清空消息队列 ,结束Handler生命周期)。或者是使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。为了保证Handler中消息队列中的所有消息都能被执行,此处推荐使用 静态内部类 + 弱引用的方式。

  2. 线程造成的内存泄漏

    在 Activity 内定义了一个匿名的 AsyncTask 对象,就有可能发生内存泄漏。如果 Activity 被销毁之后 AsyncTask 仍然在执行,那就会阻止垃圾回收器回收Activity 对象,进而导致内存泄漏,直到执行结束才能回收 Activity。

    同样的,使用 ThreadTimerTask 也可能导致 Activity 泄漏。只要它们是通过匿名类创建的,尽管它们在单独的线程被执行,它们也会持有对 Activity 的强引用,进而导致内存泄漏

总结
内部类引起的泄漏不仅仅会发生在Activity上,其他任何内部类出现的地方,都需要特别留意!我们可以考虑尽量使用static类型的内部类,同时使用WeakReference的机制来避免因为互相引用而出现的泄露。

2)考虑使用Application Context而不是Activity Context
对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露。(Activity Context被传递到其他实例中,这可能导致Activity自身被引用而发生泄漏)

3)注意临时Bitmap对象的及时回收

4)注意监听器的注销
在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的listener,需要记得及时remove这个listener。

5)注意WebView的泄漏
Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的WebView存在内存泄露的问题,看这里WebView causes memory leak - leaks the parent Activity。所以通常根治这个问题的办法是为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

6)注意Cursor对象是否及时关闭
在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。

9.6.4 常用的内存检查工具。

(1)Memory Monitor 工具:

它是Android Studio自带的一个内存监视工具,它可以很好地帮助我们进行内存实时分析。通过点击Android Studio右下角的Memory Monitor标签,打开工具可以看见较浅蓝色代表free的内存,而深色的部分代表使用的内存从内存变换的走势图变换,可以判断关于内存的使用状态,例如当内存持续增高时,可能发生内存泄漏;当内存突然减少时,可能发生GC等,如下图所示。

(2)LeakCanary工具:

LeakCanary是Square公司基于MAT开发的一款监控Android内存泄漏的开源框架。其工作的原理是: 监测机制利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收(在ReferenceQueue中说明可以被回收,不存在泄漏;否则,可能存在泄漏,LeakCanary是执行一遍GC,若还未在ReferenceQueue中,就会认定为泄漏)。

如果Activity被认定为泄露了,就抓取内存dump文件(Debug.dumpHprofData);之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析;接着通过HeapAnalyzer (checkForLeak—findLeakingReference—findLeakTrace)来进行内存泄漏分析。最后通过DisplayLeakService进行内存泄漏的展示。

(3)Android Lint 工具:

Android Lint Tool 是Android Sutido种集成的一个Android代码提示工具,它可以给你布局、代码提供非常强大的帮助。硬编码会提示以级别警告,例如:在布局文件中写了三层冗余的LinearLayout布局、直接在TextView中写要显示的文字、字体大小使用dp而不是sp为单位,就会在编辑器右边看到提示。

9.7 电量优化

Battery Historian(电量使用记录分析工具)

Battery Historian是Android 5.0开始引入的新API。通过下面的指令,可以得到设备上的电量消耗信息。
电量优化的一些建议
1.充电时执行任务

 if (!checkForPower()) {             Toast.makeText(view.getContext(), "当前非充电状态", Toast.LENGTH_SHORT).show();        return;    }    /**         * 是否充电         * AC --- 交流电         * USB         * WireLess -- 无线充电         *         * @return         */        private boolean checkForPower() {                 IntentFilter mIntentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);            Intent intent = registerReceiver(null, mIntentFilter);            int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);            boolean isUsb = plugged == BatteryManager.BATTERY_PLUGGED_USB;            boolean isAc = plugged == BatteryManager.BATTERY_PLUGGED_AC;            boolean isWireless = false;            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {                     //api >= 17                isWireless = plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS;            }            return (isUsb || isAc || isWireless);        }

2.连接Wifi后执行任务
我们知道wifi网络传输的电量消耗要比移动网络少很多,应该尽量减少移动网络下的数据传输,多在WiFi环境下传输数据,所以我们可以把一些不需要实时性的任务留到连接wifi后在执行

3.wake_lock
系统为了节省电量,CPU在没有任务忙的时候就会自动进入休眠。有任务需要唤醒CPU高效执行的时候,就会给CPU加wake_lock锁。但是使用wake_lock结束时需要释放锁,如果忘记释放,会使得CPU一直执行消耗电量,所以推荐使用带超时的wake lock或者WakefulBroadcastReceiver

 wakeLock.acquire(timeout);

4.大量高频次的CPU唤醒及操作集中处理

5.定位

定位完成,及时关闭如果需要实时定位,减少更新频率根据实际情况,选择gsp定位还是网络定位,降低电量消耗

6.网络优化可以促进电量优化

十、算法题

建议阅读常见面试算法题汇总

更多相关文章

  1. Android(安卓)webView与js 交互以及jsbridge框架源码分析
  2. Android异步消息处理机制完全解析-Handler详解
  3. [Android]解决failed to get the required ADT version from sdk
  4. Android中Handler Runnable与Thread的区别详解
  5. Android打开第三方地图使用方法
  6. Android(安卓)控件 —— ListView
  7. 浅谈Java中Collections.sort对List排序的两种方法
  8. 类和 Json对象
  9. Python list sort方法的具体使用

随机推荐

  1. android network develop(2)----network
  2. Canvas.clipPath不能用
  3. 在Android中使用NDK调用OpenGl
  4. Android 解析CSV文件,中文乱码
  5. Android 反射调用类
  6. android颜色值
  7. 【Android】向sdcard中写入文件
  8. Android传感器(六):光传感器
  9. Android 4.0 HttpURLConnection 下载失败
  10. Android绘制一条边为弧形的矩形