吐血整理,看完可达年薪百万,高级Android工程师必备知识点

Java部分

Java基础

java基础面试知识点

1. java中==和equals和hashCode的区别

基本数据类型的比较用==,这样是比较它们的值。
引用类型(类,接口,数组)的比较,如果是双等号,那么比较的是它们的内存地址,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。
看一下源码大家都会明白,对于-128到127之间的数,会进行缓存,Integer b1 = 127时,会将127进行缓存,下次再写Integer i6 = 127时,就会直接从缓存中取,就不会new了。
Integer b1 = 127;java在编译的时候,被翻译成-> Integer b1 = Integer.valueOf(127);
eauals一般都是通过对象的内容是否相等来判断对象是否相等.
hascode部分
a.如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
b.如果两个对象不equals,他们的hashcode有可能相等。
c.如果两个对象hashcode相等,他们不一定equals。
d.如果两个对象hashcode不相等,他们一定不equals
引用链接: java中==和equals和hashCode的区别

2. int、char、long各占多少字节数

Java基本类型占用的字节数:
1字节: byte , boolean
2字节: short , char
4字节: int , float
8字节: long , double
编码与中文:
Unicode/GBK: 中文2字节
UTF-8: 中文通常3字节,在拓展B区之后的是4字节
综上,中文字符在编码中占用的字节数一般是2-4个字节

3. int与integer的区别

  • Integer是int的包装类,int则是java的一种基本数据类型
  • Integer变量必须实例化后才能使用,而int变量不需要
  • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
  • Integer的默认值是null,int的默认值是0
    int与integer的区别

4.谈谈对Java多态的理解

理解的要点:多态意味着父亲的变量可以指向子类对象
面向对象程序设计的三大支柱是封装、继承和多态
封装对外把相应的属性和方法实现的细节进行了隐藏。继承关系使一个子类继承父亲的特征,并且加上了一些新的特征。子类是它的父亲的特殊化,

每一个子类的实例都是其父亲的实例,但是反过来就不成立。例如:每个圆都是一个几何对象,但并非每一个几何对象都是圆。因此,总可以将子类的实例传给需要父亲型的参数

5. String、StringBuffer、StringBuilder区别

1、长度是否可变
String 是被 final 修饰的,他的长度是不可变的,就算调用 String 的concat 方法,那也是把字符串拼接起来并重新创建一个对象,把拼接后的 String 的值赋给新创建的对象
StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象,StringBuffer 与 StringBuilder 中的方法和功能完全是等价的。调用StringBuffer 的 append 方法,来改变 StringBuffer 的长度,并且,相比较于 StringBuffer,String 一旦发生长度变化,是非常耗费内存的!
2、执行效率
三者在执行速度方面的比较:StringBuilder > StringBuffer > String
3、应用场景
如果要操作少量的数据用 = String
单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
多线程操作字符串缓冲区 下操作大量数据 = StringBuffer
StringBuffer和StringBuilder区别
1、是否线程安全
StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问),StringBuffer是线程安全的。只是StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。
2、应用场景
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。
然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。 append方法与直接使用+串联相比,减少常量池的浪费

6.什么是内部类?内部类的作用?

在java语言中,可以把一个类定义到另外一个类的内部,在类里面的这个类就叫内部类,外面的类就叫外部类。
内部类好处
1.隐藏你不想让别人知道的操作,也即封装性。
2.一个内部类对象可以访问创建它的外部类对象的内容,甚至包括私有变量!
内部类可以分为多种;主要以下4种:静态内部类,成员内部类,局部内部类,匿名内部类

静态内部类
静态内部类是指被声明为static的内部类,他可以不依赖内部类而实例,而通常的内部类需要实例化外部类,从而实例化。静态内部类不可以有与外部类有相同的类名。不能访问外部类的普通成员变量,但是可以访问静态成员变量和静态方法(包括私有类型)

成员内部类
一个 静态内部类去掉static 就是成员内部类,他可以自由的引用外部类的属性和方法,无论是静态还是非静态。但是不可以有静态属性和方法

局部内部类
定义在一个代码块的内类,他的作用范围是所在代码块,是内部类中最少使用的一类型。局部内部类跟局部变量一样,不能被public ,protected,private以及static修饰,只能访问方法中定义final类型的局部变量

匿名内部类
匿名内部类是一种没有类名的内部类,不使用class,extends,implements,没有构造函数,他必须继承其他类或实现其他接口。匿名内部类的好处是使代码更加简洁,紧凑,但是带来的问题是易读性下降。

内部类的使用时机
1、实现事件监听器的时候(比方说actionListener 。。。采用内部类很容易实现);
2、编写事件驱动时(内部类的对象可以访问外部类的成员方法和变量,注意包括私有成员);
3、在能实现功能的情况下,为了节省编译后产生的字节码(内部类可以减少字节码文件,即java文件编译后的.class文件);

7.抽象类和接口区别

1.抽象类可以有构造方法,接口中不能有构造方法。
2.抽象类中可以有普通成员变量,接口中没有普通成员变量
3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
4. 抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
5. 抽象类中可以包含静态方法,接口中不能包含静态方法
6. 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
7. 一个类可以实现多个接口,但只能继承一个抽象类。

8.抽象类的概述与特点

a:抽象类和抽象方法必须用abstract修饰
*abstract class 类名()
*public abstract void eat()
b:抽象类不一定有抽象方法,有抽象方法的类一定是抽象类或者是抽象接口
c:抽象类不能实例化,那么抽象类如何实例化呢?
按照多态的方式,由具体的子类实例化,其实这也是多态的一种,抽象类多态
d:抽象类的子类
要么是抽象类要么重写抽象类中的所有抽象方法

9. 泛型中extends和super的区别

? extends T :表示上界是T, ? 都是继承自T的,都是T的子类;
? super T :表示下界是T,?都是T的父类;
第一、 频繁往外读取内容的,适合用 ? extends T;
第二、 经常往里插入的,适合用 ? super T;

10.父类的静态方法能否被子类重写

不能重写,但子类能把父类的静态方法继承过来,子类可以重新定义父类的静态方法,并将父类的静态方法屏蔽掉,取消屏蔽可以使用父类名.静态方法名的方式调用。

11.进程和线程的区别

(1)进程
进程是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。

(2)线程
线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

(3)联系
线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

(4)区别:理解它们的差别,我从资源使用的角度出发。(所谓的资源就是计算机里的中央处理器,内存,文件,网络等等)

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

借鉴地址:https://www.cnblogs.com/jobbible/p/9766649.html

12.final,finally,finalize的区别

1、final修饰符(关键字)。被final修饰的类,就意味着不能再派生出新的子类,不能作为父类而被子类继承。因此一个类不能既被abstract声明,又被final声明。将变量或方法声明为final,可以保证他们在使用的过程中不被修改。被声明为final的变量必须在声明时给出变量的初始值,而在以后的引用中只能读取。被final声明的方法也同样只能使用,即不能方法重写。
2、finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行。try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,跳转到catch块中执行。finally块则是无论异常是否发生,都会执行finally块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在finally块中。

3、finalize是方法名。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

13.序列化的方式

序列化的定义:将数据对象转换为二进制流的过程就称为对象的序列化(Serialization),反过来,将二进制流转换为对象就是反序列化(Deserializable)
1、数据持久化:在很多应用中,需要对好多对象进行序列化,存到物理硬盘,较长时间的保存,比如,Session对象,当有数万用户并发访问的时候,就会有数万的Session对象,内存会承受很大的压力,这个时候,就会把一些对象先序列化到硬盘中,需要使用的时候再还原到内存中。序列化对象要保留充分的信息,用来恢复数据对象,但是为了节约存储空间和网络带宽,序列化出的二进制流要尽可能小。

2、网络传输:当两个进程在互相通信的时候,就会进行数据传输,不管是何种类型的数据,都必须要转成二进制流来传输,接受方收到后再转为数据对象

java原生序列化:java类通过实现Serializable接口来实现。这个接口没有任何方法,只是标识,java序列化保留了对象的元数据,以及对象数据,兼容性最好,但是不支持跨语言,性能也一般。

14.Serializable 和Parcelable 的区别

1、作用

Serializable的作用是为了保存对象的属性到本地文件、数据库、网络流、RMI(Remote Method Invocation)以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。使用了反射技术,并且期间产生临时对象

Android的Parcelable的设计初衷是因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体。

2、效率及选择

Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化

15.静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?

父类的静态属性和方法可以被子类继承

不可以被子类重写:当父类的引用指向子类时,使用对象调用静态方法或者静态变量,是调用的父类中的方法或者变量。并没有被子类改写。

原因:

因为静态方法从程序开始运行后就已经分配了内存,也就是说已经写死了。所有引用到该方法的对象(父类的对象也好子类的对象也好)所指向的都是同一块内存中的数据,也就是该静态方法。

子类中如果定义了相同名称的静态方法,并不会重写,而应该是在内存中又分配了一块给子类的静态方法,没有重写这一说。

16.静态内部类的设计意图

我们知道,在java中类是单继承的,一个类只能继承另一个具体类或抽象类(可以实现多个接口)。这种设计的目的是因为在多继承中,当多个父类中有重复的属性或者方法时,子类的调用结果会含糊不清,因此用了单继承。

而使用内部类的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响

静态内部类与非静态内部类之间存在一个最大的区别:非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有。

没有这个引用就意味着:
它的创建是不需要依赖于外围类的。
它不能使用任何外围类的非static成员变量和方法。

17.成员内部类、静态内部类、局部内部类和匿名内部类的理解,以及项目中的应用

成员内部类:虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问
局部内部类:局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
匿名内部类:匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。
静态内部类:静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

18.闭包和内部类的区别

通过内部类加接口,可以很好的实现多继承的效果。
闭包能够将一个方法作为一个变量去存储,这个方法有能力去访问所在类的自由变量。

19.string 转换成 integer的方式及原理

总结:integer.parseInt(string str)方法调用Integer内部的
parseInt(string str,10)方法,默认基数为10,parseInt内部首先
判断字符串是否包含符号(-或者+),则对相应的negative和limit进行
赋值,然后再循环字符串,对单个char进行数值计算Character.digit(char ch, int radix)在这个方法中,函数肯定进入到0-9字符的判断(相对于string转换到int),否则会抛出异常,数字就是如上面进行拼接然后生成的int类型数值。
https://blog.csdn.net/itboy_libing/article/details/80393530

java深入源码级的面试题(有难度)

20.哪些情况下的对象会被垃圾回收机制处理掉?

1.所有实例都没有活动线程访问。
2.没有被其他任何实例访问的循环引用实例。
3.Java 中有不同的引用类型。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。(强软弱虚引用)
要判断怎样的对象是没用的对象
1.采用标记计数的方法: 给内存中的对象给打上标记,对象被引用一次,计数就加1,引用被释放了,计数就减一,当这个计数为0的时候,这个对象就可以被回收了。当然,这也就引发了一个问题:循环引用的对象是无法被识别出来并且被回收的。所以就有了第二种方法:
2.采用根搜索算法:从一个根出发,搜索所有的可达对象,这样剩下的那些对象就是需要被回收的

21.讲一下常见编码方式?

计算机中存储信息的最小单元是一个字节,即8个bit,所以能表示的字符范围是0~255个
人类要表示的符号太多,无法用一个字节来完全表示
要解决这个矛盾必须要有一个新的数据结构char,从char到byte必须编码。目前常用的编码方式有ASCII、ISO8859-1、GB2312、GBK、UTF-8、UTF-16等

22.utf-8编码中的中文占几个字节;int型几个字节?

一个utf8数字占1个字节
一个utf8英文字母占1个字节
少数是汉字每个占用3个字节,多数占用4个字节。

23.静态代理和动态代理的区别,什么场景使用?

静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
动态代理类:在程序运行时,运用反射机制动态创建而成。
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
动态代理是实现JDK里的InvocationHandler接口的invoke方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象。
还有一种动态代理CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。
应用:被代理类庞大时,需要在某些方法执行前后处理一些事情时,亦或接口类与实现类经常变动时(因为使用反射所以方法的增删改并不需要修改invoke方法)。

24.Java的异常体系

https://www.cnblogs.com/aspirant/p/10790803.html
Java把异常作为一种类,当做对象来处理。所有异常类的基类是Throwable类,两大子类分别是Error和Exception。
系统错误由Java虚拟机抛出,用Error类表示。Error类描述的是内部系统错误,例如Java虚拟机崩溃。这种情况仅凭程序自身是无法处理的,在程序中也不会对Error异常进行捕捉和抛出。
异常(Exception)又分为RuntimeException(运行时异常)和CheckedException(检查时异常),两者区别如下:
RuntimeException:程序运行过程中才可能发生的异常。一般为代码的逻辑错误。例如:类型错误转换,数组下标访问越界,空指针异常、找不到指定类等等。
CheckedException:编译期间可以检查到的异常,必须显式的进行处理(捕获或者抛出到上一层)。例如:IOException, FileNotFoundException等等。

25.谈谈你对解析与分派的认识。

1.方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可变的,即“编译时可知,运行不可以变”,这类目标的方法的调用称之为解析
2.解析调用一定是个静态的过程,在编译期就完全确定,在类加载的解析阶段就将涉及的符号引用全部转变为可以确定的直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动的。于是分派方式就有静态分派和动态分派。
静态分派的最直接的解释是在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。因此在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。
显然这里不可能根据静态类型来决定调用那个方法。导致这个现象很明显的原因是因为这两个变量的实际类型不一样,jvm根据实际类型来分派方法执行版本。

26.Java中实现多态的机制是什么?

靠的是父类或接口的引用指向子类或实现类的对象,
调用的方法是内存中正在运行的那个对象的方法。

27.如何将一个Java对象序列化到文件里

1.对象需要实现Seralizable接口
2.通过ObjectOutputStream的writeObject()方法写入和ObjectInputStream的readObject()方法来进行读取

28.说说你对Java反射的理解

先讲反射机制,反射就是程序运行期间JVM会对任意一个类洞悉它的属性和方法,对任意一个对象都能够访问它的属性和方法。依靠此机制,可以动态的创建一个类的对象和调用对象的方法。其次就是反射相关的API,只讲一些常用的,比如获取一个Class对象。Class.forName(完整类名)。通过Class对象获取类的构造方法,class.getConstructor。根据class对象获取类的方法,getMethod和getMethods。使用class对象创建一个对象,class.newInstance等。最后可以说一下反射的优点和缺点,优点就是增加灵活性,可以在运行时动态获取对象实例。缺点是反射的效率很低,而且会破坏封装,通过反射可以访问类的私有方法,不安全。

29.说说你对Java注解的理解

Java注解(Annotation)也叫作元数据,以‘@注解名’在代码中存在,它是一种在源代码中标注的特殊标记,可以标注源代码中的类、属性、方法、参数等代码,主要用于创建文档,跟踪代码中的依赖性,执行基本编译时检查。

30.你对依赖注入的理解

组件之间依赖关系由容器在运行期决定
依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

谁依赖于谁:当然是应用程序依赖于IoC容器;
为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)

31.说一下泛型原理

泛型的作用在于在编译阶段保证我们使用了正确的类型,并且由编译器帮我们加入转型动作,使得转型是不需要关心且安全的。
Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

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

第一:在Java程序中String类型是使用最多的,这就牵扯到大量的增删改查,每次增删改差之前其实jvm需要检查一下这个String对象的安全性,就是通过hashcode,当设计成不可变对象时候,就保证了每次增删改查的hashcode的唯一性,也就可以放心的操作。

第二:网络连接地址URL,文件路径path通常情况下都是以String类型保存, 假若String不是固定不变的,将会引起各种安全隐患。就好比我们的密码不能以String的类型保存,,如果你将密码以明文的形式保存成字符串,那么它将一直留在内存中,直到垃圾收集器把它清除。而由于字符串被放在字符串缓冲池中以方便重复使用,所以它就可能在内存中被保留很长时间,而这将导致安全隐患

第三:字符串值是被保留在常量池中的,也就是说假若字符串对象允许改变,那么将会导致各种逻辑错误

33.Object类的equal和hashCode方法重写,为什么?

字段属性值完全相同的两个对象因为hashCode不同,所以在hashmap中的table数组的下标不同,从而这两个对象就会同时存在于集合中,所以重写equals()就一定要重写hashCode()方法

数据结构

34.并发集合了解哪些?

(1)阻塞式集合(blocking collection):这类集合包括添加和移除数据的方法。当集合已满或者为空时,被调用的添加或者 移除方法就不能立即执行,那么调用这个饭方法的线程将被阻塞,一直到该方法可以被成功执行。
(2)非阻塞式集合(Non-blocking collection):这类集合也包括添加和移除数据的方法。如果集合已满或者为空时,在调用添加或者移除方法时会返回null或者抛出异常,但是调用这个方法的线程不会被阻塞。
以下就是java常用的并发集合:
非阻塞式列表对应的实现类:ConcurrentLinkedDeque
阻塞式列表对应的实现类:LinkedBlockingDeque
用于数据生成或者消费的阻塞式列表对应的实现类:LinkedTransferQueue
按优先级排序列表元素的阻塞式列表对应的实现类:PriorityBlockingQueue
带有延迟列表元素的阻塞式列表对应的实现类:DelayQueue
非阻塞式列表可遍历映射对应的饿实现类:ConcurrentSkipListMap
随机数字对应的实现类:ThreadLockRandom
原子变量对应的实现类:AtomicLong和AtomicIntegerArray

35.Java集合框架继承关系

Collection<–List<–Vector
Collection<–List<–ArrayList
Collection<–List<–LinkedList
Collection<–Set<–HashSet
Collection<–Set<–HashSet<–LinkedHashSet
Collection<–Set<–SortedSet<–TreeSet
Map<–HashMap
Map<–TreeMap

36.List,Set,Map的区别

List:
1.可以允许重复的对象。
2.可以插入多个null元素。
3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
4.常用的实现类有 ArrayList、LinkedList 和 Vector。

Set:
1.不允许重复对象
2.无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
3.只允许一个 null 元素
4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。

Map:
1.Map不是collection的子接口或者实现类。Map是一个接口。
2.Map 的 每个 Entry 都持有两个对象,也就是一个键一个值(键值对),Map 可能会持有相同的值对象但键对象必须是唯一的。
3.TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
4.Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
5.Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。(HashMap、TreeMap最常用)

37.List和Map的实现方式以及存储方式

List:
常用实现方式有:ArrayList和LinkedList
ArrayList 的存储方式:数组,查询快
LinkedList的存储方式:链表,插入,删除快
Set:
常用实现方式有:HashSet和TreeSet
HashSet的存储方式:哈希码算法,加入的对象需要实现hashcode()方法,快速查找元素
TreeSet的存储方式:按序存放,想要有序就要实现Comparable接口
附加:
集合框架提供了2个实用类:collections(排序,复制、查找)和Arrays对数组进行(排序,复制、查找)
Map:
常用实现方式有:HashMap和TreeMap
HashMap的存储方式:哈希码算法,快速查找键值
TreeMap存储方式:对键按序存放

38.HashMap的实现原理

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

39.HashMap数据结构

jdk1.7 中使用个 Entry 数组来存储数据,用key的 hashcode 取模来决定key会被放到数组里的位置,如果 hashcode 相同,或者 hashcode 取模后的结果相同( hash collision ),那么这些 key 会被定位到 Entry 数组的同一个格子里,这些 key 会形成一个链表。在 hashcode 特别差的情况下,比方说所有key的 hashcode 都相同,这个链表可能会很长,那么 put/get 操作都可能需要遍历这个链表,也就是说时间复杂度在最差情况下会退化到 O(n)

jdk1.8 中使用一个 Node 数组来存储数据,但这个 Node 可能是链表结构,也可能是红黑树结构,如果插入的 key 的 hashcode 相同,那么这些key也会被定位到 Node 数组的同个格子里。如果同一个格子里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用 treeifyBin 函数,将链表转换为红黑树。那么即使 hashcode 完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销也就是说put/get的操作的时间复杂度最差只有 O(log n),但是真正想要利用 JDK1.8 的好处,有一个限制:key的对象,必须正确的实现了 Compare 接口

40.HashMap如何put数据(从HashMap源码角度讲解)?

1:首先判断数组是否被初始化,如果没有初始化则调用扩容的方法resize,进行初始化。
2:获取数组下标:index=(n-1)&hash,并赋值p=tab[(n-1)&hash]
3:如果p==null,则说明此下标index还没有任何的值,所以直接把key-value封装成Node放到数组中。
4:如果p!=null,说明此下标index已经有值了,要么是链表,要么是红黑树
4.1:如果是红黑树则直接调用putTreeVal方法,如果红黑树上已经存在key则直接覆盖,如果不存在key则把新节点插入到红黑树,并对红黑树进行修复
4.2:如果是链表,则进行循环链表,如果链表中已经存在key,则这接覆盖,如果不存在,则添加到链表的尾部,然后判断链表是否达到了转变红黑的阀门,如果到了,直接链表到红黑树的转变
4.3:从链表到红黑树的转变,HashMap中会首先判断数组的长度是否大于64,如果小於则调用resize()进行扩容,如果大於则进行链表到红黑树的转变。
5:通过上面的操作,如果HashMap中存在将要插入的key,通过参数onlyIfAbSent判断是否覆盖旧值,如果onlyIfAbSent=true则覆盖,否则则不覆盖
6:如果HashMap中不存在将要插入的key,则插入,插入后需要判断是否需要扩容,如果需要则调用resize()方法进行扩容。

41.HashMap怎么手写实现?

1.hashmap的实现
  ① 初始化
    1)定义一个Node<K, V>的数组来存放元素,但不立即初始化,在使用的时候再加载
    2)定义数组初始大小为16
    3)定义负载因子,默认为0.75,
    4)定义size用来记录容器存放的元素数量
  ② put的实现思路
    1) 判断容器是否为空,为空则初始化。
    2)判断容器的size是否大于阀值,是的话就扩容为以前长度的两倍,并重新计算其中元素的存放位置,进行重新存放
    3)计算出key的index角标位置
    4)判断计算出的index位置是否存在元素,存在的话则遍历链表,判断key是否存在,存在则更新,不存在则增加
  ③ get的实现思路
    1)通过key计算出它所在的index
    2)遍历index位置处的链表,并获取value返回。

/**
 * 自定义hashMap
 */
public class MyHashMap<K, V> implements MyMap<K, V>{
    //1.定义一个容器用来存放元素, 但不立即初始化,使用懒加载方式
    Node<K, V>[] table = null; 
    //2.定义容器的默认大小
    static int DEFAULT_INITIAL_CAPACITY = 16;
    //3.HashMap默认负载因子,负载因子越小,hash冲突机率越低,综合结论得出0.75最为合适
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //4.记录当前容器实际大小
    static int size;
    @SuppressWarnings("unchecked")
    @Override
    public V put(K k, V v) {
        //1.判断容器是否为空为空则初始化。
        if (table == null) {
            table = new Node[DEFAULT_INITIAL_CAPACITY];
        }
        //如果size大于阈值则进行扩容
        if (size > DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR) {
            resize();
        } 
        //2.计算出index角标
        int index = getIndex(k, DEFAULT_INITIAL_CAPACITY);   
        //3.将k-v键值对放进相对应的角标,如果计算出角标相同则以链表的形势存放
        Node<K, V> node = table[index];
        if (node == null) {
            table[index] = new Node<>(k, v, null);
            size ++;
            return table[index].getValue();
        } else {
            Node<K, V> newNode = node;          
            //循环遍历每个节点看看是否存在相同的key
            while (newNode != null) {
                //这里要用equals 和 == 因为key有可能是基本数据类型,也有可能是引用类型
                if (k.equals(newNode.getKey()) || k == newNode.getKey()) {
                    newNode.setValue(v);
                    size ++;
                    return v;
                } 
                newNode = node.getNextNode();
            }
            table[index] = new Node<K, V>(k, v, table[index]);
            size ++;           
            return table[index].getValue();
        }
    }
    /**
     * 获取index
     * @param key
     * @param length
     * @return
     */
    public int getIndex(K key, int length) {
        int hashCode = key.hashCode();
        int index = hashCode % length;
        return index;
    }
    /**
     * 获取key
     */
    @Override
    public V get(K k) {
        int index = getIndex(k, DEFAULT_INITIAL_CAPACITY);
        Node<K, V> node = table[index];
        if (k.equals(node.getKey()) || k == node.getKey()) {
            return node.getValue();
        } else {
            Node<K, V> nextNode = node.getNextNode();
            while(nextNode != null) {
                if (k.equals(nextNode.getKey()) || k == nextNode.getKey()) {
                    return nextNode.getValue();
                }
            }
        }
        return null;
    }
    /**
     * 对size进行扩容
     */
    @SuppressWarnings("unchecked")
    public void resize() {
        //1.创建新的table长度扩展为以前的两倍
        int newLength = DEFAULT_INITIAL_CAPACITY * 2;
        Node<K, V>[] newtable = new Node[newLength];
        //2.将以前table中的取出,并重新计算index存入
        for (int i = 0; i < table.length; i++) {
            Node<K, V> oldtable = table[i];
            while (oldtable != null) {
                //将table[i]的位置赋值为空,
                table[i] = null;
                //计算新的index值
                K key = oldtable.getKey();
                int index = getIndex(key, newLength); 
                //将以前的nextnode保存下来
                Node<K, V> nextNode = oldtable.getNextNode();  
                //将newtable的值赋值在oldtable的nextnode上,如果以前是空,则nextnode也是空
                oldtable.setNextNode(newtable[index]);
                newtable[i] = oldtable;  
                //将以前的nextcode赋值给oldtable以便继续遍历
                oldtable = nextNode;
            }     
        }
        //3.将新的table赋值回老的table
        table = newtable;
        DEFAULT_INITIAL_CAPACITY = newLength;
        newtable = null;
        
    }
    @Override
    public int size() {
        return size;
    }
    @SuppressWarnings("hiding")
    class Node<K, V> implements Entry<K, V> {
        private K key;
        private V value;
        private Node<K, V> nextNode; //下一节点
        public Node(K key, V value, Node<K, V> nextNode) {
            super();
            this.key = key;
            this.value = value;
            this.nextNode = nextNode;
        }
        @Override
        public K getKey() {
            return this.key;
        }
        @Override
        public V getValue() {
            return this.value;
        }
        @Override
        public void setValue(V value) {
            this.value = value;
        }
        public Node<K, V> getNextNode() {
            return nextNode;
        }
        public void setNextNode(Node<K, V> nextNode) {
            this.nextNode = nextNode;
        }
        public void setKey(K key) {
            this.key = key;
        }
    }
    // 测试方法.打印所有的链表元素
    public void print() {
        for (int i = 0; i < table.length; i++) {
            Node<K, V> node = table[i];
            System.out.print("下标位置[" + i + "]");
            while (node != null) {
                System.out.print("[ key:" + node.getKey() + ",value:" + node.getValue() + "]");
                node = node.nextNode;
            }
            System.out.println();
        }
    } 
}

42.ConcurrentHashMap的实现原理

在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。
https://baijiahao.baidu.com/s?id=1617089947709260129&wfr=spider&for=pc

43.ArrayMap和HashMap的对比

HashMap和ArrayMap各自的优势
1.查找效率
HashMap因为其根据hashcode的值直接算出index,所以其查找效率是随着数组长度增大而增加的。
ArrayMap使用的是二分法查找,所以当数组长度每增加一倍时,就需要多进行一次判断,效率下降。
所以对于Map数量比较大的情况下,推荐使用
2.扩容数量
HashMap初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。
ArrayMap每次扩容的时候,如果size长度大于8时申请size*1.5个长度,大于4小于8时申请8个,小于4时申请4个。
这样比较ArrayMap其实是申请了更少的内存空间,但是扩容的频率会更高。因此,如果当数据量比较大的时候,还是使用HashMap更合适,因为其扩容的次数要比ArrayMap少很多。

3.扩容效率
HashMap每次扩容的时候时重新计算每个数组成员的位置,然后放到新的位置。
ArrayMap则是直接使用System.arraycopy。
所以效率上肯定是ArrayMap更占优势。
这里需要说明一下,网上有一种传闻说因为ArrayMap使用System.arraycopy更省内存空间,这一点我真的没有看出来。arraycopy也是把老的数组的对象一个一个的赋给新的数组。当然效率上肯定arraycopy更高,因为是直接调用的c层的代码。
4.内存耗费
以ArrayMap采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap的使用。而HashMap没有这种设计。
由于ArrayMap只缓存了长度是4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当的节省内存的。
5.总结
综上所述,
数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。
而数据量比较大的时候,则推荐使用HashMap

44.HashTable实现原理

HashTable
和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。
Hashtable中的映射不是有序的。

45.TreeMap具体实现

TreeMap存储K-V键值对,通过红黑树(R-B tree)实现;
TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现;
TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;
TreeMap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过Key值的自然顺序进行排序

46.HashMap和HashTable的区别

1.共同点:都是双列集合,底层都是哈希算法
2.区别:

  • 1.HashMap是线程不安全的,效率高,JDK1.2版本
  • Hashtable是线程安全的,效率低,JDK1.0版本
  • 2.HashMap可以存储null键和null值
  • Hashtable不可以存储null键和null值

47.HashMap与HashSet的区别

HashMap HashSet
实现 Map 接口 实现 Set 接口
存储键值对 仅存储对象
调用 put() 向 map 中添加元素 调用 add() 方法向 Set 中添加元素
HashMap 使用键 (key) 计算 HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说,hashcode 可能相同,所以 equals() 方法从是用来判断对象的相等性

48.HashSet与HashMap怎么判断集合元素重复

HashSet的底层是借助HashMap来实现的,利用HashMap中Key的唯一性,来保证HashSet中不出现重复值。
为了保证HashSet中的对象不会出现重复值,在被存放元素的类中必须要重写hashCode()和equals()这两个方法。

49.ArrayList和LinkedList的区别,以及应用场景

ArrayList底层是用数组实现的,可以认为ArrayList是一个可改变大小的数组。随着越来越多的元素被添加到ArrayList中,其规模是动态增加的。
LinkedList底层是通过双向链表实现的, LinkedList和ArrayList相比,增删的速度较快。但是查询和修改值的速度较慢。
LinkedList更适合大量的循环,并且循环时进行插入或者删除。
ArrayList更适合大量的存取和删除操作。

50.数组和链表的区别

不同:链表是链式的存储结构;数组是顺序的存储结构。
链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。
链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;
数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。
相同:两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。

51.二叉树的深度优先遍历和广度优先遍历

深度优先遍历,也就是先序、中序、后续遍历
深度优先遍历从某个顶点出发,首先访问这个顶点,然后访问该顶点的第一个未被访问的邻结点,以此邻结点为顶点继续访问,同时记录其余未访问的邻接点,当一个顶点的所有邻接点都被访问时,回退一个顶点,将未访问的邻接点作为顶点,重复此步骤,直到所有结点都被访问完为止。

广度优先遍历从某个顶点出发,首先访问这个顶点,然后找出这个结点的所有未被访问的邻接点,访问完后再访问这些结点中第一个邻接点的所有结点,重复此方法,直到所有结点都被访问完为止

52.堆和栈的区别

一、程序的内存分配方式不同
栈区(stack):编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构的栈。
堆区(heap):一般是由程序员分配释放,若程序员不释放的话,程序结束时可能由OS回收,值得注意的是他与数据结构的堆是两回事,分配方式倒是类似于数据结构的链表。
二、申请方式不同
stack 由系统自动分配,heap 需要程序员自己申请。
C 中用函数 malloc分配空间,用 free 释放,C++用 new 分配,用 delete 释放。
三、申请后系统的响应不同
栈:只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录内存地址的链表,当系统收到程序的申请时,遍历该链表,寻找第一个空间大于所申请的空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的 delete 或 free 语句就能够正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会将多余的那部分重新放入空闲链表中。
四、 申请的大小限制不同
栈:在 windows 下,栈是向低地址扩展的数据结构,是一块连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域,这是由于系统是由链表在存储空闲内存地址,自然堆就是不连续的内存区域,且链表的遍历也是从低地址向高地址遍历的,堆得大小受限于计算机系统的有效虚拟内存空间,由此空间,堆获得的空间比较灵活,也比较大。
五、申请的效率不同
栈:栈由系统自动分配,速度快,但是程序员无法控制。
堆:堆是有程序员自己分配,速度较慢,容易产生碎片,不过用起来方便。
六、堆和栈的存储内容不同
栈:在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令的地址,然后函数的各个参数,在大多数的 C 编译器中,参数是从右往左入栈的,当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令。
堆:一般是在堆的头部用一个字节存放堆的大小,具体内容由程序员安排。

53.什么是深拷贝和浅拷贝?

浅拷贝(浅复制、浅克隆):
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所拷贝的对象,而不复制它所引用的对象。
深拷贝(深复制、深克隆):
被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍

54.开启线程的三种方式?

1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程

55.线程和进程的区别?

(1)进程
进程是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
(2)线程
线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
(3)联系
线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(4)区别:理解它们的差别,我从资源使用的角度出发。(所谓的资源就是计算机里的中央处理器,内存,文件,网络等等)
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

56.run()和start()方法区别

(1)通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。
(2)然后通过此Thread类调用方法run()来完成其运行操作的,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程终止,而CPU再运行其它线程
(3).run()方法当作普通方法的方式调用,程序还是要顺序执行

57.如何控制某个方法允许并发访问线程的个数?

package com.soyoungboy;

 import java.util.concurrent.Semaphore;
 /**
 *
 * @author soyoungboy 2017年1月25日15:51:15
 *
 */
public class SemaphoreTest {
static Semaphore semaphore = new Semaphore(5,true);
 public static void main(String[] args) {
 for(int i=0;i<100;i++){
 new Thread(new Runnable() {

 @Override
 public void run() {
 test();
 }
 }).start();
 }

 }

 public static void test(){
 try {
 //申请一个请求
 semaphore.acquire();
 } catch (InterruptedException e1) {
 e1.printStackTrace();
 }
 System.out.println(Thread.currentThread().getName()+"进来了");
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 System.out.println(Thread.currentThread().getName()+"走了");
 //释放一个请求
 semaphore.release();
 }
 }

构造函数创建了一个 Semaphore 对象,并且初始化了 5 个信号。这样的效果是控件 test 方法最多只能有 5 个线程并发访问,对于 5 个线程时就排队等待,走一个来一下;
请求一个信号(消费一个信号),如果信号被用完了则等待;
释放一个信号,释放的信号新的线程就可以使用了.

58. 在Java中wait和seelp方法的不同

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

59.谈谈wait/notify关键字的理解

wait():
等待对象的同步锁,需要获得该对象的同步锁才可以调用这个方法,否则编译可以通过,但运行时会收到一个异常:IllegalMonitorStateException。
调用任意对象的 wait() 方法导致该线程阻塞,该线程不可继续执行,并且该对象上的锁被释放。

notify():
唤醒在等待该对象同步锁的线程(只唤醒一个,如果有多个在等待),注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
调用任意对象的notify()方法则导致因调用该对象的 wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

notifyAll():
唤醒所有等待的线程,注意唤醒的是notify之前wait的线程,对于notify之后的wait线程是没有效果的。

60.什么导致线程阻塞?

阻塞状态的线程的特点是:该线程放弃CPU的使用,暂停运行,只有等到导致阻塞的原因消除之后才恢复运行。或者是被其他的线程中断,该线程也会退出阻塞状态,同时抛出InterruptedException
1)线程执行了Thread.sleep(intmillsecond);方法,当前线程放弃CPU,睡眠一段时间,然后再恢复执行
2)线程执行一段同步代码,但是尚且无法获得相关的同步锁,只能进入阻塞状态,等到获取了同步锁,才能回复执行。
3)线程执行了一个对象的wait()方法,直接进入阻塞状态,等待其他线程执行notify()或者notifyAll()方法。
4)线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。

61.• 线程如何关闭

1).正常关闭:推荐使用业务标志位结束线程的工作流程,待线程工作结束自行关闭,如下 mWorking 进行控制线程的业务是否继续进行
2).使用Android AsyncTask来关闭线程
3)stop()和interrupt()方法虽然可以中断线程,但是会使线程代码中逻辑执行不完整。

62. 讲一下java中的同步的方法

为何要使用同步?
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,
因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
同步的方式
1.同步方法:即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
2.同步代码块:即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
3.使用特殊域变量(volatile)实现线程同步
a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
4.使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
ReenreantLock类的常用方法有:
(1)ReentrantLock() : 创建一个ReentrantLock实例
(2)lock() : 获得锁
(3)unlock() : 释放锁

63.既然 ArrayList 是线程不安全的,怎么保证它的线程安全性呢?或者有什么替代方案?

java.util.Collections.SynchronizedList
它能把所有 List 接口的实现类转换成线程安全的List,比 Vector 有更好的扩展性和兼容性,SynchronizedList的构造方法如下:

final List<E> list;
SynchronizedList(List<E> list) {
    super(list);
    this.list = list;
}

很可惜,它所有方法都是带同步对象锁的,和 Vector 一样,它不是性能最优的。即使你能说到这里,面试官还会继续往下追问,比如在读多写少的情况,SynchronizedList这种集合性能非常差,还有没有更合适的方案?
介绍两个并发包里面的并发集合类:
java.util.concurrent.CopyOnWriteArrayList
java.util.concurrent.CopyOnWriteArraySet
CopyOnWrite集合类也就这两个,Java 1.5 开始加入,你要能说得上这两个才能让面试官信服。
CopyOnWriteArrayList
CopyOnWrite(简称:COW):即复制再写入,就是在添加元素的时候,先把原 List 列表复制一份,再添加新的元素。
添加元素时,先加锁,再进行复制替换操作,最后再释放锁。
可以看到,获取元素并没有加锁。
这样做的好处是,在高并发情况下,读取元素时就不用加锁,写数据时才加锁,大大提升了读取性能。
CopyOnWriteArraySet
CopyOnWriteArraySet逻辑就更简单了,就是使用 CopyOnWriteArrayList 的 addIfAbsent 方法来去重的,添加元素的时候判断对象是否已经存在,不存在才添加进集合。
这两种并发集合,虽然牛逼,但只适合于读多写少的情况,如果写多读少,使用这个就没意义了,因为每次写操作都要进行集合内存复制,性能开销很大,如果集合较大,很容易造成内存溢出。
总结
下次面试官问你线程安全的 List,你可以从 Vector > SynchronizedList > CopyOnWriteArrayList 这样的顺序依次说上来,这样才有带入感,也能体现你对知识点的掌握程度。

64.谈谈NIO的理解

NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
1、面向流与面向缓冲
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。
Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
Java NIO面向缓冲区的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

2、阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

3、选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

65. 字节流与字符流有什么区别:

计算机中的一切最终都是以二进制字节形式存在的,对于我们经常操作的字符串,在写入时其实都是先得到了其对应的字节,然后将字节写入到输出流,在读取时其实都是先读到的是字节,然后将字节直接使用或者转换为字符给我们使用。由于对于字节和字符两种操作的需求比较广泛,所以 Java 专门提供了字符流与字节流相关IO类。对于程序运行的底层设备来说永远都只接受字节数据,所以当我们往设备写数据时无论是字节还是字符最终都是写的字节流。字符流是字节流的包装类,所以当我们将字符流向字节流转换时要注意编码问题(因为字符串转成字节数组的实质是转成该字符串的某种字节编码)。字符流和字节流的使用非常相似,但是实际上字节流的操作不会经过缓冲区(内存)而是直接操作文本本身的,而字符流的操作会先经过缓冲区(内存)然后通过缓冲区再操作文件。

字符流和字节流的使用非常相似,但是实际上字节流的操作不会经过缓冲区(内存)而是直接操作文本本身的,而字符流的操作会先经过缓冲区(内存)然后通过缓冲区再操作文件。

66. 字节流和字符流哪个好,如何选择?

缓大多数情况下使用字节流会更好,因为字节流是字符流的包装,而大多数时候 IO 操作都是直接操作磁盘文件,所以这些流在传输时都是以字节的方式进行的(图片等都是按字节存储的)。
而如果对于操作需要通过 IO 在内存中频繁处理字符串的情况使用字符流会好些,因为字符流具备缓冲区,提高了性能。

67.• 死锁的四个必要条件?

死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进

二、死锁产生的原因

  1. 系统资源的竞争
    系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。
  2. 进程运行推进顺序不合适
    进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。

1.互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2。请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
3.不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
4.循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

68.怎么避免死锁?

我们可以通过破坏死锁产生的4个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。
破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

69. 谈谈对多线程的理解

为了解决负载均衡问题,充分利用CPU资源.为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰多线程的好处:
1.使用线程可以把占据时间长的程序中的任务放到后台去处理
2.用户界面更加吸引人,这样比如用户点击了一个按钮去触发某件事件的处理,可以弹出一个进度条来显示处理的进度
3.程序的运行效率可能会提高
4.在一些等待的任务实现上如用户输入,文件读取和网络收发数据等,线程就比较有用了.

多线程的缺点:
1.如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换.
2.更多的线程需要更多的内存空间
3.线程中止需要考虑对程序运行的影响.
4.通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生

70.如何保证多线程读写文件的安全?

多线程文件并发安全其实就是在考察线程并发安全,最普通的方式就是使用 wait/notify、Condition、synchronized、ReentrantLock 等方式,这些方式默认都是排它操作(排他锁),也就是说默认情况下同一时刻只能有一个线程可以对文件进行操作,所以可以保证并发文件操作的安全性,但是在并发读数量远多于写数量的情况下性能却不那么好。因此推荐使用 ReadWriteLock 的实现类 ReentrantReadWriteLock,它也是 Lock 的一种实现,允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。所以相对于排他锁来说提高了并发效率。ReentrantReadWriteLock 读写锁里面维护了两个继承自 Lock 的锁,一个用于读操作(ReadLock),一个用于写操作(WriteLock)。

71.• 多线程断点续传原理

在下载大文件的时候,我们往往要使用多线程断点续传,保证数据的完整性,首先说多线程,我们要多线程下载一个大文件,就有开启多个线程,多个connection,既然是一个文件分开几个线程来下载,那肯定就是一个线程下载一个部分,如果文件的大小是200M, 使用两个线程下载, 第一个线程下载1-100M, 第二个线程下载101-200M。
我们在请求的header里面设置conn.setRequestProperty(“Range”, “bytes=”+startPos+"-"+endPos);
这里startPos是指从数据端的哪里开始,endPos是指数据端的结束
根据这样我们就知道,只要多个线程,按顺序指定好开始跟结束,就可以解决下载冲突的问题了。
每个线程找到自己开始写的位置,就是seek(startPos)
这样就可以保证数据的完整性,也不会重复写入了

RandomAccessFile threadFile = new RandomAccessFile(this.saveFile,"rwd");  
threadFile.seek(startPos);  
threadFile.write(buffer,0,offset);

断点续传其实也很简单,原理就是使用数据库保存上次每个线程下载的位置和长度
例如我开了两个线程T1,T2来下载一个文件,设文件总大小为1024M,那么就是每个线程下载512M
可是我的下载中断了,那么我下次启动线程的时候(继续下载),是不是应该要知道,我原来下载了多少呢
所以是这样的,每下载一点,就更新数据库的数据

Android面试题

Android基础

72.• 四大组件生命周期及其基本用法

1.Activity::一个Activity通常就是一个单独的屏幕,它上面可以显示一些控件也可以监听并处理用户的事件做出响应。
Activity之间通过Intent进行通信
在这里插入图片描述
2.service:一个Service 是一段长生命周期的,没有用户界面的程序,可以用来开发如监控类程序。
在这里插入图片描述
3.BroadcastReceive广播接收器生命周期:
生命周期只有十秒左右,如果在 onReceive() 内做超过十秒内的事情,就会报ANR(Application No Response) 程序无响应的错误信息
它的生命周期为从回调onReceive()方法开始到该方法返回结果后结束
有动态注册和静态注册
4.Content Provider内容提供者 :
android平台提供了Content Provider使一个应用程序的指定数据集提供给其他应用程序。这些数据可以存储在文件系统中、在一个SQLite数据库、或以任何其他合理的方式,
其他应用可以通过ContentResolver类(见ContentProviderAccessApp例子)从该内容提供者中获取或存入数据.(相当于在应用外包了一层壳),
只有需要在多个应用程序间共享数据是才需要内容提供者。例如,通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中
它的好处:统一数据访问方式。

73.Activity之间的通信方式

Intent
借助类的静态变量
借助全局变量/Application
借助外部工具
– 借助SharedPreference
– 使用Android数据库SQLite
– 赤裸裸的使用File
– Android剪切板
借助Service

74.横竖屏切换时,Activity各种情况下的生命周期

1.AndroidManifest没有设置configChanges属性
竖屏启动:
onCreate -->onStart–>onResume
切换横屏:
onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart -->onRestoreInstanceState–>onResume -->onPause -->onStop -->onDestroy
(Android 6.0 Android 7.0 Android 8.0)
横屏启动:
onCreate -->onStart–>onResume
切换竖屏:
onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart -->onRestoreInstanceState–>onResume -->onPause -->onStop -->onDestroy
(Android 6.0 Android 7.0 Android 8.0)
总结:没有设置configChanges属性Android 6.0 7.0 8.0 系统手机 表现都是一样的,当前的界面调用onSaveInstanceState走一遍流程,然后重启调用onRestoreInstanceState再走一遍完整流程,最终destory。

2.AndroidManifest设置了configChanges android:configChanges=“orientation”
竖屏启动:
onCreate -->onStart–>onResume
切换横屏:
onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart -->onRestoreInstanceState–>onResume -->onPause -->onStop -->onDestroy
(Android 6.0)
onConfigurationChanged–>onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart -->onRestoreInstanceState–>onResume -->onPause -->onStop -->onDestroy
(Android 7.0)
onConfigurationChanged
(Android 8.0)
横屏启动:
onCreate -->onStart–>onResume
切换竖屏:
onPause -->onSaveInstanceState -->onStop -->onDestroy -->onCreate–>onStart -->onRestoreInstanceState–> onResume -->onPause -->onStop -->onDestroy
(Android 6.0 )
onConfigurationChanged–>onPause -->onSaveInstanceState -->onStop -->onDestroy -->
onCreate–>onStart -->onRestoreInstanceState–>onResume -->onPause -->onStop -->onDestroy
(Android 7.0)
onConfigurationChanged
(Android 8.0)
总结:设置了configChanges属性为orientation之后,Android6.0 同没有设置configChanges情况相同,完整的走完了两个生命周期,调用了onSaveInstanceState和onRestoreInstanceState方法;Android 7.0则会先回调onConfigurationChanged方法,剩下的流程跟Android 6.0 保持一致;Android 8.0 系统更是简单,
只是回调了onConfigurationChanged方法,并没有走Activity的生命周期方法。

3.AndroidManifest设置了configChanges
android:configChanges=“orientation|keyboardHidden|screenSize”
竖(横)屏启动:onCreate -->onStart–>onResume
切换横(竖)屏:onConfigurationChanged (Android 6.0 Android 7.0 Android 8.0)
总结:设置android:configChanges=“orientation|keyboardHidden|screenSize” 则都不会调用Activity的其他生命周期方法,只会调用onConfigurationChanged方法。

4.AndroidManifest设置了configChanges
android:configChanges=“orientation|screenSize”
竖(横)屏启动:onCreate -->onStart–>onResume
切换横(竖)屏:onConfigurationChanged (Android 6.0 Android 7.0 Android 8.0)
总结:没有了keyboardHidden跟3是相同的,orientation代表横竖屏切换 screenSize代表屏幕大小发生了改变,
设置了这两项就不会回调Activity的生命周期的方法,只会回调onConfigurationChanged 。

5.AndroidManifest设置了configChanges
android:configChanges=“orientation|keyboardHidden”
总结:跟只设置了orientation属性相同,Android6.0 Android7.0会回调生命周期的方法,Android8.0则只回调onConfigurationChanged。说明如果设置了orientation 和 screenSize 都不会走生命周期的方法,keyboardHidden不影响。
1.不设置configChanges属性不会回调onConfigurationChanged,且切屏的时候会回调生命周期方法。
2.只有设置了orientation 和 screenSize 才会保证都不会走生命周期,且切屏只回调onConfigurationChanged。
3.设置orientation,没有设置screenSize,切屏会回调onConfigurationChanged,但是还会走生命周期方法。
注:这里只选择了Android部分系统的手机做测试,由于不同系统的手机品牌也不相同,可能略微会有区别。
另:
代码动态设置横竖屏状态(onConfigurationChanged当屏幕发生变化的时候回调)
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
获取屏幕状态(int ORIENTATION_PORTRAIT = 1; 竖屏 int ORIENTATION_LANDSCAPE = 2; 横屏)
int screenNum = getResources().getConfiguration().orientation;
configChanges属性

  1. orientation 屏幕在纵向和横向间旋转
    2.keyboardHidden 键盘显示或隐藏
    3.screenSize 屏幕大小改变了
    4.fontScale 用户变更了首选的字体大小
    5.locale 用户选择了不同的语言设定
    6.keyboard 键盘类型变更,例如手机从12键盘切换到全键盘
    7.touchscreen或navigation 键盘或导航方式变化,一般不会发生这样的事件
    常用的包括:orientation keyboardHidden screenSize,设置这三项界面不会走Activity的生命周期,只会回调onConfigurationChanged方法。
    screenOrientation属性
    1.unspecified 默认值,由系统判断状态自动切换
    2.landscape 横屏
  2. portrait 竖屏
    4.user 用户当前设置的orientation值
  3. behind 下一个要显示的Activity的orientation值
  4. sensor 使用传感器 传感器的方向
  5. nosensor 不使用传感器 基本等同于unspecified
    仅landscape和portrait常用,代表界面默认是横屏或者竖屏,还可以再代码中更改。

75.• Activity与Fragment之间生命周期比较

在这里插入图片描述
Fragment生命周期
onAttach
onCreate
onCreateView
onActivityCreate ______以上相当于Activity的onCreate方法

onStart ______相当于Activity的onStart方法
onResume ______相当于Activity的onResume方法
onPause ______相当于Activity的onPause方法
onStop ______相当于Activity的onStop方法

onDestroyView
onDestroy
onDetach ______以上相当于Activity的onDestroy方法

当Activity包含一个Fragment的时候,Activity和Fragment生命周期的变化:
Activity(onCreate)—> Fragment(onAttach onCreate onCreateView onActivityCreate)—>
Activity(onStart)—> Fragment(onStart)—>
Activity(onResume)—> Fragment(onResume)—>
Fragment(onPause)—> Activity(onPause)—>
Fragment(onStop)—> Activity(onStop)—>
Fragment(onDestroyView onDestroy onDetach)—> Activity(onDestroy)
由于Fragment依附于Activity,所以启动的时候Activity的方法肯定在前面,Fragment的方法在后面,但是在要销毁的时候,Fragment的方法先执行,再执行Activity的方法。

在宿主Activity中使用hide、show方式切换Fragment的时候,Fragment的生命周期是:
a 初始化
Fragment1(onAttach onCreate onCreateView onActivityCreate) —> Fragment1(onStart)—> Fragment1(onResume)
Fragment2(onAttach onCreate onCreateView onActivityCreate) —> Fragment2(onStart)—> Fragment2(onResume)
b Fragment1和Fragment2来回切换都没有回调生命周期
c 当某一个Fragment调用了跳转到另一个Activity的时候(或者按HOME键的时候)
Fragment1(onPause)—> Fragment1(onStop)
Fragment2(onPause)—> Fragment2(onStop)
d 当在一个透明的Activity中弹出一个Dialog时(与Activity的情况相同)
Fragment1(onPause)
Fragment2(onPause)
e 当宿主Activity被销毁的时候
Fragment1(onPause)—> Fragment1(onStop)—> Fragment1(onDestroyView onDestroy onDetach)
Fragment2(onPause)—> Fragment2(onStop)—> Fragment2(onDestroyView onDestroy onDetach)

当采用FragmentStatePagerAdapter适配器加载‘Fragment的时候,Fragment的生命周期同上面的情况相同。

76.• Activity上有Dialog的时候按Home键时的生命周期

Dialog与 DialogFragment 都不会出发activity的生命周期变动。
也就是说,Dialog的show与dismiss不会引发activity的onPause()和onResume的执行。

77.• 两个Activity 之间跳转时必然会执行的是哪几个方法?

一般情况下比如说有两个activity,分别叫A,B。
当在A 里面激活B 组件的时候, A会调用onPause()方法,然后B调用onCreate() ,onStart(), onResume()。
这个时候B覆盖了A的窗体, A会调用onStop()方法。
如果B是个透明的窗口,或者是对话框的样式, 就不会调用A的onStop()方法。
如果B已经存在于Activity栈中,B就不会调用onCreate()方法。

78.• Activity的四种启动模式对比

standard:标准模式:如果在mainfest中不设置就默认standard;standard就是新建一个Activity就在栈中新建一个activity实例;
singleTop:栈顶复用模式:与standard相比栈顶复用可以有效减少activity重复创建对资源的消耗,但是这要根据具体情况而定,不能一概而论;
singleTask:栈内单例模式,栈内只有一个activity实例,栈内已存activity实例,在其他activity中start这个activity,Android直接把这个实例上面其他activity实例踢出栈GC掉;
singleInstance :堆内单例:整个手机操作系统里面只有一个实例存在就是内存单例;
Activity四种启动模式常见使用场景:
LauchMode Instance
standard mainfest中没有配置就默认标准模式
singleTop 登录页面、WXPayEntryActivity、WXEntryActivity 、推送通知栏
singleTask 程序模块逻辑入口:主页面(Fragment的containerActivity)、WebView页面、扫一扫页面
singleInstance 系统Launcher、锁屏键、来电显示等系统应用

79.• Activity状态保存与恢复

第一: 有哪些状态是需要保存的?
有哪些状态是需要保存的呢?最简单明了的就是对一些数据的保存,比如你正在操作一些数据,当面临突发情况,你的数据还没有操作完,这时候你就需要将数据进行保存,以便我们再次回到这个页面的时候不用重头再来。

第二: 什么情况下需要Activity状态的保存与恢复?
有些设备配置可能会在运行时发生变化(例如屏幕方向、键盘可用性及语言)。 发生这种变化时,Android 会重启正在运行的 Activity(先后调用 onDestroy() 和 onCreate())。重启行为旨在通过利用与新设备配置匹配的备用资源自动重新加载您的应用,来帮助它适应新配置。
要妥善处理重启行为,Activity 必须通过常规的Activity 生命周期恢复其以前的状态,在 Activity 生命周期中,Android 会在销毁 Activity 之前调用 onSaveInstanceState(),以便您保存有关应用状态的数据。 然后,您可以在 onCreate() 或 onRestoreInstanceState() 期间恢复 Activity 状态。

首先是在onSaveInstanceState中保存数据

   @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        //保存销毁之前的数据
        outState.putString("number",mMNumber.getText().toString());
        Log.d(TAG, "onSaveInstanceState");

    }

在onRestoreInstanceState对数据进行恢复

   @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        Log.d(TAG, "onRestoreInstanceState");
        //恢复数据
       String s = savedInstanceState.getString("number");
       mMNumber.setText(s);
    }

到这里我们总结一下这个方法的调用过程,当运行时配置发生变更的时候,程序总的会销毁当前的Activity,然后重新创建一个新的Activity,在这个过程中,销毁当前Activity之前会先调用onSaveInstanceState让我们来保存数据,然后重建Activity在调用onCreat方发之后会调用onRestoreInstanceState让我们来对数据进行恢复,当然也可以在onCret中进行数据恢复
第五: 这个知识点你需要注意的地方?

通过以上的讲诉,相信你对Activity状态的保存与恢复已经掌握的差不多了,在这里我再补充几点

  1. 关于onSaveInstanceState
    这个方法默认情况下会自动保存有关Activity的视图层次结构的状态信息,简单举个例子,我们以系统控件EditText来说,系统默认会保存有关这个控件的一个信息,也就是当你在这个控件中输入内容的时候,即使旋转屏幕内容也不会丢失,因为系统已经默认为其实现了我们说的那两个方法,但是有个前提,这个控件必须设置id,否则数据依旧会丢失,另外如果你重写了onRestoreInstanceState也要保证必须有这行代码

super.onRestoreInstanceState(savedInstanceState);
关于旋转屏幕无法调用onSaveInstanceState的问题。
出现这种问题你复写的肯定以下方法
public void onSaveInstanceState (Bundle outState, PersistableBundle outPersistentState);
改成以下方法即可
public void onSaveInstanceState (Bundle outState);

80.fragment各种情况下的生命周期

Activity中调用replace()方法时的生命周期
新替换的Fragment:onAttach > onCreate > onCreateView > onViewCreated > onActivityCreated > onStart > onResume
被替换的Fragment:onPause > onStop > onDestroyView > onDestroy > onDetach

Activity中调用replace()方法和addToBackStack()方法时的生命周期
新替换的Fragment(没有在BackStack中):onAttach > onCreate > onCreateView > onViewCreated > onActivityCreated > onStart > onResume
新替换的Fragment(已经在BackStack中):onCreateView > onViewCreated > onActivityCreated > onStart > onResume
被替换的Fragment:onPause > onStop > onDestroyView

Fragment在运行状态后跟随Activity的生命周期
Fragment在上述的各种情况下进入了onResume后,则进入了运行状态,以下4个生命周期方法将跟随所属的Activity一起被调用:
onPause > onStop > onStart > onResume

81. fragment之间传递数据的方式?

Intent传值
广播传值
静态调用
本地化存储传值
暴露接口/方法调用
eventBus等之类的

82.• 怎么在Activity 中启动自己对应的Service?

第一种方式:通过StartService启动Service
通过startService启动后,service会一直无限期运行下去,只有外部调用了stopService()或stopSelf()方法时,该Service才会停止运行并销毁。
第二种方式:通过bindService启动Service
bindService启动服务特点:
1.bindService启动的服务和调用者之间是典型的client-server模式。调用者是client,service则是server端。service只有一个,但绑定到service上面的client可以有一个或很多个。这里所提到的client指的是组件,比如某个Activity。
2.client可以通过IBinder接口获取Service实例,从而实现在client端直接调用Service中的方法以实现灵活交互,这在通过startService方法启动中是无法实现的。
3.bindService启动服务的生命周期与其绑定的client息息相关。当client销毁时,client会自动与Service解除绑定。当然,client也可以明确调用Context的unbindService()方法与Service解除绑定。当没有任何client与Service绑定时,Service会自行销毁。

83.• 谈谈你对ContentProvider的理解

特点
Android四大组件之一,需要进行注册,一般有name,authorities,export等属性
是一种定义数据共享的接口,并是不android数据存储的方式之一
跨进程数据访问
android系统很多系统应用都是使用ContentProvider方式进行储存的(图片,通讯录,视频,音频等)
数据更新监听方便
优缺点
优点:
为数据访问提供统一的接口,解决了不同储存方式需要不同的API的访问所带来的繁琐
跨进程数据的访问,实现了不同App数据访问提供了很大的便利
缺点:
不能单独使用,必须需要和其他的储存方式结合使用

84.android广播分类

a.标准广播
标准广播发出以后,所有的广播接收器,可以几乎在同一时刻同时接受到这条广播。
优点:效率高
缺点:不能被截断。
b.有序广播
有序广播发出以后,同一时刻只能有一个广播接收器收到这条广播。优先级高的广播先接受到这条
广播。在当前广播接收器处理完自己的逻辑以后,可以执行两种动作:
1.继续传递广播
2.将广播截断

85.• 广播使用的方式

(1)静态注册(常驻型广播):在清单文件中注册, 常见的有监听设备启动,常驻注册不会随程序生命周期改变,适用于长期监听,这种常驻型广播当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。

(2)动态注册(非常驻型广播):在代码中注册,这种方式注册的广播会跟随程序的生命周期。随着程序的结束,也就停止接受广播了。使用与一些需要与生命周期同步的监听。

86.本地广播和全局广播有什么差别?

一、应用场景不同
1、BroadcastReceiver用于应用之间的传递消息;
2、而LocalBroadcastManager用于应用内部传递消息,比broadcastReceiver更加高效。
二、使用安全性不同
1、BroadcastReceiver使用的Content API,所以本质上它是跨应用的,所以在使用它时必须要考虑到不要被别的应用滥用;
2、LocalBroadcastManager不需要考虑安全问题,因为它只在应用内部有效。

1、本地广播:发送的广播事件不被其他应用程序获取,也不能响应其他应用程序发送的广播事件。本地广播只能被动态注册,不能静态注册。动态注册或方法时需要用到LocalBroadcastManager。
实现原理:单例LocalBroadcastManager

2、全局广播:发送的广播事件可被其他应用程序获取,也能响应其他应用程序发送的广播事件(可以通过 exported–是否监听其他应用程序发送的广播 在清单文件中控制) 全局广播既可以动态注册,也可以静态注册。
(1)不让别的应用收到自己发送的广播
在Androidmanifest.xml中为BroadcastReceiver添加权限,如果是自定义权限记得先声明:

发送广播是传入:
public void sendOrderedBroadcast(Intent intent, String receiverPermission)
sendBroadcast(intent, "com.android.permission.quanxian ");
只有具有permission权限的Receiver才能接收此广播要接收该广播,在Receiver应用的AndroidManifest.xml中要添加对应的相应的权限。

(2)过滤掉自己不愿接收的广播。
android:exported 此broadcastReceiver能否接收其他App的发出的广播,这个属性默认值有点意思,其默认值是由receiver中有无intent-filter决定的,如果有intent-filter,默认值为true,否则为false。
android:permission设置之后,只有具有相应权限的广播发送方发送的广播才能被此broadcastReceiver所接收;
上述设置也可以达到接收或者不接收的目的,但是全局广播其实原理是利用binder机制和AMS进行交互如果只是应用内使用,资源耗费或者说延时还是可以优化的。

87.• AlertDialog,popupWindow,Activity区别

AlertDialog :用来提示用户一些信息,用起来也比较简单,设置标题类容 和按钮即可,如果是加载的自定义的view ,用 dialog.setView(layout);加载布局即可(其实就是一个布满全屏的空间)
popupWindow:就是一个悬浮在Activity之上的窗口,可以用展示任意布局文件
activity:是Android系统中的四大组件之一,可以用于显示View。Activity是一个与用户交互的系统模块,几乎所有的Activity都是和用户进行交互的
区别:

AlertDialog是非阻塞式对话框:AlertDialog弹出时,后台还可以做事情;

而PopupWindow是阻塞式对话框:PopupWindow弹出时,程序会等待,在PopupWindow退出前,程序一直等待,只有当我们调用了dismiss方法的后,PopupWindow退出,程序才会向下执行。

88• Application 和 Activity 的 Context 对象的区别

在需要传递Context参数的时候,如果是在Activity中,我们可以传递this(这里的this指的是Activity.this,是当前Activity的上下文)或者Activity.this。这个时候如果我们传入getApplicationContext(),我们会发现这样也是可以用的。可是大家有没有想过传入Activity.this和传入getApplicationContext()的区别呢?首先Activity.this和getApplicationContext()返回的不是同一个对象,一个是当前Activity的实例,一个是项目的Application的实例,这两者的生命周期是不同的,它们各自的使用场景不同,this.getApplicationContext()取的是这个应用程序的Context,它的生命周期伴随应用程序的存在而存在;而Activity.this取的是当前Activity的Context,它的生命周期则只能存活于当前Activity,这两者的生命周期是不同的。getApplicationContext() 生命周期是整个应用,当应用程序摧毁的时候,它才会摧毁;Activity.this的context是属于当前Activity的,当前Activity摧毁的时候,它就摧毁。

Android源码相关分析

89.• invalidate和postInvalidate的区别及使用

Android中实现view的更新有两组方法,一组是invalidate,另一组是postInvalidate,其中前者是在UI线程自身中使用,而后者在非UI线程中使用。
1,利用invalidate()刷新界面
  实例化一个Handler对象,并重写handleMessage方法调用invalidate()实现界面刷新;而在线程中通过sendMessage发送界面更新消息。
2,使用postInvalidate()刷新界面
使用postInvalidate则比较简单,不需要handler,直接在线程中调用postInvalidate即可。

90.• Activity-Window-View三者的差别

Activity是安卓四大组件之一,负责界面展示、用户交互与业务逻辑处理;
Window就是负责界面展示以及交互的职能部门,就相当于Activity的下属,Activity的生命周期方法负责业务的处理;
View就是放在Window容器的元素,Window是View的载体,View是Window的具体展示。
然后一句话介绍下三者的关系:
Activity通过Window来实现视图元素的展示,window可以理解为一个容器,盛放着一个个的view,用来执行具体的展示工作。

91.• 如何优化自定义View

(1)减少不必要的代码:对于频繁调用的方法,需要尽量减少不必要的代码。

(2)不在 onDraw 中做内存分配的事:先从 onDraw 开始,需要特别注意不应该在这里做内存分配的事情,因为它会导致 GC,从而导致卡顿。在初始化或者动画间隙期间做分配内存的动作。不要在动画正在执行的时候做内存分配的事情。

(3)减少 onDraw 被调用的次数:大多数时候导致 onDraw 都是因为调用了 invalidate().因此请尽量减少调用 invaildate()的次数。如果可能的话,尽量调用含有 4 个参数的 invalidate()方法而不是没有参数的 invalidate()。没有参数的 invalidate 会强制重绘整个 view。

(4)减少 layout 的次数:一个非常耗时的操作是请求 layout。任何时候执行 requestLayout(),会使得 Android UI 系统去遍历整个View 的层级来计算出每一个 view 的大小。如果找到有冲突的值,它会需要重新计算好几次。

(5)选用扁平化的 View:另外需要尽量保持 View 的层级是扁平化的(去除冗余、厚重和繁杂的装饰效果),这样对提高效率很有帮助。

(6)复杂的 UI 使用 ViewGroup:如果你有一个复杂的 UI,你应该考虑写一个自定义的 ViewGroup 来执行他的 layout 操作。(与内置的view 不同,自定义的 view 可以使得程序仅仅测量这一部分,这避免了遍历整个 view 的层级结构来计算大小。)继承 ViewGroup作为自定义 view 的一部分,有子 views,但是它从来不测量它们。而是根据他自身的 layout 法则,直接设置它们的大小。

92 • 低版本SDK如何实现高版本api?

在低版本的手机系统中,如果直接使用高版本的API肯定会报:“NoSuchMethod”的Crash的。
所以正确的做法应该是在注解的方法中,做版本判断,在低版本中依然使用老的方式处理。版本判断时我们需要判断具体的版本号

93 • 描述一次网络请求的流程

,大概是经历了域名解析、TCP的三次握手、建立TCP连接后发起HTTP请求、服务器响应HTTP请求、解析服务器数据
1.域名解析:会解析域名对应的IP地址。
2. TCP的三次握手
第一次握手,客户端发了个连接请求消息到服务端,服务端收到信息后知道自己与客户端是可以连接成功的,但此时客户端并不知道服务端是否已经接收到了它的请求,所以服务端接收到消息后的应答,客户端得到服务端的反馈后,才确定自己与服务端是可以连接上的,这就是第二次握手。
客户端只有确定了自己能与服务端连接上才能开始发数据。所以两次握手肯定是最基本的。
3.建立TCP连接后发起HTTP请求
TCP三次握手建立连接成功后,客户端按照指定的格式开始向服务端发送HTTP请求,服务端接收请求后,解析HTTP请求,处理完业务逻辑,最后返回一个具有标准格式的HTTP响应给客户端。
4.客户端得到服务器响应的数据,从而开始对应的解析流程。

94.HttpUrlConnection 和 okhttp关系

我们最熟悉的肯定是HttpUrlConnection,这是google官方提供的用来访问网络,但是HttpUrlConnection实现的比较简单,只支持1.0/1.1,并没有上面讲的多路复用,如果碰到app大量网络请求的时候,性能比较差,而且HttpUrlConnection底层也是用Socket来实现的。

OkHttp 与 HttpUrlConnection一样,实现了一个网络连接的过程( OkHttp 和 HttpUrlConnection是一级的)。OkHttp使用的是sink和source,这两个是在Okio这个开源库里的,sink相当于outputStream,source相当于是inputStream。Sink和source比InputSrteam和OutputSrewam更加强大(其子类有缓冲BufferedSink、支持Gzip压缩GzipSink、服务于GzipSink的ForwardingSink和InflaterSink)

95.• Bitmap对象的理解

1.Bitmap在Android中指的是一张图片。
2.通过BitmapFactory类提供的四类方法:
1)decodeFile(从文件系统加载出一个Bitmap对象)
2)decodeResource(从资源中加载出一个Bitmap对象)
3)decodeStream(从输入流中加载出一个Bitmap对象)
4)decodeByteArray(从字节数组中加载出一个Bitmap对象)
其中decodeFile , decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。
3.BitmapFactory.Options的参数
①inSampleSize参数
上述四类方法都支持BitmapFactory.Options参数,而Bitmap的按一定采样率进行缩放就是通过BitmapFactory.Options参数实现的,主要用到了inSampleSize参数,即采样率。通过对inSampleSize的设置,对图片的像素的高和宽进行缩放。
当inSampleSize=1,即采样后的图片大小为图片的原始大小。小于1,也按照1来计算。
当inSampleSize>1,即采样后的图片将会缩小,缩放比例为1/(inSampleSize的二次方)。
关于inSampleSize取值的注意事项:
通常是根据图片宽高实际的大小/需要的宽高大小,分别计算出宽和高的缩放比。但应该取其中最小的缩放比,避免缩放图片太小,到达指定控件中不能铺满,需要拉伸从而导致模糊。
②inJustDecodeBounds参数
我们需要获取加载的图片的宽高信息,然后交给inSampleSize参数选择缩放比缩放。
想先不加载图片却能获得图片的宽高信息,可通过inJustDecodeBounds=true,然后加载图片就可以实现只解析图片的宽高信息,并不会真正的加载图片,所以这个操作是轻量级的。
当获取了宽高信息,计算出缩放比后,然后在将inJustDecodeBounds=false,再重新加载图片,就可以加载缩放后的图片。
4.高效加载Bitmap的流程
①将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。这里要设置Options.inJustDecodeBounds=true,这时候decode的bitmap为null,只是把图片的宽高放在Options里,
②从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
③根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
④将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。

96.• looper架构(handler的运行机制)

Handler里面有一个重要的成员变量Looper,Looper里面维护了一个MessageQueue(消息队列),当我们使用handler.post或者sendMessage相关的方法都是将消息Message放入到消息队列中。每一个线程都将拥有一个自己的Looper,是通过:

1static final ThreadLocal sThreadLocal
实现的,顾名思义ThreadLocal是和线程绑定的。当我们有一个线程A使用sThreadLocal.set(Looper a),线程B使用sThreadLocal.set(Looper b)的方式存储,如果我们在线程B中使用sThreadLocal.get()将会得到Looper b的实例。所以我们每个线程可以拥有独立的Looper,Looper里面维护了一个消息队列,也就是每一个线程维护自己的消息队列。

当在主线程中时,在你的应用启动时系统便给我们创建了一个MainLooper存入了sThreadLocal中,所以平时我们使用Handler时,如果是在主线程中创建的,我们是不需再去创建一个Looper给Handler的,因为系统已经做了,所以当我们new Handler时,系统便将之前存入的Looper通过sThreadLoca中get出来,然后再去从对应的消息队列中读取执行。

而当我们在子线程中创建Handler时,如果直接new Handler运行时肯定会报错的,提示我们必须先调用Looper.prepare()方法,为什么呢?因为我们没有创建子线程对应的Looper放入sThreadLocal当中,而prepare方法就是new了一个Looper的实例通过sThreadLocal.set设置到当前线程的。整个建立过程类似于下图:

也就是说,Handler创建的时候肯定会在一个线程当中(主线程或者子线程),并且创建一个Looper实例与此线程绑定(无论是系统帮我们创建或者通过prepare自己绑定),在Looper中维护一个消息队列,然后looper循环的从消息队列中读取消息执行(在消息队列所在线程执行)。这就是整个Handler的运行机制了。

97.• ActivityThread,AMS,WMS的工作原理

AMS统一调度所有应用程序的Activity
WMS控制所有Window的显示与隐藏以及要显示的位置
在视图层次中,Activity在WIndow之上,如下图
在这里插入图片描述
WMS(WindowManagerService)
管理的整个系统所有窗口的UI

作用:
为所有窗口分配Surface:客户端向WMS添加一个窗口的过程,其实就是WMS为其分配一块Suiface的过程,一块块Surface在WMS的管理下有序的排布在屏幕上。Window的本质就是Surface。(简单的说Surface对应了一块屏幕缓冲区)
管理Surface的显示顺序、尺寸、位置
管理窗口动画
输入系统相关:WMS是派发系统按键和触摸消息的最佳人选,当接收到一个触摸事件,它需要寻找一个最合适的窗口来处理消息,而WMS是窗口的管理者,系统中所有的窗口状态和信息都在其掌握之中,完成这一工作不在话下。

AMS(ActivityManagerService)
ActivityManager是客户端用来管理系统中正在运行的所有Activity包括Task、Memory、Service等信息的工具。但是这些这些信息的维护工作却不是又ActivityManager负责的。在ActivityManager中有大量的get()方法,那么也就说明了他只是提供信息给AMS,由AMS去完成交互和调度工作。

作用:

统一调度所有应用程序的Activity的生命周期
启动或杀死应用程序的进程
启动并调度Service的生命周期
注册BroadcastReceiver,并接收和分发Broadcast
启动并发布ContentProvider
调度task
处理应用程序的Crash
查询系统当前运行状态

启动流程:

第一阶段:启动ActivityManagerService。
第二阶段:调用setSystemProcess。
第三阶段:调用installSystemProviders方法。
第四阶段:调用systemReady方法。

工作流程:

AMS的工作流程,其实就是Activity的启动和调度的过程,所有的启动方式,最终都是通过Binder机制的Client端,调用Server端的AMS的startActivityXXX()系列方法。所以可见,工作流程又包括Client端和Server端两个。

Client端流程:

Launcher主线程捕获onClick()点击事件后,调用Launcher.startActivitySafely()方法。Launcher.startActivitySafely()内部调用了Launcher.startActivity()方法,Launcher.startActivity()内部调用了Launcher的父类Activity的startActivity()方法。

Activity.startActivity()调用Activity.startActivityForResult()方法,传入该方法的requestCode参数若为-1,则表示Activity启动成功后,不需要执行Launcher.onActivityResult()方法处理返回结果。

启动Activity需要与系统ActivityManagerService交互,必须纳入Instrumentation的监控,因此需要将启动请求转交instrumentation,即调用Instrumentation.execStartActivity()方法。

Instrumentation.execStartActivity()首先通过ActivityMonitor检查启动请求,然后调用ActivityManagerNative.getDefault()得到ActivityManagerProxy代理对象,进而调用该代理对象的startActivity()方法。

ActivityManagerProxy是ActivityManagerService的代理对象,因此其内部存储的是BinderProxy,调用ActivityManagerProxy.startActivity()实质是调用BinderProxy.transact()向Binder驱动发送START_ACTIVITY_TRANSACTION命令。Binder驱动将处理逻辑从Launcher所在进程切换到ActivityManagerService所在进程。

Server端流程:

启动Activity的请求从Client端传递给Server端后,便进入了启动应用的七个阶段,这里也是整理出具体流程。

1)预启动

ActivityManagerService.startActivity()
ActivityStack.startActivityMayWait()
ActivityStack.startActivityLocked()
ActivityStack.startActivityUncheckedLocked()
ActivityStack.startActivityLocked()(重载)
ActivityStack.resumeTopActivityLocked()

2)暂停

ActivityStack.startPausingLocked()
ApplicationThreadProxy.schedulePauseActivity()
ActivityThread.handlePauseActivity()
ActivityThread.performPauseActivity()
ActivityManagerProxy.activityPaused()
completePausedLocked()

3)启动应用程序进程

第二次进入ActivityStack.resumeTopActivityLocked()
ActivityStack.startSpecificActivityLocked()
startProcessLocked()
startProcessLocked()(重载)
Process.start()

4)加载应用程序Activity

ActivityThread.main()
ActivityThread.attach()
ActivityManagerService.attachApplication()
ApplicationThread.bindApplication()
ActivityThread.handleBindApplication()

5)显示Activity

ActivityStack.realStartActivityLocked()
ApplicationThread.scheduleLaunchActivity()
ActivityThead.handleLaunchActivity()
ActivityThread.performLaunchActivity()
ActivityThread.handleResumeActivity()
ActivityThread.performResumeActivity()
Activity.performResume()
ActivityStack.completeResumeLocked()

6)Activity Idle状态的处理

7)停止源Activity
ActivityStack.stopActivityLocked()
ApplicationThreadProxy.scheduleStopActivity()
ActivityThread.handleStopActivity()
ActivityThread.performStopActivityInner()

98.• 自定义View如何考虑机型适配

布局类建议:

合理使用warp_content,match_parent.
尽可能的是使用RelativeLayout
引入android的百分比布局。
针对不同的机型,使用不同的布局文
件放在对应的目录下,android会自动匹配。
Icon类建议:

尽量使用svg转换而成xml。
切图的时候切大分辨率的图,应用到布局当中。在小分辨率的手机上也会有很好的显示效果。
使用与密度无关的像素单位dp,sp

99.• LaunchMode应用场景

standard 模式
这是默认模式,每次激活Activity时都会创建Activity实例,并放入任务栈中。使用场景:大多数Activity。

singleTop 模式
如果在任务的栈顶正好存在该Activity的实例,就重用该实例( 会调用实例的 onNewIntent() ),否则就会创建新的实例并放入栈顶,即使栈中已经存在该Activity的实例,只要不在栈顶,都会创建新的实例。使用场景如新闻类或者阅读类App的内容页面。

singleTask 模式
如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的 onNewIntent() )。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移出栈。如果栈中不存在该实例,将会创建新的实例放入栈中。使用场景如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。

singleInstance 模式
在一个新栈中创建该Activity的实例,并让多个应用共享该栈中的该Activity实例。一旦该模式的Activity实例已经存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例( 会调用实例的 onNewIntent() )。其效果相当于多个应用共享一个应用,不管谁激活该 Activity 都会进入同一个应用中。使用场景如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。

100.•请介绍下ContentProvider 是如何实现数据共享的?

ContentProvider是以Uri的形式对外提供数据,ContenrResolver是根据Uri来访问数据。
** 步骤:**

定义自己的ContentProvider类,该类需要继承Android系统提供的ContentProvider基类。

在Manifest.xml 文件中注册ContentProvider,(四大组件的使用都需要在Manifest文件中注册) 注册时需要绑定一个URL。

例如: android:authorities=“com.myit.providers.MyProvider”
说明:authorities就相当于为该ContentProvider指定URL。 注册后,其他应用程序就可以通过该Uri来访问MyProvider所暴露的数据了。
其他程序使用ContentResolver来操作。

调用Activity的ContentResolver获取ContentResolver对象
调用ContentResolver的insert(),delete(),update(),query()进行增删改查。
一般来说,ContentProvider是单例模式,也就是说,当多个应用程序通过ContentResolver来操作ContentProvider提供的数据时,ContentResolver调用的数据操作将会委托给同一个ContentResolver

101.•IntentService原理及作用是什么?

在Android开发中,我们或许会碰到这么一种业务需求,一项任务分成几个子任务,子任务按顺序先后执行,子任务全部执行完后,这项任务才算成功。那么,利用几个子线程顺序执行是可以达到这个目的的,但是每个线程必须去手动控制,而且得在一个子线程执行完后,再开启另一个子线程。或者,全部放到一个线程中让其顺序执行。这样都可以做到,但是,如果这是一个后台任务,就得放到Service里面,由于Service和Activity是同级的,所以,要执行耗时任务,就得在Service里面开子线程来执行。那么,有没有一种简单的方法来处理这个过程呢,答案就是IntentService。

    IntentService是继承于Service并处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,同时,当任务执行完后,IntentService会自动停止,而不需要我们去手动控制。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandleIntent回调方法中执行,并且,每次只会执行一个工作线程,执行完第一个再执行第二个,以此类推。

   所有请求都在一个单线程中,不会阻塞应用程序的主线程(UI Thread),同一时间只处理一个请求。

那么,用IntentService有什么好处呢?首先,我们省去了在Service中手动开线程的麻烦,第二,当操作完成时,我们不用手动停止Service,第三,it’s so easy to use!

102.• ApplicationContext和ActivityContext的区别

ApplicationContext的生命周期与Application的生命周期相关的,ApplicationContext随着Application的销毁而销毁,伴随application的一生,与activity的生命周期无关.

ActivityContext跟Activity的生命周期是相关的,但是对一个Application来说,Activity可以销毁几次,那么属于Activity的context就会销毁多次

Context一共有Application、Activity和Service三种类型,因此一个应用程序中Context数量的计算公式就可以这样写:

Context数量 = Activity数量 + Service数量 + 1

上面的1代表着Application的数量,因为一个应用程序中可以有多个Activity和多个Service,但是只能有一个Application。

和UI相关的方法基本都不建议或者不可使用Application,并且,前三个操作基本不可能在Application中出现。实际上,只要把握住一点,凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。
还有就是,在使用context的时候,为防止内存泄露,注意一下几个方面:

不要让生命周期长的对象引用activity context,即保证引用activity的对象要与activity本身生命周期是一样的。
对于生命周期长的对象,可以使用application context。
避免非静态的内部类,尽量使用静态类,避免生命周期问题,注意内部类对外部对象引用导致的生命周期变化。

103.• SP是进程同步的吗?有什么方法做到同步?

  1. SharedPreferences不支持进程同步

一个进程的情况,经常采用SharePreference来做,但是SharePreference不支持多进程,它基於单个文件的,默认是没有考虑同步互斥,而且,APP对SP对象做了缓存,不好互斥同步.
MODE_MULTI_PROCESS的作用是什么?

在getSharedPreferences的时候, 会强制让SP进行一次读取操作,从而保证数据是最新的. 但是若频繁多进程进行读写 . 若某个进程持有了一个外部sp对象, 那么不能保证数据是最新的. 因为刚刚被别的进程更新了.

2.考虑用ContentProvider来实现SharedPreferences的进程同步.ContentProvider基于Binder,不存在进程间互斥问题,对于同步,也做了很好的封装,不需要开发者额外实现。
另外ContentProvider的每次操作都会重新getSP. 保证了sp的一致性.

104• 谈谈多线程在Android中的使用

.Android提供了四种常用的操作多线程的方式,分别是:
Handler+Thread
AsyncTask
ThreadPoolExecutor
IntentService

105.• AndroidManifest的作用与理解

标签层:
1.package=“应用包名”
整个应用的包名。这里有个坑,当我们通过ComponentName来启动某个Activity时,所用的包名一定是这个应用的包名,而不是当前Activity的包名。

2.xmlns:android=“http://schemas.android.com/apk/res/android”
命名空间的声明,使得各种Android系统级的属性能让我们使用。当我们需要使用自定义属性时,可以将其修改为res-auto,编译时会为我们自动去找到该自定义属性。

3.android:sharedUserId=“android.uid.system”
将当前应用进程设置为系统级进程。

4.uses-permission
为我们的应用添加必须的权限。同时我们也可以该层声明自定义的权限。

标签层:
应用层标签,用来配置我们的apk的整体属性,也可以统一指定所有界面的主题。

1.“android:name”、“android:icon”、“android:label”
顾名思义,用来指定应用的名称、在桌面的启动图标、应用的标签名

2.“android:theme”
为当前应用的每个界面都默认设置一个主题,可以后续在activity标签层单独覆盖此Theme。

3.“android:allowBackup”
关闭应用程序数据的备份和恢复功能,注意该属性值默认为true,如果你不需要你的应用被恢复导致隐私数据暴露,必须手动设置此属性。

4.android:hardwareAccelerated=“true”
开启硬件加速,一般应用不推介使用。就算非要使用也最好在某个Activity单独开启,避免过大的内存开销。

5.android:taskAffinity
设置Activity任务栈的名称,可忽略。

<具体组件/>标签层:
因为、在实际开发中接触得不多,这部分主要讲解 、标签。

关于Activity标签的属性,个人最觉得绕和难掌握的就是Intent-filter的匹配规则了,每次使用错了都要去查资料修改,所以这边总结得尽可能仔细。

1.android:configChanges 当我们的界面大小,方向,字体等config参数改变时,我们的Activity就会重新执行onCreate的生命周期。而当我们设置此属性后,就可以强制让Activity不重新启动,而是只会调用一次onConfigurationChanged方法,所以我们可以在这里做一些相关参数改变的操作。

2.“android.intent.category.LAUNCHER”、“android.intent.action.MAIN”
这两个属性共同将当前Activity声明为了我们应用的入口,将应用注册至系统的应用列表中,缺一不可。

注:这里还有一点需要注意,如果希望我们的应用有多个入口,每个入口能进入到app的不同Activity中时,光设置这两个属性还不够,还要为它指定一个进程和启动模式。

android:process=".otherProcess"

android:launchMode =“singleInstance”

1.android:exported=“true”
将当前组件暴露给外部。属性决定它是否可以被另一个Application的组件启动。

当我们通过intent去隐式调用一个Activity时,需要同时匹配注册activity中的action、category、data才能正常启动,而这三个属性的匹配规则也略有不同。

2.action
action是最简单的匹配项,我们将其理解为一个区分大小写的字符串即可。

一般用来代表某一种特定的动作,隐式调用时intent必须setAction。一个过滤器中可以有多个action属性,只要我们的itent和其中任意一项equal则就算匹配成功。

3.category
category属性也是一个字符串,匹配时也必须和过滤器中定义的值相同。

当我们没有为intent设置addCategory时,系统为帮我们默认添加一个值为"android.intent.category.DEFAULT"的category。

反过来说,如果我们需要我们自己写的Activity能接受隐式intent启动,我们就必须在它的过滤器中添加"android.intent.category.DEFAULT",否则无法成功启动。

3.data
data比较复杂,幸运地是我们几乎用不到它。

额外扩展一些关于activity的属性:

标签:
标签是提供组件额外的数据用的,它本身是一个键值对,写在清单文件中之后,可以在代码中获取。

android:excludeFromRecents=“true”
设置为true后,当用户按了“最近任务列表”时候,该activity不会出现在最近任务列表中,可达到隐藏应用的目的。

关于receiver,广播接收器,也可以给他设置接收权限。一个permission问题:
<receiver

    android:name="com.android.settings.AliAgeModeReceiver"

    android:permission="com.android.settings.permission.SWITH_SETTING">

    <intent-filter>

        <action android:name="com.android.settings.action.SWITH_AGED_MODE"/>

    </intent-filter>

可在receiver标签中添加权限, 发广播时可以设置相应权限的应用接收

常见的一些原理性问题

106.• Handler机制和底层实现

在这里插入图片描述
上面一共出现了几种类,ActivityThread,Handler,MessageQueue,Looper,msg(Message),对这些类作简要介绍:
ActivityThread:程序的启动入口,该类就是我们说的主线程,它对Looper进行操作的。
Handler:字面意思是操控者,该类有比较重要的地方,就是通过handler来发送消息(sendMessage)到MessageQueue和 操作控件的更新(handleMessage)。handler下面持有这MessageQueue和Looper的对象。
MessageQueue:字面意思是消息队列,就是封装Message类。对Message进行插入和取出操作。
Message:这个类是封装消息体并被发送到MessageQueue中的,给类是通过链表实现的,其好处方便MessageQueue的插入和取出操作。还有一些字段是(int what,Object obj,int arg1,int arg2)。what是用户定义的消息和代码,以便接收者(handler)知道这个是关于什么的。obj是用来传输任意对象的,arg1和arg2是用来传递一些简单的整数类型的。
先获取looper,如果没有就创建

创建过程:
ActivityThread 执行looperMainPrepare(),该方法先实例化MessageQueue对象,然后实例化Looper对象,封装mQueue和主线程,把自己放入ThreadLocal中
再执行loop()方法,里面会重复死循环执行读取MessageQueue。
(接着ActivityThread 执行Looper对象中的loop()方法)
此时调用sendMessage()方法,往MessageQueue中添加数据,其取出消息队列中的handler,执行dispatchMessage(),进而执行handleMessage(),Message的数据结构是基于链表的

107.• HandlerThread

HandlerThread本质上就是一个普通Thread,只不过内部建立了Looper.
HandlerThread的特点
HandlerThread将loop转到子线程中处理,说白了就是将分担MainLooper的工作量,降低了主线程的压力,使主界面更流畅。
开启一个线程起到多个线程的作用。处理任务是串行执行,按消息发送顺序进行处理。HandlerThread本质是一个线程,在线程内部,代码是串行处理的。
但是由于每一个任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。
HandlerThread拥有自己的消息队列,它不会干扰或阻塞UI线程。
对于网络IO操作,HandlerThread并不适合,因为它只有一个线程,还得排队一个一个等着

108.• 关于Handler,在任何地方new Handler 都是什么线程下?

不传递 Looper 创建 Handler:Handler handler = new Handler();上文就是 Handler 无参创建的源码,可以看到是通过 Looper.myLooper() 来获取 Looper 对象,也就是说对于不传递 Looper 对象的情况下,在哪个线程创建 Handler 默认获取的就是该线程的 Looper 对象,那么 Handler 的一系列操作都是在该线程进行的。
传递 Looper 对象创建 Handler:Handler handler = new Handler(looper);那么看看传入 Looper 的构造函数:

public Handler(Looper looper) {
    this(looper, null, false);
}
public Handler(Looper looper, Callback callback) {
    this(looper, callback, false);
}
// 第一个参数是 looper 对象,第二个 callback 对象,第三个消息处理方式(是否异步)
public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

可以看出来传递 Looper 对象 Handler 就直接使用了。所以对于传递 Looper 对象创建 Handler 的情况下,传递的 Looper 是哪个线程的,Handler 绑定的就是该线程。

109• ThreadLocal原理,实现及如何保证Local属性?

当需要使用多线程时,有个变量恰巧不需要共享,此时就不必使用synchronized这么麻烦的关键字来锁住,每个线程都相当于在堆内存中开辟一个空间,线程中带有对共享变量的缓冲区,通过缓冲区将堆内存中的共享变量进行读取和操作,ThreadLocal相当于线程内的内存,一个局部变量。每次可以对线程自身的数据读取和操作,并不需要通过缓冲区与 主内存中的变量进行交互。并不会像synchronized那样修改主内存的数据,再将主内存的数据复制到线程内的工作内存。ThreadLocal可以让线程独占资源,存储于线程内部,避免线程堵塞造成CPU吞吐下降。

在每个Thread中包含一个ThreadLocalMap,ThreadLocalMap的key是ThreadLocal的对象,value是独享数据。

110.• 请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系

简单的说,Handler获取当前线程中的looper对象,looper用来从存放Message的MessageQueue中取出Message,再有Handler进行Message的分发和处理.

Message Queue(消息队列):用来存放通过Handler发布的消息,通常附属于某一个创建它的线程,可以通过Looper.myQueue()得到当前线程的消息队列

Handler:可以发布或者处理一个消息或者操作一个Runnable,通过Handler发布消息,消息将只会发送到与它关联的消息队列,然也只能处理该消息队列中的消息

Looper:是Handler和消息队列之间通讯桥梁,程序组件首先通过Handler把消息传递给Looper,Looper把消息放入队列。Looper也把消息队列里的消息广播给所有的

Handler:Handler接受到消息后调用handleMessage进行处理

Message:消息的类型,在Handler类中的handleMessage方法中得到单个的消息进行处理

在单线程模型下,为了线程通信问题,Android设计了一个Message Queue(消息队列), 线程间可以通过该Message Queue并结合Handler和Looper组件进行信息交换。下面将对它们进行分别介绍:

  1. Message

    Message消息,理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。

  2. Handler

    Handler处理者,是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的 Handler对象引用来sendMessage(Message)。而使用Handler,需要implement 该类的 handleMessage(Message)方法,它是处理这些Message的操作内容,例如Update UI。通常需要子类化Handler来实现handleMessage方法。

  3. Message Queue

    Message Queue消息队列,用来存放通过Handler发布的消息,按照先进先出执行。

    每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。

  4. Looper

    Looper是每条线程里的Message Queue的管家。Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL。对于子线程使用Looper,API Doc提供了正确的使用方法:这个Message机制的大概流程:

    1. 在Looper.loop()方法运行开始后,循环地按照接收顺序取出Message Queue里面的非NULL的Message。

    2. 一开始Message Queue里面的Message都是NULL的。当Handler.sendMessage(Message)到Message Queue,该函数里面设置了那个Message对象的target属性是当前的Handler对象。随后Looper取出了那个Message,则调用 该Message的target指向的Hander的dispatchMessage函数对Message进行处理。在dispatchMessage方法里,如何处理Message则由用户指定,三个判断,优先级从高到低:

    1. Message里面的Callback,一个实现了Runnable接口的对象,其中run函数做处理工作;

    2. Handler里面的mCallback指向的一个实现了Callback接口的对象,由其handleMessage进行处理;

    3. 处理消息Handler对象对应的类继承并实现了其中handleMessage函数,通过这个实现的handleMessage函数处理消息。

    由此可见,我们实现的handleMessage方法是优先级最低的!

    1. Handler处理完该Message (update UI) 后,Looper则设置该Message为NULL,以便回收!

    在网上有很多文章讲述主线程和其他子线程如何交互,传送信息,最终谁来执行处理信息之类的,个人理解是最简单的方法——判断Handler对象里面的Looper对象是属于哪条线程的,则由该线程来执行!

    1. 当Handler对象的构造函数的参数为空,则为当前所在线程的Looper;
  5. Looper.getMainLooper()得到的是主线程的Looper对象,Looper.myLooper()得到的是当前线程的Looper对象。

111.• 请描述一下View事件传递分发机制

事件分发原理: 责任链模式,事件层层传递,直到被消费。
View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。
View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

112.• View刷新机制

在Android的View刷新机制中,父View负责刷新(invalidateChild)、布局(layoutChild)显示子View。而当子View需要刷新时,则是通知父View刷新子view来完成。

invalidate()和postInvalidate() 的区别及使用
当Invalidate()被调用的时候,View的OnDraw()就会被调用;Invalidate()是刷新UI,UI更新必须在主线程,所以invalidate必须在UI线程中被调用,如果在子线程中更新视图的就调用postInvalidate()。

postInvalidate()实际调用的方法,mHandler.sendMessageDelayed,在子线程中用handler发送消息,所以才能在子线程中使用。

113• View绘制流程

View的绘制流程:OnMeasure()——>OnLayout()——>OnDraw()
第一步:OnMeasure():测量视图大小。从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。
第二步:OnLayout():确定View位置,进行页面布局。从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。
第三步:OnDraw():绘制视图。ViewRoot创建一个Canvas对象,然后调用OnDraw()。六个步骤:、绘制视图的背景;、保存画布的图层(Layer);、绘制View的内容;、绘制View子视图,如果没有就不用;

114.• 为什么不能在子线程更新UI?

表象
我们先从表象上分析一下,假设可以在子线程更新UI,会产生那些后果呢?
如果不同的线程控制同一块UI,因为时间的延时性,网络的延迟性,很有可能界面图像会乱套,会花掉。而且出了问题也非常不容易排查问题出在了哪里。从硬件上考虑,每个手机只有一个显示芯片,根本上不可能同时处理多个绘制请求),减少更新线程数,其实是提高了更新效率。

本质
如果可以并发的更新UI,事实上是 “is not thread safe”的,也就是线程不安全。我们都知道,线程安全问题其实就是,不同的线程对同一块资源的调用。在更新UI的同时,会涉及context资源的调用,所以产生了线程安全问题。

115.ANR产生的原因是什么?

1.只有主线程才会产生ANR,主线程就是UI线程;

2.必须发生某些输入事件或特定操作,比如按键或触屏等输入事件,在BroadcastReceiver或Service的各个生命周期调用函数;

3.上述事件响应超时,不同的context规定的上限时间不同

a.主线程对输入事件5秒内没有处理完毕

b.主线程在执行BroadcastReceiver的onReceive()函数时10秒内没有处理完毕

c.主线程在Service的各个生命周期函数时20秒内没有处理完毕。

那么导致ANR的根本原因是什么呢?简单的总结有以下两点:

1.主线程执行了耗时操作,比如数据库操作或网络编程

2.其他进程(就是其他程序)占用CPU导致本进程得不到CPU时间片,比如其他进程的频繁读写操作可能会导致这个问题。

细分的话,导致ANR的原因有如下几点:

1.耗时的网络访问

2.大量的数据读写

3.数据库操作

4.硬件操作(比如camera)

5.调用thread的join()方法、sleep()方法、wait()方法或者等待线程锁的时候

6.service binder的数量达到上限

7.system server中发生WatchDog ANR

8.service忙导致超时无响应

9.其他线程持有锁,导致主线程等待超时

10.其它线程终止或崩溃导致主线程一直等待

那么如何避免ANR的发生呢或者说ANR的解决办法是什么呢?

1.避免在主线程执行耗时操作,所有耗时操作应新开一个子线程完成,然后再在主线程更新UI。

2.BroadcastReceiver要执行耗时操作时应启动一个service,将耗时操作交给service来完成。

3.避免在Intent Receiver里启动一个Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。如果你的应用程序在响应Intent广 播时需要向用户展示什么,你应该使用Notification Manager来实现。

116.• Oom 是否可以try catch?

只有在一种情况下,这样做是可行的:

在try语句中声明了很大的对象,导致OOM,并且可以确认OOM是由try语句中的对象声明导致的,那么在catch语句中,可以释放掉这些对象,解决OOM的问题,继续执行剩余语句。

但是这通常不是合适的做法。

Java中管理内存除了显式地catch OOM之外还有更多有效的方法:比如SoftReference, WeakReference, 硬盘缓存等。
在JVM用光内存之前,会多次触发GC,这些GC会降低程序运行的效率。
如果OOM的原因不是try语句中的对象(比如内存泄漏),那么在catch语句中会继续抛出OOM

什么情况导致OOM?
OOM常见情况:

1)Acitivity没有对栈进行管理,如果开启过多,就容易造成内存溢出
2)加载大的图片或者同时数量过多的图片的时候
3)程序存在内存泄漏问题,导致系统可用内存越来越小。
4)递归次数过多,也会导致内存溢出.
5)频繁的内存抖动,也会造成OOM异常的发生,大量小的对象被频繁的创建,导致内存碎片,从而当需要分配内存的时候,虽然总体上还有内存分配,但是由于这些内存不是连续的,导致无法分配,系统就直接返回OOM了

有什么解决方法可以避免OOM?
1)减小对象的内存占用,避免OOM的第一步就是要尽量减少新分配出来的对象占用内存的大小,尽量使用更加轻量的对象。
2)内存对象的重复利用,大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码时显式地在程序里创建对象池,然后处理好复用的实现逻辑。要么就是利用系统框架既有的某些复用特性,减少对象的重复创建,从而降低内存的分配与回收。
3)避免对象的内存泄露,内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分 配内存的时候,空闲空间不足而出现OOM。
4)内存使用策略优化。

117.• 内存泄漏?

什么是内存泄漏?
内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗。内存泄漏并不是指物理上的内存消失,这里的内存泄漏是值由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费。

怎样会导致内存泄漏?
资源对象没关闭造成的内存泄漏,如查询数据库后没有关闭游标cursor
构造Adapter时,没有使用 convertView 重用
Bitmap对象不在使用时调用recycle()释放内存
对象被生命周期长的对象引用,如activity被静态集合引用导致activity不能释放

内存泄漏有什么危害?
内存泄漏对于app没有直接的危害,即使app有发生内存泄漏的情况,也不一定会引起app崩溃,但是会增加app内存的占用。内存得不到释放,慢慢的会造成app内存溢出。所以我们解决内存泄漏的目的就是防止app发生内存溢出。

118• LruCache默认缓存大小

设置LruCache缓存的大小,一般为当前进程可用容量的1/8。

一、Android中的缓存策略
一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比较好理解,那么为什么还要删除缓存呢?这是因为不管是内存缓存还是硬盘缓存,它们的缓存大小都是有限的。当缓存满了之后,再想其添加缓存,这个时候就需要删除一些旧的缓存并添加新的缓存。

因此LRU(Least Recently Used)缓存算法便应运而生,LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。

二、LruCache的使用
LruCache是Android 3.1所提供的一个缓存类,所以在Android中可以直接使用LruCache实现内存缓存。而DisLruCache目前在Android 还不是Android SDK的一部分,但Android官方文档推荐使用该算法来实现硬盘缓存。

1.LruCache的介绍
LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。

2.LruCache的使用
LruCache的使用非常简单,我们就已图片缓存为例。

int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024);
int cacheSize = maxMemory/8;
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};
①设置LruCache缓存的大小,一般为当前进程可用容量的1/8。
②重写sizeOf方法,计算出要缓存的每张图片的大小。

注意:缓存的总容量和每个缓存对象的大小所用单位要一致。

三、LruCache的实现原理
LruCache的核心思想很好理解,就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。而最近访问的对象将放在队头,最后被淘汰。

119• ContentProvider的权限管理

ContentProvider可以理解为一个Android应用对外开放的接口,只要是符合它所定义的Uri格式的请求,均可以正常访问执行操作。其他的Android应用可以使用ContentResolver对象通过与ContentProvider同名的方法请求执行,被执行的就是ContentProvider中的同名方法。所以ContentProvider很多对外可以访问的方法,在ContentResolver中均有同名的方法,是一一对应的
访问权限

对于ContentProvider暴露出来的数据,应该是存储在自己应用内存中的数据,对于一些存储在外部存储器上的数据,并不能限制访问权限,使用ContentProvider就没有意义了。对于ContentProvider而言,有很多权限控制,可以在AndroidManifest.xml文件中对节点的属性进行配置,一般使用如下一些属性设置:

android:grantUriPermssions:临时许可标志。
android:permission:Provider读写权限。
android:readPermission:Provider的读权限。
android:writePermission:Provider的写权限。
android:enabled:标记允许系统启动Provider。
android:exported:标记允许其他应用程序使用这个Provider。
android:multiProcess:标记允许系统启动Provider相同的进程中调用客户端。

120• 如何通过广播拦截和abort一条短信

BroadcastReceiver还可以监听系统进程,比如android的收短信,电量低,电量改变,系统启动,等……只要BroadcastReceiver监听了这些进程,就可以实现很多有趣的功能,比如,接收短信的这条广播是一条有序广播,所以我们可以监听这条信号,在传递给真正的接收程序时,我们将自定义的广播接收程序的优先级大于它,并且取消广播的传播,这样就可以实现拦截短信的功能了。

public class SmsReceiver extends BroadcastReceiver {
        // 当接收到短信时被触发
        @Override
        public void onReceive(Context context, Intent intent) {
            // 如果是接收到短信
            if (intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) {
                // 取消广播(这行代码将会让系统收不到短信)
                abortBroadcast();
                StringBuilder sb = new StringBuilder();
                // 接收由SMS传过来的数据
                Bundle bundle = intent.getExtras();
                // 判断是否有数据
                if (bundle != null) {
                    // 通过pdus可以获得接收到的所有短信消息
                    Object[] pdus = (Object[]) bundle.get("pdus");
                    // 构建短信对象array,并依据收到的对象长度来创建array的大小
                    SmsMessage[] messages = new SmsMessage[pdus.length];
                    for (int i = 0; i < pdus.length; i++) {
                        messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
                    }
                    // 将送来的短信合并自定义信息于StringBuilder当中
                    for (SmsMessage message : messages) {
                        sb.append("短信来源:");
                        // 获得接收短信的电话号码
                        sb.append(message.getDisplayOriginatingAddress());
                        sb.append("\n------短信内容------\n");
                        // 获得短信的内容
                        sb.append(message.getDisplayMessageBody());
                    }
                }
                Toast.makeText(context, sb.toString(), 5000).show();
            }
        }
    }

121.广播是否可以请求网络?

可以,不过需要开启线程操作

122.• 广播引起anr的时间限制是多少

当 onReceive() 方法在 10 秒内没有执行完毕, Android 会认为该程序无响应

123.• Android为什么引入Parcelable?

Parcelable是Android提供一套序列化机制,它将序列化后的字节流写入到一个共性内存中,其他对象可以从这块共享内存中读出字节流,并反序列化成对象。因此效率比较高,适合在对象间或者进程间传递信息。

124.• ListView 中图片错位的问题是如何产生的?

图片错位原理:
如果我们只是简单显示list中数据,而没用convertview的复用机制和异步操作,就不会产生图片错位;重用convertview但没用异步,也不会有错位现象。但我们的项目中list一般都会用,不然会很卡。
在上图中,我们能看到listview中整屏刚好显示7个item,当向下滑动时,显示出item8,而item8是重用的item1,如果此时异步网络请求item8的图片,比item1的图片慢,那么item8就会显示item1的image。当item8下载完成,此时用户向上滑显示item1时,又复用了item8的image,这样就导致了图片错位现象(item1和item8是用的同一块内存哦)。

解决方法:
对imageview设置tag,并预设一张图片。
向下滑动后,item8显示,item1隐藏。但由于item1是第一次进来就显示,所以一般情况下,item1都会比item8先下载完,但由于此时可见的item8的tag,和隐藏了的item1的url不匹配,所以就算item1的图片下载完也不会显示到item8中,因为tag标识的永远是可见图片中的url。

关键代码:

// 给 ImageView 设置一个 tag
holder.img.setTag(imgUrl);
// 预设一个图片
holder.img.setImageResource(R.drawable.ic_launcher);

// 通过 tag 来防止图片错位
if (imageView.getTag() != null && imageView.getTag().equals(imageUrl)) {
imageView.setImageBitmap(result);
}

125.• 屏幕适配的处理技巧都有哪些?

在这里插入图片描述

126.• Recycleview和ListView的区别

一、 缓存机制对比

  1. 层级不同:

RecyclerView比ListView多两级缓存,支持多个离ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。
具体来说:
ListView(两级缓存):
在这里插入图片描述
RecyclerView(四级缓存):
在这里插入图片描述
ListView和RecyclerView缓存机制基本一致:

1). mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;

2). mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.

3). RecyclerView的优势在于a.mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;b.mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势.客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。

  1. 缓存不同:

1). RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:

View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);

RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView:

2). ListView缓存View。而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)。

二、 局部刷新

RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView。ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是"一锅端",将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

ListView获取缓存的流程:

RecyclerView获取缓存的流程:

结合RecyclerView的缓存机制,看看局部刷新是如何实现的:

以RecyclerView中notifyItemRemoved(1)为例,最终会调用requestLayout(),使整个RecyclerView重新绘制,过程为:

onMeasure()–>onLayout()–>onDraw()

其中,onLayout()为重点,分为三步:

dispathLayoutStep1():记录RecyclerView刷新前列表项ItemView的各种信息,如Top,Left,Bottom,Right,用于动画的相关计算;

dispathLayoutStep2():真正测量布局大小,位置,核心函数为layoutChildren();

dispathLayoutStep3():计算布局前后各个ItemView的状态,如Remove,Add,Move,Update等,如有必要执行相应的动画.

其中,layoutChildren()流程图:
在这里插入图片描述

在这里插入图片描述

当调用notifyItemRemoved时,会对屏幕内ItemView做预处理,修改ItemView相应的pos以及flag(流程图中红色部分):
在这里插入图片描述

当调用fill()中RecyclerView.getViewForPosition(pos)时,RecyclerView通过对pos和flag的预处理,使得bindview只调用一次.

127.• 动态权限适配方案,权限组的概念

在这里插入图片描述
具体方法:

使用Context.checkSelfPermission()接口先检查权限是否授权。
使用Activity.shouldShowRequestPermissionRationale()接口检查用户是否勾选不再提醒。
第2步返回为true时,表示用户并未勾选不再提醒选项,使用Activity.requestPermissions()接口向系统请求权限。
第2步返回为false时,表示用户已勾选不再提醒选项,则应用该弹框提示用户。
第3步执行后,不论用户是否授予权限,都会回调Activity.onRequestPermissionsResult()的函数。在Activity中重载onRequestPermissionsResult()函数,在接收授权结果,根据不同的授权结果做相应的处理。

动态权限申请的适配方案:

对于低于M版本(安卓6.0)的Android系统,没有动态权限的申请问题,动态权限的申请流程对于低于M版本的Android系统也不再适用。所以适配方案,首先要考虑低于M版本的Android系统,因此对于Android版本小于M版本时,在检查权限时,直接返回true就OK,直接屏蔽后续流程。

对于M版本(安卓6.0)的Android系统,动态权限的申请流程会产生好几个分支处理逻辑,这样不善于管理和维护。所以对于此处,为了将写得代码更加整洁和更易于维护,我们可以将动态权限申请进行一次封装,

新建一个空白的activity用户权限申请和回调,然后在activity外包装一层管理内,限制一个调用的入口和出口,对于外部暴露唯一的入口和出口,这样对于外部逻辑代码需要调用权限时,将变得异常简单,并且由于将权限申请封装在了管理类中,
对于低于M版本的Android系统也将没有任何引用,在管理类中直接对于低于M版本的权限申请请求直接回调全部已授权即可。

实际App中动态权限申请代码如下:

由于权限的动态申请与管理应该是伴随着整个App的生命周期的,所以PermissionsManager设计为单例的,且初始化保存着applicationContext作为默认跳转方式。
在App代码中,应在自定义的Application中调用PermissionsManager中的initContext()方法。
在App代码中,在需要申请权限的地方调用PermissionsManager中的newRequestPermissions()方法即可,不论Android版本是低于M版本还是高于M版本,根据实际App是否拥有对应权限,回调传入的该次权限申请的PermissionsResultsCallback接口的onRequestPermissionsResult()方法。这样就将动态权限的申请结果处理流程给唯一化,将权限的申请的入口和出口都唯一化。
这样将权限申请的细节和Android系统版本适配的细节都封装在了PermissionsManager内部,对于应用外部只需要知道申请权限的入口以及申请权限接口的出口即可。

128.下拉状态栏是不是影响activity的生命周期

Android下拉通知栏不会影响Activity的生命周期方法

129.• Android中开启摄像头的主要步骤

获得摄像头管理器CameraManager mCameraManager,mCameraManager.openCamera()来打开摄像头
指定要打开的摄像头,并创建openCamera()所需要的CameraDevice.StateCallback stateCallback
在CameraDevice.StateCallback stateCallback中调用takePreview(),这个方法中,使用CaptureRequest.Builder创建预览需要的CameraRequest,并初始化了CameraCaptureSession,最后调用了setRepeatingRequest(previewRequest, null, childHandler)进行了预览
点击屏幕,调用takePicture(),这个方法内,最终调用了capture(mCaptureRequest, null, childHandler)
在new ImageReader.OnImageAvailableListener(){}回调方法中,将拍照拿到的图片进行展示

130.• 微信主页面的实现方式

viewpager + fragment

131.• 微信上消息小红点的原理

自定义view画小红点和数字

132.• 图片库对比

https://www.jianshu.com/p/44a4ee648ab4

133.• 图片框架缓存实现

内存缓存
Glide内存缓存的实现自然也是使用的LruCache算法。不过除了LruCache算法之外,Glide还结合了一种弱引用的机制,共同完成了内存缓存功能。

Glide的图片加载过程中会调用两个方法来获取内存缓存,loadFromCache()和loadFromActiveResources()。这两个方法中一个使用的就是LruCache算法,另一个使用的就是弱引用。

当acquired变量大于0的时候,说明图片正在使用中,也就应该放到activeResources弱引用缓存当中。而经过release()之后, 如果acquired变量等于0了,说明图片已经不再被使用了,那么此时会调用listener的onResourceReleased()方法来释放资源

当图片不再使用时,首先会将缓存图片从activeResources中移除,然后再将它put到LruResourceCache当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存的功能。

硬盘缓存
Glide是使用的自己编写的DiskLruCache工具类,但是基本的实现原理都是差不多的。

一种是调用decodeFromCache()方法从硬盘缓存当中读取图片,一种是调用decodeFromSource()来读取原始图片。默认情况下Glide会优先从缓存当中读取,只有缓存中不存在要读取的图片时,才会去读取原始图片。

调用transform()方法来对图片进行转换,然后在writeTransformedToCache()方法中将转换过后的图片写入到硬盘缓存中,调用的同样是DiskLruCache实例的put()方法,不过这里用的缓存Key是resultKey(也就是调整图片大小后进行缓存)。

134.• LRUCache原理

缓存保存了一个强引用(Android 2.3开始,垃圾回收器更倾向于回收弱引用和软引用,软引用和弱引用变得不可靠,Android 3.0中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放)限制值的数量. 每当值被访问的时候,它会被移动到队列的头部. 当缓存已满的时候加入新的值时,队列中最后的值会出队,可能被回收

LRUCache内部维护主要是通过LinkedHashMap实现

这是一个安全的线程,多线程缓存通过同步实现

135.• 自己去实现图片库,怎么做

Glide原理
应该具备以下功能:

图片的同步加载
图片的异步加载
图片压缩
内存缓存
磁盘缓存
网络拉取

136.• Glide源码解析

https://blog.csdn.net/rjgcszlc/article/details/81843072

137.• Glide使用什么缓存?

Glide缓存非常先进,很灵活,很全面,总体上来讲有内存缓存和磁盘文件缓存。缓冲机制概括来讲就是读缓存以及是写入缓存的机制。而Glide读缓存时机就是先内存缓存查找再到磁盘缓存查找最后网络,写入缓存则就是在获取到原始source图片之后,先写入磁盘缓存,再加入内存缓存。

138.网络框架对比

网络请求框架总结

1.xutils

此框架庞大而周全,这个框架可以网络请求,同时可以图片加载,又可以数据存储,又可以 View 注解,使用这种框架很方便,这样会使得你整个项目对它依赖性太强,万一以后这个库不维护了,或者中间某个模块出问题了,这个影响非常大,所以在项目开发时,一般会更喜欢选择专注某一领域的框架。

2.OkHttp

Android 开发中是可以直接使用现成的api进行网络请求的,就是使用HttpClient、HttpUrlConnection 进行操作,目前HttpClient 已经被废弃,而 android-async-http 是基于HttpClient的,可能也是因为这个原因作者放弃维护。 而OkHttp是Square公司开源的针对Java和Android程序,封装的一个高性能http请求库,它的职责跟HttpUrlConnection 是一样的,支持 spdy、http 2.0、websocket ,支持同步、异步,而且 OkHttp 又封装了线程池,封装了数据转换,封装了参数使用、错误处理等,api使用起来更加方便。可以把它理解成是一个封装之后的类似HttpUrlConnection的东西,但是在使用的时候仍然需要自己再做一层封装,这样才能像使用一个框架一样更加顺手。

3.Volley

Volley是Google官方出的一套小而巧的异步请求库,该框架封装的扩展性很强,支持HttpClient、HttpUrlConnection, 甚至支持OkHttp,而且Volley里面也封装了ImageLoader,所以如果你愿意你甚至不需要使用图片加载框架,不过这块功能没有一些专门的图片加载框架强大,对于简单的需求可以使用,稍复杂点的需求还是需要用到专门的图片加载框架。Volley也有缺陷,比如不支持post大数据,所以不适合上传文件。不过Volley设计的初衷本身也就是为频繁的、数据量小的网络请求而生。

4.Retrofit

Retrofit是Square公司出品的默认基于OkHttp封装的一套RESTful网络请求框架,RESTful是目前流行的一套api设计的风格, 并不是标准。Retrofit的封装可以说是很强大,里面涉及到一堆的设计模式,可以通过注解直接配置请求,可以使用不同的http客户端,虽然默认是用http ,可以使用不同Json Converter 来序列化数据,同时提供对RxJava的支持,使用Retrofit + OkHttp + RxJava + Dagger2 可以说是目前比较潮的一套框架,但是需要有比较高的门槛。

5.Volley VS OkHttp

Volley的优势在于封装的更好,而使用OkHttp你需要有足够的能力再进行一次封装。而OkHttp的优势在于性能更高,因为 OkHttp基于NIO和Okio ,所以性能上要比 Volley更快。IO 和 NIO这两个都是Java中的概念,如果我从硬盘读取数据,第一种方式就是程序一直等,数据读完后才能继续操作这种是最简单的也叫阻塞式IO,还有一种是你读你的,程序接着往下执行,等数据处理完你再来通知我,然后再处理回调。而第二种就是 NIO 的方式,非阻塞式, 所以NIO当然要比IO的性能要好了,而 Okio是 Square 公司基于IO和NIO基础上做的一个更简单、高效处理数据流的一个库。理论上如果Volley和OkHttp对比的话,更倾向于使用 Volley,因为Volley内部同样支持使用OkHttp,这点OkHttp的性能优势就没了, 而且 Volley 本身封装的也更易用,扩展性更好些。

6.OkHttp VS Retrofit

毫无疑问,Retrofit 默认是基于 OkHttp 而做的封装,这点来说没有可比性,肯定首选 Retrofit。

7.Volley VS Retrofit

这两个库都做了不错的封装,但Retrofit解耦的更彻底,尤其Retrofit2.0出来,Jake对之前1.0设计不合理的地方做了大量重构, 职责更细分,而且Retrofit默认使用OkHttp,性能上也要比Volley占优势,再有如果你的项目如果采用了RxJava ,那更该使用 Retrofit 。所以这两个库相比,Retrofit更有优势,在能掌握两个框架的前提下该优先使用 Retrofit。但是Retrofit门槛要比Volley稍高些, 要理解他的原理,各种用法,想彻底搞明白还是需要花些功夫的,如果你对它一知半解,那还是建议在商业项目使用Volley吧。

8.总结

综上,如果以上三种网络库你都能熟练掌握,那么优先推荐使用Retrofit,前提是最好你们的后台api也能遵循RESTful的风格, 其次如果不想使用或者没能力掌握Retrofit ,那么推荐使用Volley ,毕竟Volley不需要做过多的封装,如果需要上传大数据, 那么不建议使用 Volley,该采用 OkHttp 。

139.HTTP与HTTPS的区别以及如何实现安全性

1.Url开头:HTTP 的 URL 以 HTTP:// 开头,而 HTTPS 的 URL 以 HTTPs:// 开头;

2.安全性:HTTP 是不安全的,而 HTTPS 是安全的。HTTP协议运行在TCP之上,所有传输的内容都是明文,HTTPS运行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有传输的内容都经过加密的。
3.传输效率:传输效率上 HTTP 要高于 HTTPS ,因为 HTTPS 需要经过加密过程,过程相比于 HTTP 要繁琐一点,效率上低一些也很正常;

4.费用:HTTP 无需证书,而 HTTPS 必需要认证证书;相比于 HTTP 不需要证书来说,使用 HTTPS 需要证书,申请证书是要费用的,HTTPS 这笔费用是无法避免的

5.端口:HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

6.防劫持性:HTTPS可以有效的防止运营商劫持,解决了防劫持的一个大问题。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章