2019年为android开发准备的面试题(含答案)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/smileiam/article/details/86667862

2018年算是互联网的寒冬,一大波公司宣扬裁员,也确实裁掉一部分,有的拿到了高额的补偿,可以慢慢拿着工资、喝着小酒、找着工作,甚至找个一年半载也不是问题。虽然自己公司还没扬言裁员,且自己还茫目自信,再怎么裁也不会裁到我这等优秀员工身上啊。但寒冬归寒冬,饭还是要吃、酒还是要喝,做技术的不管需不需要面试,也要时不时抽空更新下自己,让自己涨涨知识,一些基本知识点重新拾起,也为自己不久的将来面试做准备吧。

2019,加油〜〜

下面的题目全是从别人那copy过来的,答案全部是自己总结的,希望自己能温故而知新〜〜

(一) java基础面试知识点

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

1)==若是基本数据类型比较,是比较值,若是引用类型,则比较的是他们在内存中的存放地址。对象是存放在堆中,栈中存放的对象的引用,所以==是对栈中的值进行比较,若返回true代表变量的内存地址相等;

2)equals是Object类中的方法,Object类的equals方法用于判断对象的内存地址引用是不是同一个地址(是不是同一个对象)。若是类中覆盖了equals方法,就要根据具体代码来确定,一般覆盖后都是通过对象的内容是否相等来判断对象是否相等。

3)hashCode()计算出对象实例的哈希码,在对象进行散列时作为key存入。之所以有hashCode方法,因为在批量的对象比较中,hashCode比较要比equals快。在添加新元素时,先调用这个元素的hashCode方法,一下子能定位到它应该旋转的物理位置,若该位置没有元素,可直接存储;若该位置有元素,就调用它的equals方法与新元素进行比较,若相同则不存,不相同,就放到该位置的链表末端。

4)equals与hashCode方法关系:

hashCode()是一个本地方法,实现是根据本地机器上关的。equals()相等的对象,hashCode()也一定相等;hashCode()不等,equals()一定也不等;hashCode()相等,equals()可能相等,也可能不等。

所以在重写equals(Object obj)方法,有必要重写hashCode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashCode()返回值。

5)equals与==的关系:

Integer b1 = 127;在java编译时被编译成Integer b1 = Integer.valueOf(127);对于-128到127之间的Integer值,用的是原生数据类型int,会在内存里供重用,也就是这之间的Integer值进行==比较时,只是进行int原生数据类型的数值进行比较。而超出-128〜127的范围,进行==比较时是进行地址及数值比较。

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

int\float占用4个字节,short\char占用2个字节,long\double占用8个字节,byte占用1个字节 boolean占一位

基本数据类型存放在栈里,包装类栈里存放的是对象的引用,即值的地址,而值存放在堆里。

3、int与integer的区别

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

4、谈谈对java多态的理解

同一个消息可以根据发送对象的不同而采用多种不同的行为方式,在执行期间判断所引用的对象的实际类型,根据其实际的类型调用其相应的方法。

作用:消除类型之间的耦合关系。实现多态的必要条件:继承、重写(因为必须调用父类中存在的方法)、父类引用指向子类对象

5、String、StringBuffer、StringBuilder区别

都是字符串类,String类中使用字符数组保存字符串,因有final修饰符,String对象是不可变的,每次对String操作都会生成新的String对象,这样效率低,且浪费内存空间。但线程安全。

StringBuilder和StringBuffer也是使用字符数组保存字符,但这两种对象都是可变的,即对字符串进行append操作,不会产生新的对象。它们的区别是:StringBuffer对方法加了同步锁,是线程安全的,StringBuilder非线程安全。

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

内部类指在类的内部再定义另一个类。

内部类的作用:1)实现多重继承,因为java中类的继承只能单继承,使用内部类可达到多重继承;2)内部类可以很好的实现隐藏,一般非内部类,不允许有private或protected权限的,但内部类可以;3)减少了类文件编译后产生的字节码文件大小;

内部类在编译完后也会产生.class文件,但文件名称是:外部类名称$内部类名称.class。分为以下几种:

1)成员内部类,作为外部类的一个成员存在,与外部类的属性、方法并列,成员内部类持有外部类的引用,成员内部类不能定义static变量和方法。应用场合:每一个外部类都需要一个内部类实例,内部类离不开外部类存在。

2)静态内部类,内部类以static声明,其他类可通过外部类.内部类来访问。特点:不会持有外部类的引用,可以访问外部类的静态变量,若要访问成员变量须通过外部类的实例访问。应用场合:内部类不需要外部类的实例,仅为外部类提供或逻辑上属于外部类,逻辑上可单独存在。设计的意义:加强了类的封装性(静态内部类是外部类的子行为或子属性,两者保持着一定关系),提高了代码的可读性(相关联的代码放在一起)。

3)匿名内部类,在整个操作中只使用一次,没有名字,使用new创建,没有具体位置。

4)局部内部类,在方法内或是代码块中定义类,

7、抽象类和接口区别

抽象类在类前面须用abstract关键字修饰,一般至少包含一个抽象方法,抽象方法指只有声明,用关键字abstract修饰,没有具体的实现的方法。因抽象类中含有无具体实现的方法,固不能用抽象类创建对象。当然如果只是用abstract修饰类而无具体实现,也是抽象类。抽象类也可以有成员变量和普通的成员方法。抽象方法必须为public或protected(若为private,不能被子类继承,子类无法实现该方法)。若一个类继承一个抽象类,则必须实现父类中所有的抽象方法,若子类没有实现父类的抽象方法,则也应该定义为抽象类。

接口用关键字interface修饰,接口也可以含有变量和方法,接口中的变量会被隐式指定为public static final变量。方法会被隐式的指定为public abstract,接口中的所有方法均不能有具体的实现,即接口中的方法都必须为抽象方法。若一个非抽象类实现某个接口,必须实现该接口中所有的方法。

区别:1)抽象类可以提供成员方法实现的细节,而接口只能存在抽象方法;

2)抽象类的成员变量可以是各种类型,而接口中成员变量只能是public static final类型;

3)接口中不能含有静态方法及静态代码块,而抽象类可以有静态方法和静态代码块;

4)一个类只能继承一个抽象类,用extends来继承,却可以实现多个接口,用implements来实现接口。

7.1、抽象类的意义

抽象类是用来提供子类的通用性,用来创建继承层级里子类的模板,减少代码编写,有利于代码规范化。

7.2、抽象类与接口的应用场景

抽象类的应用场景:1)规范了一组公共的方法,与状态无关,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定状态来实现特定功能;

2)定义一组接口,但不强迫每个实现类都必须实现所有的方法,可用抽象类定义一组方法体可以是空方法体,由子类选择自己感兴趣的方法来覆盖;

7.3、抽象类是否可以没有方法和属性?

可以

7.4、接口的意义

1)有利于代码的规范,对于大型项目,对一些接口进行定义,可以给开发人员一个清晰的指示,防止开发人员随意命名和代码混乱,影响开发效率。

2)有利于代码维护和扩展,当前类不能满足要求时,不需要重新设计类,只需要重新写了个类实现对应的方法。

3)解耦作用,全局变量的定义,当发生需求变化时,只需改变接口中的值即可。

4)直接看接口,就可以清楚知道具体实现类间的关系,代码交给别人看,别人也能立马明白。

8、泛型中extends和super的区别

<? extends T>限定参数类型的上界,参数类型必须是T或T的子类型,但对于List<? extends T>,不能通过add()来加入元素,因为不知道<? extends T>是T的哪一种子类;

<? super T>限定参数类型的下界,参数类型必须是T或T的父类型,不能能过get()获取元素,因为不知道哪个超类;

9、父类的静态方法能否被子类重写?静态属性和静态方法是否可以被继承?

父类的静态方法和属性不能被子类重写,但子类可以继承父类静态方法和属性,如父类和子类都有同名同参同返回值的静态方法show(),声明的实例Father father = new Son(); (Son extends Father),会调用father对象的静态方法。静态是指在编译时就会分配内存且一直存在,跟对象实例无关。

10、进程和线程的区别

进程:具有一定独立功能的程序,是系统进行资源分配和调度运行的基本单位。每一个Android应用可以理解为一个进程,但一个应用也可以设置多个进程。

线程:进程的一个实体,是CPU调度的苯单位,也是进程中执行运算的最小单位,即执行处理机调度的基本单位,如果把进程理解为逻辑上操作系统所完成的任务,线程则表示完成该任务的许多可能的子任务之一。Android中分主线程和子线程,UI更新只能在主线程中进行。

关系:一个进程可有多个线程,至少一个;一个线程只能属于一个进程。同一进程的所有线程共享该进程的所有资源。不同进程的线程间要利用消息通信方式实现同步。

区别:进程有独立的地址空间,而多个线程共享内存;进程具有一个独立功能的程序,线程不能独立运行,必须依存于应用程序中;

11、final,finally,finalize的区别

final:变量、类、方法的修饰符,被final修饰的类不能被继承,变量或方法被final修饰则不能被修改和重写。

finally:异常处理时提供finally块来执行清除操作,不管有没有异常抛出,此处代码都会被执行。如果try语句块中包含return语句,finally语句块是在return之后运行;

finalize:Object类中定义的方法,若子类覆盖了finalize()方法,在在垃圾收集器将对象从内存中清除前,会执行该方法,确定对象是否会被回收。

12、序列化Serializable 和Parcelable 的区别

序列化:将一个对象转换成可存储或可传输的状态,序列化后的对象可以在网络上传输,也可以存储到本地,或实现跨进程传输;

为什么要进行序列化:开发过程中,我们需要将对象的引用传给其他activity或fragment使用时,需要将这些对象放到一个Intent或Bundle中,再进行传递,而Intent或Bundle只能识别基本数据类型和被序列化的类型。

Serializable:表示将一个对象转换成可存储或可传输的状态。

Parcelable:与Serializable实现的效果相同,也是将一个对象转换成可传输的状态,但它的实现原理是将一个完整的对象进行分解,分解后的每一部分都是Intent所支持的数据类型,这样实现传递对象的功能。

Parcelable实现序列化的重要方法:序列化功能是由writeToParcel完成,通过Parcel中的write方法来完成;反序列化由CREATOR完成,内部标明了如何创建序列化对象及数级,通过Parcel的read方法完成;内容描述功能由describeContents方法完成,一般直接返回0。

区别:Serializable在序列化时会产生大量临时变量,引起频繁GC。Serializable本质上使用了反射,序列化过程慢。Parcelable不能将数据存储在磁盘上,在外界变化时,它不能很好的保证数据的持续性。

选择原则:若仅在内存中使用,如activity\service间传递对象,优先使用Parcelable,它性能高。若是持久化操作,优先使用Serializable

注意:静态成员变量属于类,不属于对象,固不会参与序列化的过程;用transient关键字编辑的成员变量不会参与序列化过程;可以通过重写writeObject()和readObject()方法来重写系统默认的序列化和反序列化。

13、谈谈对kotlin的理解

特点:1)代码量少且代码末尾没有分号;2)空类型安全(编译期处理了各种null情况,避免执行时异常);3)函数式的,可使用lambda表达式;4)可扩展方法(可扩展任意类的的属性);5)互操作性强,可以在一个项目中使用kotlin和java两种语言混合开发;

14、string 转换成 integer的方式及原理

1)parseInt(String s)内部调用parseInt(s, 10)默认为10进制 。2)正常判断null\进制范围,length等。3)判断第一个字符是否是符号位。4)循环遍历确定每个字符的十进制值。5)通过*=和-=进行计算拼接。6)判断是否为负值返回结果。

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

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

利用可达性分析算法,虚拟机会将一些对象定义为GC Roots,从GC Roots出发沿着引用链向下寻找,如果某个对象不能通过GC Roots寻找到,虚拟机就认为该对象可以被回收掉。

1.1 哪些对象可以被看做是GC Roots呢?

1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

2)方法区中的类静态属性引用的对象,常量引用的对象;

3)本地方法栈中JNI(Native方法)引用的对象;

1.2 对象不可达,一定会被垃圾收集器回收么?

即使不可达,对象也不一定会被垃圾收集器回收,1)先判断对象是否有必要执行finalize()方法,对象必须重写finalize()方法且没有被运行过。2)若有必要执行,会把对象放到一个队列中,JVM会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。

2、讲一下常见编码方式?

编码的意义:计算机中存储的最小单元是一个字节即8bit,所能表示的字符范围是255个,而人类要表示的符号太多,无法用一个字节来完全表示,固需要将符号编码,将各种语言翻译成计算机能懂的语言。

1)ASCII码:总共128个,用一个字节的低7位表示,0〜31控制字符如换回车删除等;32~126是打印字符,可通过键盘输入并显示出来;

2)ISO-8859-1,用来扩展ASCII编码,256个字符,涵盖了大多数西欧语言字符。

3)GB2312:双字节编码,总编码范围是A1-A7,A1-A9是符号区,包含682个字符,B0-B7是汉字区,包含6763个汉字;

4)GBK为了扩展GB2312,加入了更多的汉字,编码范围是8140~FEFE,有23940个码位,能表示21003个汉字。

5)UTF-16: ISO试图想创建一个全新的超语言字典,世界上所有语言都可通过这本字典Unicode来相互翻译,而UTF-16定义了Unicode字符在计算机中存取方法,用两个字节来表示Unicode转化格式。不论什么字符都可用两字节表示,即16bit,固叫UTF-16。

6)UTF-8:UTF-16统一采用两字节表示一个字符,但有些字符只用一个字节就可表示,浪费存储空间,而UTF-8采用一种变长技术,每个编码区域有不同的字码长度。  不同类型的字符可以由1~6个字节组成。                                                                                                                                                                                                                       

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

utf-8是一种变长编码技术,utf-8编码中的中文占用的字节不确定,可能2个、3个、4个,int型占4个字节。

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

代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问,将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类的方法。

区别:1)静态代理:由程序员创建或是由特定工具生成,在代码编译时就确定了被代理的类是哪一个是静态代理。静态代理通常只代理一个类;

2)动态代理:在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口下的多个实现类;

实现步骤:a.实现InvocationHandler接口创建自己的调用处理器;b.给Proxy类提供ClassLoader和代理接口类型数组创建动态代理类;c.利用反射机制得到动态代理类的构造函数;d.利用动态代理类的构造函数创建动态代理类对象;

使用场景:Retrofit中直接调用接口的方法;Spring的AOP机制;

5、Java的异常体系

Java中Throwable是所有异常和错误的超类,两个直接子类是Error(错误)和Exception(异常):

1)Error是程序无法处理的错误,由JVM产生和抛出,如OOM、ThreadDeath等。这些异常发生时,JVM一般会选择终止程序。

2)Exception是程序本身可以处理的异常,又分为运行时异常(RuntimeException)(也叫Checked Eception)和非运行时异常(不检查异常Unchecked Exception)。运行时异常有NullPointerException\IndexOutOfBoundsException等,这些异常一般是由程序逻辑错误引起的,应尽可能避免。非运行时异常有IOException\SQLException\FileNotFoundException以及由用户自定义的Exception异常等。

6、谈谈你对解析与分派的认识。

解析指方法在运行前,即编译期间就可知的,有一个确定的版本,运行期间也不会改变。解析是静态的,在类加载的解析阶段就可将符号引用转变成直接引用。

分派可分为静态分派和动态分派,重载属于静态分派,覆盖属于动态分派。静态分派是指在重载时通过参数的静态类型而非实际类型作为判断依据,在编译阶段,编译器可根据参数的静态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。

7、修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法?

会调用对象的equals方法,如果对象的equals方法没有被重写,equals方法和==都是比较栈内局部变量表中指向堆内存地址值是否相等。

8、Java中实现多态的机制是什么?

多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用修改源程序,就可以让引用变量绑定到各种不同的类实现上。Java实现多态有三个必要条件:继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。

9、如何将一个Java对象序列化到文件里?

ObjectOutputStream.writeObject()负责将指定的流写入,ObjectInputStream.readObject()从指定流读取序列化数据。

//写入
try {
    ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("D:/student.txt"));
    os.writeObject(studentList);
    os.close();
} catch(FileNotFoundException e) {
    e.printStackTrace();
} catch(IOException e) {
    e.printStackTrace();
}

10、说说你对Java反射的理解

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

反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只对系统应用开放,这里就可以利用java的反射机制通过反射来获取所需的私有成员或是方法。

1) 获取类的Class对象实例 Class clz = Class.forName("com.zhenai.api.Apple");

2) 根据Class对象实例获取Constructor对象  Constructor appConstructor = clz.getConstructor();

3) 使用Constructor对象的newInstance方法获取反射类对象 Object appleObj = appConstructor.newInstance();

4) 获取方法的Method对象  Method setPriceMethod = clz.getMethod("setPrice", int.class);

5) 利用invoke方法调用方法  setPriceMethod.invoke(appleObj, 14);

6) 通过getFields()可以获取Class类的属性,但无法获取私有属性,而getDeclaredFields()可以获取到包括私有属性在内的所有属性。带有Declared修饰的方法可以反射到私有的方法,没有Declared修饰的只能用来反射公有的方法,其他如Annotation\Field\Constructor也是如此。

11、说说你对Java注解的理解

注解是通过@interface关键字来进行定义的,形式和接口差不多,只是前面多了一个@

public @interface TestAnnotation {

}

使用时@TestAnnotation来引用,要使注解能正常工作,还需要使用元注解,它是可以注解到注解上的注解。元标签有@Retention @Documented @Target @Inherited @Repeatable五种

@Retention说明注解的存活时间,取值有RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候,并不会被加载到JVM中。RetentionPolicy.RUNTIME可以留到程序运行的时候,它会被加载进入到JVM中,所以在程序运行时可以获取到它们。

@Documented 注解中的元素包含到javadoc中去

@Target  限定注解的应用场景,ElementType.FIELD给属性进行注解;ElementType.LOCAL_VARIABLE可以给局部变量进行注解;ElementType.METHOD可以给方法进行注解;ElementType.PACKAGE可以给一个包进行注解 ElementType.TYPE可以给一个类型进行注解,如类、接口、枚举

@Inherited 若一个超类被@Inherited注解过的注解进行注解,它的子类没有被任何注解应用的话,该子类就可继承超类的注解;

注解的作用:

1)提供信息给编译器:编译器可利用注解来探测错误和警告信息

2)编译阶段:软件工具可以利用注解信息来生成代码、html文档或做其它相应处理;

3)运行阶段:程序运行时可利用注解提取代码

注解是通过反射获取的,可以通过Class对象的isAnnotationPresent()方法判断它是否应用了某个注解,再通过getAnnotation()方法获取Annotation对象

12、说一下泛型原理,并举例说明

泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。Java泛型是在Java1.5以后出现的,为保持对以前版本的兼容,使用了擦除的方法实现泛型。擦除是指在一定程度无视类型参数T,直接从T所在的类开始向上T的父类去擦除,如调用泛型方法,传入类型参数T进入方法内部,若没在声明时做类似public T methodName(T extends Father t){},Java就进行了向上类型的擦除,直接把参数t当做Object类来处理,而不是传进去的T。即在有泛型的任何类和方法内部,它都无法知道自己的泛型参数,擦除和转型都是在边界上发生,即传进去的参在进入类或方法时被擦除掉,但传出来的时候又被转成了我们设置的T。在泛型类或方法内,任何涉及到具体类型(即擦除后的类型的子类)操作都不能进行,如new T(),或者T.play()(play为某子类的方法而不是擦除后的类的方法)

13、Java中String的了解

1)String类是final型,固String类不能被继承,它的成员方法也都默认为final方法。String对象一旦创建就固定不变了,对String对象的任何改变都不影响到原对象,相关的任何改变操作都会生成新的String对象。

2)String类是通过char数组来保存字符串的,String对equals方法进行了重定,比较的是值相等。

String a = "test"; String b = "test"; String c = new String("test");

a、b和字面上的test都是指向JVM字符串常量池中的"test"对象,他们指向同一个对象。而new关键字一定会产生一个对象test,该对象存储在堆中。所以new String("test")产生了两个对象,保存在栈中的c和保存在堆中的test。而在java中根本就不存在两个完全一模一样的字符串对象,故在堆中的test应该是引用字符串常量池中的test。

例:

String str1 = "abc"; //栈中开辟一块空间存放引用str1,str1指向池中String常量"abc"
String str2 = "def"; //栈中开辟一块空间存放引用str2,str2指向池中String常量"def"
String str3 = str1 + str2;//栈中开辟一块空间存放引用str3
//str1+str2通过StringBuilder的最后一步toString()方法返回一个新的String对象"abcdef"
//会在堆中开辟一块空间存放此对象,引用str3指向堆中的(str1+str2)所返回的新String对象。
System.out.println(str3 == "abcdef");//返回false
因为str3指向堆中的"abcdef"对象,而"abcdef"是字符池中的对象,所以结果为false。JVM对String str="abc"对象放在常量池是在编译时做的,而String str3=str1+str2是在运行时才知道的,new对象也是在运行时才做的。

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

1)字符串常量池需要String不可变。因为String设计成不可变,当创建一个String对象时,若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。如果字符串变量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象。

2)String对象可以缓存hashCode。字符串的不可变性保证了hash码的唯一性,因此可以缓存String的hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较hashCode,提高了比较性能;

3)安全性。String被许多java类用来当作参数,如url地址,文件path路径,反射机制所需的Strign参数等,若String可变,将会引起各种安全隐患。

(三) 数据结构

1、常用数据结构简介

数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素间的关系组成。常用的数据有:数组、栈、队列、链表、树、图、堆、散列表。

1)数组:在内存中连续存储多个元素的结构。数组元素通过下标访问,下标从0开始。优点:访问速度快;缺点:数组大小固定后无法扩容,只能存储一种类型的数据,添加删除操作慢。适用场景:适用于需频繁查找,对存储空间要求不高,很少添加删除。

2)栈:一种特殊的线性表,只可以在栈顶操作,先进后出,从栈顶放入元素叫入栈,从栈顶取出元素叫出栈。应用场景:用于实现递归功能,如斐波那契数列。

3)队列:一种线性表,在列表一端添加元素,另一端取出,先进先出。使用场景:多线程阻塞队列管理中。

4)链表:物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个结点,一个是存储元素的数据域,一个是指向下一个结点地址的指针域。有单链表、双向链表、循环链表。优点:可以任意加减元素,不需要初始化容量,添加删除元素只需改变前后两个元素结点的指针域即可。缺点:因为含有大量指针域,固占用空间大,查找耗时。适用场景:数据量小,需频繁增加删除操作。

5)树:由n个有限节点组成一种具有层次关系的集合。二叉树(每个结点最多有两个子树,结点的度最大为2,左子树和右子树有顺序)、红黑树(HashMap底层源码)、B+树(mysql的数据库索引结构)

6)散列表(哈希表):根据键值对来存储访问。

7)堆:堆中某个节点的值总是不大于或不小于其父节点的值,堆总是一棵完全二叉树。

8)图:由结点的有穷集合V和边的集合E组成。

2、并发集合了解哪些?

1)并发List,包括Vector和CopyOnWriteArrayList是两个线程安全的List,Vector读写操作都用了同步,CopyOnWriteArrayList在写的时候会复制一个副本,对副本写,写完用副本替换原值,读时不需要同步。

2)并发Set,CopyOnWriteArraySet基于CopyOnWriteArrayList来实现的,不允许存在重复的对象。

3)并发Map,ConcurrentHashMap,内部实现了锁分离,get操作是无锁的。

4)并发Queue,ConcurrentLinkedQueue适用于高并发场景下的队列,通过无锁方式实现。 BlockingQueue阻塞队列,应用场景,生产者-消费者模式,若生产快于消费,生产队列装满时会阻塞,等待消费。

5)并发Deque, LinkedBlockingDueue没有进行读写锁分离,同一时间只能有一个线程对其操作。

6)并发锁重入锁ReentrantLock,互斥锁,一次最多只能一个线程拿到锁。

7)读写锁ReadWriteLock,有读取和写入锁两种,读取允许多个读取线程同时持有,而写入只能有一个线程持有。

3、列举java的集合以及集合之间的继承关系

5、容器类介绍以及之间的区别

1)Collection接口:集合框架的根接口,它是集合类框架中最具一般性的顶层接口。

2)Map接口:提供了键值对的映射关系的集合,关键字不能有重复值,每个关键字至多可映射一个值。HashMap(通过散列机制,用于快速访问),TreeMap(保持key处于排序状态,访问速度不如hashmap), LinkedHashMap(保持元素的插入顺序)

3)Set接口:可包含重复的元素,LinkedHashSet TreeSet(用红黑树来存储元素) HashSet

4)List接口:可通过索引对元素进行精准的插入和查找,实现类有ArrayList LinkedList

5)Queue接口:继承自Collection接口,LinkedList实现了Queue接口,提供了支持队列的行为。

6)Iterator接口:为了迭代集合

7)Comparable接口:用于比较

6、List,Set,Map的区别

Set是一个无序的集合,不能包含重复的元素;

list是一个有序的集合可以包含重复的元素,提供了按索引访问的方式;

map包含了key-value对,map中key必须唯一,value可以重复。

7、HashMap的实现原理

1)数据结构

jdk1.7及以前,HashMap由数组+链表组成,数组Entry是HashMap的主体,Entry是HashMap中的一个静态内部类,每一个Entry包含一个key-value键值对,链表是为解决哈希冲突而存在。

从jdk1.8起,HashMap是由数组+链表/红黑树组成,当某个bucket位置的链表长度达到阀值8时,这个链表就转变成红黑树。

2)HashMap是线程不安全的,存储比较快,能接受null值,HashMap通过put(key, value)来储存元素,通过get(key)来得到value值,通过hash算法来计算hashcode值,用hashcode标识Entry在bucket中存储的位置。

3)HashMap中为什么要使用加载因子,为什么要进行扩容

加载因子是指当HashMap中存储的元素/最大空间值的阀值,如果超过这个值,就会进行扩容。加载因子是为了让空间得到充分利用,如果加载因子太大,虽对空间利用更充分,但查找效率会降低;如果加载因子太小,表中的数据过于稀疏,很多空间还没用就开始扩容,就会对空间造成浪费。

至于为什么要扩容,如果不扩容,HashMap中数组处的链表会越来越长,这样查找效率就会大大降低。

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

当我们使用put(key, value)存储对象到HashMap中时,具体实现步骤如下:

1)先判断table数组是否为空,为空以默认大小构建table,table默认空间大小为16

2)计算key的hash值,并计算hash&(n-1)值得到在数组中的位置index,如果该位置没值即table[index]为空,则直接将该键值对存放在table[index]处。

3)如果table[index]处不为空,说明发生了hash冲突,判断table[index]处结点是否是TreeNode(红黑树结点)类型数据,如果是则执行putTreeVal方法,按红黑树规则将键值对存入;

4)如果table[index]是链表形式,遍历该链表上的数据,将该键值对放在table[index]处,并将其指向原index处的链表。判断链表上的结点数是否大于链表最大结点限制(默认为8),如果超过了需执行treeifyBin()操作,则要将该链表转换成红黑树结构。

5)判断HashMap中数据个数是否超过了(最大容量*装载因子),如果超过了,还需要对其进行扩容操作。

7.2 HashMap如何get数据?

get(key)方法获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=table[hash&(n-1)],先判断first(即数组中的那个)的key是否与参数key相等,不等的话,判断结点是否是TreeNode类型,是则调用getTreeNode(hash, key)从二叉树中查找结点,不是TreeNode类型说明还是链表型,就遍历链表找到相同的key值返回对应的value值即可。

7.3 当两个对象的hashcode相同,即发生碰撞时,HashMap如何处理

当两个对象的hashcode相同,它们的bucket位置相同,hashMap会用链表或是红黑树来存储对象。Entry类里有一个next属性,作用是指向下一个Entry。第一个键值对A进来,通过计算其key的hash得到index,记做Entry[index]=A。一会又进来一个键值对B,通过计算其key的hash也是index,HashMap会将B.next=A, Entry[index]=B.如果又进来C,其key的hash也是index,会将C.next=B, Entry[index]=C.这样bucket为index的地方存放了A\B\C三个键值对,它们能过next属性链在一起。数组中存储的是最后插入的元素,其他元素都在后面的链表里。

7.4 如果两个键的hashcode相同,如何获取值对象?

当调用get方法时,hashmap会使用键对象的hashcode找到bucket位置,找到bucket位置后,会调用key.equals()方法去找到链表中正确的节点,最终找到值对象。

7.5 hashMap如何扩容

HashMap默认负载因为是0.75,当一个map填满了75%的bucket时,和其他集合类一样,将会创建原来HashMap大小两倍的bucket数组,来重新调整HashMap的大小,并将原来的对象放入新的bucket数组中。

在jdk1.7及以前,多线程扩容可能出现死循环。因为在调整大小过程中,存储在某个bucket位置中的链表元素次序会反过来,而多线程情况下可能某个线程翻转完链表,另外一个线程又开始翻转,条件竞争发生了,那么就死循环了。

而在jdk1.8中,会将原来链表结构保存至节点e中,将原来数组中的位置设为null,然后依次遍历e,根据hash&n是否为0分成两条支链,保存在新数组中。如果多线程情况可能会取到null值造成数据丢失。

8、ConcurrentHashMap的实现原理

1)jdk1.7及以前:一个ConcurrentHashMap由一个segment数组和多个HashEntry组成,每一个segment都包含一个HashEntry数组, Segment继承ReentrantLock用来充当锁角色,每一个segment包含了对自己的HashEntry的操作,如get\put\replace操作,这些操作发生时,对自己的HashEntry进行锁定。由于每一个segment写操作只锁定自己的HashEntry,可以存在多个线程同时写的情况。

jdk1.8以后:ConcurrentHashMap取消了segments字段,采用transient volatile HashEntry<K, V> table保存数据,采用table数组元素作为锁,实现对每一个数组数据进行加锁,进一小减少并发冲突概率。ConcurrentHashMap是用Node数组+链表+红黑树数据结构来实现的,并发制定用synchronized和CAS操作。

2)Segment实现了ReentrantLock重入锁,当执行put操作,会进行第一次key的hash来定位Segment的位置,若该Segment还没有初始化,会通过CAS操作进行赋值,再进行第二次hash操作,找到相应的HashEntry位置。

9、ArrayMap和HashMap的对比

1)存储方式不一样,HashMap内部有一个Node<K,V>[]对象,每个键值对都会存储到这个对象里,当用put方法添加键值对时,会new一个Node对象,tab[i] = newNode(hash, key, value, next);

ArrayMap存储则是由两个数组来维护,int[] mHashes; Object[] mArray; mHashes数组中保存的是每一项的HashCode值,mArray存的是键值对,每两个元素代表一个键值对,前面保存key,后面保存value。mHashes[index]=hash; mArray[index<<1]=key; mArray[(index<<1)+1]=value;

ArrayMap相对于HashMap,无需为每个键值对创建Node对象,且在数组中连续存放,更省空间。

2)添加数据时扩容处理不一样,进行了new操作,重新创建对象,开销很大;而ArrayMap用的是copy数据,所有效率相对高些;

3)ArrayMap提供了数组收缩功能,在clear或remove后,会重新收缩数组,释放空间;

4)ArrayMap采用二分法查找,mHashes中的hash值是按照从小到大的顺序连续存放的,通过二分查找来获取对应hash下标index,去mArray中查找键值对。mHashes中的index*2是mArray中的key下标,index*2+1为value的下标,由于存在hash碰撞情况,二分查找到的下标可能是多个连续相同的hash值中的任意一个,此时需要用equals比对命中的key对象是否相等,不相等,应当从当前index先向后再向前遍历所有相同hash值。

5)sparseArray比ArrayMap进一步优化空间,SparseArray专门对基本类型做了优化,Key只能是可排序的基本类型,如int\long,对value,除了泛型Value,还对每种基本类型有单独实现,如SparseBooleanArray\SparseLongArray等。无需包装,直接使用基本类型值,无需hash,直接使用基本类型值索引和判断相等,无碰撞,无需调用hashCode方法,无需equals比较。SparseArray延迟删除。

10、HashTable实现原理

Hashtable中的无参构造方法Hashtable()中调用了this(11, 0.75f),说明它默认容量是11,加载因子是0.75,在构造方法上会new HashtableEntry<?, ?>[initialCapacity]; 会新建一个容量是初始容量的HashtableEntry数组。HashtableEntry数组中包含hash\Key\Value\next变量,链表形式,重写了hashCode和equals方法。Hashtable所有public方法都在方法体上加上了synchronized锁操作,说明它是线程安全的。它还实现了Serializable接口中的writeObject和readObject方法,分别实现了逐行读取和写入的功能,并且加了synchronized锁操作。

(1) put(Key, Value)方法

1)先判断value是否为空,为空抛出空指针异常;

2)根据key的hashCode()值,计算table表中的位置索引(hash&0x7FFFFFFF)%tab.length值index,如果该索引处有值,再判断该索引处链表中是否包含相同的key,如果key值相同则替换旧值。

3)如果没有相同的key值,调用addEntry方法,在addEntry中判断count大小是否超过了最大容量限制,如果超过了需要重新rehash(),容量变成原来容量*2+1,将原表中的值都重新计算hash值放入新表中。再构造一个HashtableEntry对象放入相应的table表头,如果原索引处有值,则将table[index].next指向原索引处的链表。

(2)get方法

根所key.hashCode(),计算它在table表中的位置,(hash&0x7FFFFFFF)%tab.length,遍历该索引处表的位置中是否有值,是否存在链表,再判断是key值和hash值是否相等,相等则返回对应的value值。

11、HashMap和HashTable的区别

1)Hashtable是个线程安全的类,在对外方法都添加了synchronized方法,序列化方法上也添加了synchronized同步锁方法,而HashMap非线程安全。这也导致Hashtable的读写等操作比HashMap慢。

2)Hashtable不允许值和键为空,若为空会抛出空指针。而HashMap允许键和值为空;

3)Hashtable根据key值的hashCode计算索引,(hash&0x7FFFFFFF)%tab.length,保证hash值始终为正数且不超过表的长度。而HashMap中计算索引值是通过hash(key)&(tab.length-1),是通过与操作,计算出在表中的位置会比Hashtable快。

4)Hashtable容量能为任意大于等于1的正数,而HashMap的容量必须为2^n,Hashtable默认容量为11,HashMap初始容量为16

5)Hashtable每次扩容,新容量为旧容量的2倍+1,而HashMap为旧容量的2倍。

12、HashMap与HashSet的区别

HashSet底层实现是HashMap,内部包含一个HashMap<E, Ojbect> map变量

private transient HashMap<E,Object> map;

一个Object PRESENT变量(当成插入map中的value值)

private static final Object PRESENT = new Object();

HashSet中元素都存到HashMap键值对的Key上面。具体可以查看HashSet的add方法,直接调用了HashMap的put方法,将值作为HashMap的键,值用一个固定的PRESENT值。

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

HashSet没有单独的get方法,用的是HashMap的。HashSet实现了Set接口,不允许集合中出现重复元素,将对象存储进HashSet前,要先确保对象重写了hashCode()和equals方法,以保证放入set对象是唯一的。

13、HashSet与HashMap怎么判断集合元素重复?

HashMap在放入key-value键值对是,先通过key计算其hashCode()值,再与tab.length-1做与操作,确定下标index处是否有值,如果有值,再调用key对象的equals方法,对象不同则插入到表头,相同则覆盖;

HashSet是将数据存放到HashMap的key中,HashMap是key-value形式的数据结构,它的key是唯一的,HashSet利用此原理保证放入的对象唯一性。

14、集合Set实现Hash怎么防止碰撞

HashSet底层实现是HashMap,HashMap如果两个不同Key对象的hashCode()值相等,会用链表存储,HashSet也一样。

15、ArrayList和LinkedList的区别,以及应用场景

ArrayList底层是用数组实现的,随着元素添加,其大小是动态增大的;在内存中是连续存放的;如果在集合末尾添加或删除元素,所用时间是一致的,如果在列表中间添加或删除元素,所用时间会大大增加。通过索引查找元素速度很快。适合场合:查询比较多的场景

LinkedList底层是通过双向链表实现的,LinkedList和ArrayList相比,增删速度快,但查询和修改值速度慢。在内存中不是连续内存。场景:增删操作比较多的场景。

16、二叉树的深度优先遍历和广度优先遍历的具体实现

public class Tree {
    Tree left, right;
    int data;
    public Tree(int data) {
        this.data = data;
    }

}
//深度优先遍历(其实和前序遍历实现一样)
public void queryByDeepth(Tree root) {
    if(root != null) {
        print(root.data);
    }
    if(root.left != null) queryByDeepth(root.left);
    if(root.right != null) queryByDeepth(root.right);
}

//广度优先遍历(用队列辅助实现)
public void queryByDeepth(Tree root) {
    if(root == null) return;
    Queue<Tree> queue = new LinkedList<Tree>();
    queue.offer(root);
    while(root != null || !queue.isEmpty()) {
        root = queue.poll();
        print(root.data);
        if(root.left != null) queue.offer(root.left);
        if(root.right != null) queue.offer(root.right);
    }
    
}

17、堆的结构

java的堆用于存放对象实例的,通过new一个对象实例,在堆上给实例分配内存空间。而数据结构中堆是一种树,实现了优先队列的插入和删除的时间复杂度都为o(logn),常用数组来表示。

18、堆和树的区别

堆是一棵除最底层外被完全填满的二叉树,底层上的元素从左到右填入,即完全二叉树,它可用一个数组表示,对于数组任一个位置上的i个元素,左儿子位置为2*i+1,右儿子在2*i+2上,父亲则在位置i/2上。

树又分二叉树(每个节点不能有多于两个的子节点)、二叉查找树(在二叉树基础上,对树中每个节点X,左子树中所有项的值小于X中的项,右子树中所有项的值大于X中的项)、AVL树(深度必须是O(logN),平衡二叉查找树的每个节点的左子树和右子树最多差1的二叉查找树,对于插入后破坏平衡性,通过旋转调节)、伸展树。

19、堆和栈在内存中的区别是什么(解答提示:可以从数据结构方面以及实际实现方面两个方面去回答)?

(1)数据结构方面:堆是一种二叉树;

栈是一种后进先出的存储结构,有压栈和出栈两种操作。

(2)内存方面:

栈内存:静态变量、局部变量是以压栈出栈的方式分配内存,系统会在一个代码段中分配和回收局部变量,实际上每个代码段、函数都是一个或多个嵌套的栈,不需要手动管理栈区内存。

堆内存:在Java运行时被用为对象分配内存,GC在堆内存上释放没有任何引用的对象所占的内存,任何在堆上被创建的对象都有一个全局的访问。

20、什么是深拷贝和浅拷贝

1)深拷贝:不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。即对象进行深拷贝要对整个对象图进行拷贝。

实现方式:a)重写clone方法,要为对象图的每一层的每个对象都实现Cloneable接口并重写clone方法。b)将对象序列化为字节序列后,默认会将该对象整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

2)浅拷贝:对于基本数据类型,直接进行值传递,即直接复制一个值给新对象,对一个对象的值改变不会影响到拷贝的数据;对于引用数据类型的成员变量,如成员变量是某个数组或类对象,它会进行引用传递,只是将该成员变量的引用值(地址)复制一份给新对象,因为两个对象都指向同一个实例,故改变该成员变量会影响到另一个对象的该成员变量值。

实现方式:a)通过拷贝构造方法实现,指该类的构造方法的参数为该类的对象。b)重写clone方法,使用clone方法的类必须实现Cloneable接口。

21、手写链表逆序代码

//假设LinkedList的结点是Node
//1、遍历法
public Node reverseLinkedList(LinkedList head) {
    if(head == null || head.next == null) return head;
    Node pre = null, next = null;
    while(head != null) {
        next = head.next;
        next.next = pre;
        pre = head;
        head = next;
    }
    return pre;
}
//2、递归法
public Node reverseLinkedList(LinkedList head) {
    if(head == null || head.next == null) return head;
    Node next = head.next;
    Node newNode = reverseLinkedList(next);
    next.next = head;
    head.next = null;
    return newNode;
}

22、讲一下对树,B+树的理解

具体参考这篇文章吧:B+树

B树一个节点可以拥有多于2个子节点的二叉查找树,每个节点有M-1个key,且以升序排列,位于M-1和M key的子节点的值位于M-1和M key对应的value之间,其它节点至少有M/2个子节点,普遍运用在数据库和文件系统中。

B tree

23、讲一下对图的理解

图就是一些顶点的集合,这些顶点通过一系列边结对,顶点用圆圈表示,边就是这些圆圈之间的连线,顶点之间通过边连接。

它的物理存储结构有:

1)邻接矩阵:用两个数组存储图的信息,1个以数组存储顶点,一个二维数组存储边的信息。对于顶点多而边数少的稀疏图造成存储空间大量浪费。

2)邻接表:数组+链表,用数组存储每个节点,数组中每个节点的所有邻接点组成一个链表,邻接表关心了出度,但查找入度就要遍历整个图。

遍历图:从图中某一个顶点出发遍历途中其余顶点,每一个顶点仅被访问一次。

1)深度优先遍历

int[] visited = new int[g.vnum];
public void dfsVisitGraph(Graph g, int i) {
    visited[i] = 1;
    print(g.vex[i]);
    for(int j = 0; j < g.vnum; j++) {
        if(g.arc[i][j] != 0 && g.arc[i][j] != IUNFINITY && !visited[j]) {
            dfsVisitGraph(g, j);
        }
    }
}
public void dfsTraverse(Graph g) {
    for(int i = 0; i < g.vnum; i++) {
        if(!visited[i]) {
            dfsVisitGraph(g, i);
        }
    }
}
        

2)广度优先遍历(队列)

public void bfsTraverse(Graph g) {
    Queue<Integer> queue = new LinkedList<>();
    int[] visited = new int[g.vnum];
    for(int i = 0; i < g.vnum; i++) {
        if(visited[i] == 0) {
            visited[i] = 1;
            queue.offer(g.vexs[i]);   
            print(g.vexs[i]);
            while(!queue.isEmpty()) {
                int link = queue.poll();
                for(int j = 0; j < g.vnum; j++) {
                    if(g.arc[link][j] == 1 && visited[j] == 0) {
                        visited[j] = 1;
                        print(g.vexs[j];
                        queue.offer(g.vexs[i]);   
                    }
                }
            }
        }
    }
}

24、判断单链表成环与否?

public boolean linkHasCircle(LinkedList node) {
    if(node == null || node.next == null) return false;
    Node first = node, second = node;//first慢指针一次走一步;second快指针一次走两步
    while(second.next != null && second.next.next != null) {
        first = node.next;
        second = node.next.next;
        if(first == second) {
            return true;
        }
    }
    return false;
}

25、合并多个单有序链表(假设都是递增的)

这个主要遍历链表,比较值大小,如果需要返回链表头节点,则需要先把头结点保存好

public Node merge(LinkedList node1, LinkedList node2) {
    if(node1 == null) return node2;
    if(node2 == null) return node1;
    if(node1 == null && node2 == null) return null;
    int data1 = node1.data;
    int data2 = node2.data;
    Node newNode, head;
    int data = data2;
    if(data1 <= data2) {
        data = data1;
        node1 = node1.next;
    } else {
        node2 = node2.next;
    }
    newNode = new Node(data);
    head = newNode;

    while(node1 != null && node2 != null) {
        if(node1.data <= node2.data) {
            newNode.next.data = node1.data;
            node1 = node1.next;
        } else {
            newNode.next.data = node2.data;
            node2 = node2.next;
        }
        newNode = newNode.next;
    }
    while(node1 != null) {
            newNode.next.data = node1.data;
            node1 = node1.next;
            newNode = newNode.next;
    }

    while(node2 != null) {
            newNode.next.data = node2.data;
            node2 = node2.next;
            newNode = newNode.next;
    }
    return head;
}
            

(四) 线程、多线程和线程池

1、开启线程的三种方式?

1)继承Thread类,重写run()方法,在run()方法体中编写要完成的任务 new Thread().start();

2)实现Runnable接口,实现run()方法 new Thread(new MyRunnable()).start();

3)实现Callable接口MyCallable类,实现call()方法,使用FutureTask类来包装Callable对象,使用FutureTask对象作为Thread对象的target创建并启动线程;调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

FutureTask<Integer> ft = new FutureTask<Integer>(new MyCallable());

new Thread(ft).start();

2、run()和start()方法区别

run()方法只是线程的主体方法,和普通方法一样,不会创建新的线程。只有调用start()方法,才会启动一个新的线程,新线程才会调用run()方法,线程才会开始执行。

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

创建Semaphore变量,Semaphore semaphore = new Semaphore(5, true); 当方法进入时,请求一个信号,如果信号被用完则等待,方法运行完,释放一个信号,释放的信号新的线程就可以使用。

4、在Java中wait和seelp方法的不同

wait()方法属于Object类,调用该方法时,线程会放弃对象锁,只有该对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

sleep()方法属于Thread类,sleep()导致程序暂停执行指定的时间,让出CPU,但它的监控状态依然保存着,当指定时间到了又会回到运行状态,sleep()方法中线程不会释放对象锁。

5、谈谈wait/notify关键字的理解

notify: 唤醒在此对象监视器上等待的单个线程

notifyAll(): 通知所有等待该竞争资源的线程

wait: 释放obj的锁,导致当前的线程等待,直接其他线程调用此对象的notify()或notifyAll()方法

当要调用wait()或notify()/notifyAll()方法时,一定要对竞争资源进行加锁,一般放到synchronized(obj)代码中。当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此等待线程虽被唤醒,但仍无法获得obj锁,直到调用线程退出synchronized块,释放obj锁后,其他等待线程才有机会获得锁继续执行。

6、什么导致线程阻塞?

(1)一般线程阻塞

1)线程执行了Thread.sleep(int millsecond)方法,放弃CPU,睡眠一段时间,一段时间过后恢复执行;

2)线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等到获取到同步锁,才能恢复执行;

3)线程执行了一个对象的wait()方法,直接进入阻塞态,等待其他线程执行notify()/notifyAll()操作;

4)线程执行某些IO操作,因为等待相关资源而进入了阻塞态,如System.in,但没有收到键盘的输入,则进入阻塞态。

5)线程礼让,Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或更高优先级的线程,但并不会使线程进入阻塞态,线程仍处于可执行态,随时可能再次分得CPU时间。线程自闭,join()方法,在当前线程调用另一个线程的join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前线程再由阻塞转为就绪态。

6)线程执行suspend()使线程进入阻塞态,必须resume()方法被调用,才能使线程重新进入可执行状态。

7、线程如何关闭?

1) 使用标志位,在线程的while循环中加上标志位判断,但标志位要用volatile修饰,要不对标志位的修改同步不到子线程中。

2)使用stop()方法,但该方法就像关掉电脑电源一样,可能会发生预料不到的问题

3)使用中断interrupt()

public class Thread {
    // 中断当前线程
    public void interrupt();
    // 判断当前线程是否被中断
    public boolen isInterrupt();
    // 清除当前线程的中断状态,并返回之前的值
    public static boolen interrupted();   
}

但调用interrupt()方法只是传递中断请求消息,并不代表要立马停止目标线程。

8、讲一下java中的同步的方法

之所以需要同步,因为在多线程并发控制,当多个线程同时操作一个可共享的资源时,如果没有采取同步机制,将会导致数据不准确,因此需要加入同步锁,确保在该线程没有完成操作前被其他线程调用,从而保证该变量的唯一一性和准确性。

1)synchronized修饰同步代码块或方法

由于java的每个对象都有一个内置锁,用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需获得内置锁,否则就处于阴塞状态。

2)volatile修饰变量

保证变量在线程间的可见性,每次线程要访问volatile修饰的变量时都从内存中读取,而不缓存中,这样每个线程访问到的变量都是一样的。且使用内存屏障。

3)ReentrantLock重入锁,它常用的方法有ReentrantLock():创建一个ReentrantLock实例

lock()获得锁 unlock()释放锁

4)使用局部变量ThreadLocal实现线程同步,每个线程都会保存一份该变量的副本,副本之间相互独立,这样每个线程都可以随意修改自己的副本,而不影响其他线程。常用方法ThreadLocal()创建一个线程本地变量;get()返回此线程局部的当前线程副本变量;initialValue()返回此线程局部变量的当前线程的初始值;set(T value)将此线程变量的当前线程副本中的值设置为value

5) 使用原子变量,如AtomicInteger,常用方法AtomicInteger(int value)创建个有给定初始值的AtomicInteger整数;addAndGet(int data)以原子方式将给定值与当前值相加

6)使用阻塞队列实现线程同步LinkedBlockingQueue<E>

9、如何保证线程安全?

线程安全性体现在三方法:

1)原子性:提供互斥访问,同一时刻只能有一个线和至数据进行操作。

JDK中提供了很多atomic类,如AtomicInteger\AtomicBoolean\AtomicLong,它们是通过CAS完成原子性。JDK提供锁分为两种:synchronized依赖JVM实现锁,该关键字作用对象的作用范围内同一时刻只能有一个线程进行操作。另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性是ReentrantLock。

2)可见性:一个线程对主内存的修改及时被其他线程看到。

JVM提供了synchronized和volatile,volatile的可见性是通过内存屏障和禁止重排序实现的,volatile会在写操作时,在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存;会在读操作时,在读操作前加一条load指令,从内存中读取共享变量。

3)有序性:指令没有被编译器重排序。

可通过volatile、synchronized、Lock保证有序性。

10、两个进程同时要求写或者读,能不能实现?如何防止进程的同步?

我认为可以实现,比如两个进程都读取日历进程数据是没有问题,但同时写,应该会有冲突。

可以使用共享内存实现进程间数据共享。

11、线程间操作List

由于ArrayList不是线程安全的,如果多线程操作,会导致数据不一致,因此若要实现线程安全,需要加锁操作。

12、Java中对象的生命周期

1)创建阶段(Created):为对象分配存储空间,开始构造对象,从超类到子类对static成员初始化;超类成员变量按顺序初始化,递归调用超类的构造方法,子类成员变量按顺序初始化,子类构造方法调用。

2)应用阶段(In Use):对象至少被一个强引用持有着。

3)不可见阶段(Invisible):程序运行已超出对象作用域

4)不可达阶段(Unreachable):该对象不再被强引用所持有

5)收集阶段(Collected):假设该对象重写了finalize()方法且未执行过,会去执行该方法。

6)终结阶段(Finalized):对象运行完finalize()方法仍处于不可达状态,等待垃圾回收器对该对象空间进行回收。

7)对象空间重新分配阶段(De-allocated):垃圾回收器对该对象所占用的内存空间进行回收或再分配,该对象彻底消失。

13、static synchronized 方法的多线程访问和作用

static synchronized控制的是类的所有实例访问,不管new了多少对象,只有一份,所以对该类的所有对象都加了锁。限制多线程中该类的所有实例同时访问JVM中该类对应的代码。

14、同一个类里面两个synchronized方法,两个线程同时访问的问题

如果synchronized修饰的是静态方法,锁的是当前类的class对象,进入同步代码前要获得当前类对象的锁;

普通方法,锁的是当前实例对象,进入同步代码前要获得的是当前实例的锁;

同步代码块,锁的是括号里面的对象,对给定的对象加锁,进入同步代码块库前要获得给定对象锁;

如果两个线程访问同一个对象的synchronized方法,会出现竞争,如果是不同对象,则不会相互影响。

15、volatile的原理

有volatile变量修饰的共享变量进行写操作的时候会多一条汇编代码,lock addl $0x0,lock前缀的指令在多核处理器下会将当前处理器缓存行的数据会写回到系统内存,这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。同时lock前缀也相当于一个内存屏障,对内存操作顺序进行了限制。

16、synchronized原理

synchronized通过对象的对象头(markword)来实现锁机制,java每个对象都有对象头,都可以为synchronized实现提供基础,都可以作为锁对象,在字节码层面synchronized块是通过插入monitorenter monitorexit完成同步的。持有monitor对象,通过进入、退出这个Monitor对象来实现锁机制。

17、谈谈NIO的理解

NIO( New Input/ Output) 引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免了在 Java 堆和 Native 堆中来回复制数据。  NIO 是一种同步非阻塞的 IO 模型。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写道缓冲区,保证 IO 的成功,而无需线程阻塞式地等待。

18、ReentrantLock 、lock、synchronized和volatile比较

1)volatile:解决变量在多个线程间的可见性,但不能保证原子性,只能用于修饰变量,不会发生阻塞。volatile能屏蔽编译指令重排,不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。多用于并行计算的单例模式。volatile规定CPU每次都必须从内存读取数据,不能从CPU缓存中读取,保证了多线程在多CPU计算中永远拿到的都是最新的值。

2)synchronized:互斥锁,操作互斥,并发线程过来,串行获得锁,串行执行代码。解决的是多个线程间访问共享资源的同步性,可保证原子性,也可间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。可用来修饰方法、代码块。会出现阻塞。synchronized发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。非公平锁,每次都是相互争抢资源。

3)lock是一个接口,而synchronized是java中的关键字,synchronized是内置语言的实现。lock可以让等待锁的线程响应中断。在发生异常时,如果没有主动通过unLock()去释放锁,则可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。

4)ReentrantLock可重入锁,锁的分配机制是基于线程的分配,而不是基于方法调用的分配。ReentrantLock有tryLock方法,如果锁被其他线程持有,返回false,可避免形成死锁。对代码加锁的颗粒会更小,更节省资源,提高代码性能。ReentrantLock可实现公平锁和非公平锁,公平锁就是先来的先获取资源。ReentrantReadWriteLock用于读多写少的场合,且读不需要互斥场景。

  • ReentrantLock的内部实现

AQS(AbstractQueuedSynchronizer):抽象队列同步器,定义了一套多线程访问共享资源的同步器框架。

  • lock原理
  • 死锁的四个必要条件?
  • 怎么避免死锁?
  • 对象锁和类锁是否会互相影响?
  • 什么是线程池,如何使用?
  • Java的并发、多线程、线程模型
  • 谈谈对多线程的理解
  • 多线程有什么要注意的问题?
  • 谈谈你对并发编程的理解并举例说明
  • 谈谈你对多线程同步机制的理解?
  • 如何保证多线程读写文件的安全?
  • 多线程断点续传原理
  • 断点续传的实现

(五)并发编程有关知识点(这个是一般Android开发用的少的,所以建议多去看看):

平时Android开发中对并发编程可以做得比较少,Thread这个类经常会用到,但是我们想提升自己的话,一定不能停留在表面,,我们也应该去了解一下java的关于线程相关的源码级别的东西。

二、Android面试题

Android面试题包括Android基础,还有一些源码级别的、原理这些等。所以想去大公司面试,一定要多看看源码和实现方式,常用框架可以试试自己能不能手写实现一下,锻炼一下自己。

(一)Android基础知识点

1、四大组件是什么

1)Activity:用户可操作的可视化界面,为用户提供一个完成操作指令的窗口。一个Activity通常是一个单独的屏幕,Activity通过Intent来进行通信。Android中会维持一个Activity Stack,当一个新Activity创建时,它就会放到栈顶,这个Activity就处于运行状态。

2)Service:服务,运行在手机后台,适合执行不需和用户交互且还需长期运行的任务。

3)ContentProvider:内容提供者,使一个应用程序的指定数据集提供给其他应用程序,其他应用可通过ContentResolver类从该内容提供者中获取或存入数据。它提供了一种跨进程数据共享的方式,当数据被修改后,ContentResolver接口的notifyChange函数通知那些注册监控特定URI的ContentObserver对象。

如果ContentProvider和调用者在同一进程中,ContentProvider的方法(query/insert/update/delete等)和调用者在同一线程中;如果ContentProvider和调用者不在同一进程,ContentProvider方法会运行在它自身进程的一个Binder线程中。

4)Broadcast Receiver: 广播接收者,运用在应用程序间传输信息,可以使用广播接收器来让应用对一个外部事件做出响应。

2、四大组件的生命周期和简单用法

1)Activity:onCreate()->onStart()->onResume()->onPause()->onStop()->onDestory()

onCreate():为Activity设置布局,此时界面还不可见;

onStart(): Activity可见但还不能与用户交互,不能获得焦点

onRestart(): 重新启动Activity时被回调

onResume(): Activity可见且可与用户进行交互

onPause(): 当前Activity暂停,不可与用户交互,但还可见。在新Activity启动前被系统调用保存现有的Activity中的持久数据、停止动画等。

onStop(): 当Activity被新的Activity覆盖不可见时被系统调用

onDestory(): 当Activity被系统销毁杀掉或是由于内存不足时调用

2)Service

a) onBind方式绑定的:onCreate->onBind->onUnBind->onDestory(不管调用bindService几次,onCreate只会调用一次,onStart不会被调用,建立连接后,service会一直运行,直到调用unBindService或是之前调用的bindService的Context不存在了,系统会自动停止Service,对应的onDestory会被调用)

b) startService启动的:onCreate->onStartCommand->onDestory(start多次,onCreate只会被调用一次,onStart会调用多次,该service会在后台运行,直至被调用stopService或是stopSelf)

c) 又被启动又被绑定的服务,不管如何调用onCreate()只被调用一次,startService调用多少次,onStart就会被调用多少次,而unbindService不会停止服务,必须调用stopService或是stopSelf来停止服务。必须unbindService和stopService(stopSelf)同时都调用了才会停止服务。

3)BroadcastReceiver

a) 动态注册:存活周期是在Context.registerReceiver和Context.unregisterReceiver之间,BroadcastReceiver每次收到广播都是使用注册传入的对象处理的。

b) 静态注册:进程在的情况下,receiver会正常收到广播,调用onReceive方法;生命周期只存活在onReceive函数中,此方法结束,BroadcastReceiver就销毁了。onReceive()只有十几秒存活时间,在onReceive()内操作超过10S,就会报ANR。

进程不存在的情况,广播相应的进程会被拉活,Application.onCreate会被调用,再调用onReceive。

4)ContentProvider:应该和应用的生命周期一样,它属于系统应用,应用启动时,它会跟着初始化,应用关闭或被杀,它会跟着结束。

3、Activity之间的通信方式

1)通过Intent方式传递参数跳转

2)通过广播方式

3)通过接口回调方式

4)借助类的静态变量或全局变量

5)借助SharedPreference或是外部存储,如数据库或本地文件

4、Activity各种情况下的生命周期

1) 两个Activity(A->B)切换(B正常的Activity)的生命周期:onPause(A)->onCreate(B)->onStart(B)->onResume(B)->oStop(A)

这时如果按回退键回退到A  onPause(B)->onRestart(A)->onStart(A)->onResume(A)->oStop(B)

如果在切换到B后调用了A.finish(),则会走到onDestory(A),这时点回退键会退出应用

2) 两个Activity(A->B)切换(B透明主题的Activity或是Dialog风格的Acivity)的生命周期:onPause(A)->onCreate(B)->onStart(B)->onResume(B)

这时如果回退到A  onPause(B)->onResume(A)->oStop(B)->onDestory(B)

3) Activity(A)启动后点击Home键再回到应用的生命周期:onPause(A)->oStop(A)->onRestart(A)->onStart(A)->onResume(A)

5、横竖屏切换的时候,Activity 各种情况下的生命周期

1)切换横屏时:onSaveInstanceState->onPause->onStop->onDestory->onCreate->onStart->onRestoreInstanceState->onResume

2) 切换竖屏时:会打印两次相同的log   

onSaveInstanceState->onPause->onStop->onDestory->onCreate->onStart->onRestoreInstanceState->onResume->onSaveInstanceState->onPause->onStop->onDestory->onCreate->onStart->onRestoreInstanceState->onResume

3) 如果在AndroidMainfest.xml中修改该Activity的属性,添加android:configChanges="orientation"

横竖屏切换,打印的log一样,同1)

4) 如果AndroidMainfest.xml中该Activity中的android:configChanges="orientation|keyboardHidden",则只会打印

onConfigurationChanged->

6、Activity与Fragment之间生命周期比较

Fragment生命周期:onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume->onPause->onStop->onDestoryView->onDestory->onDetach

切换到该Fragment:onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume

按下Power键:onPause->onSaveInstanceState->onStop

点亮屏幕解锁:onStart->onRestoreInstanceState->onResume

切换到其他Fragment: onPause->onStop->onDestoryView

切回到该Fragment: onCreateView->onActivityCreated->onStart->onResume

退出应用:onPause->onStop->onDestoryView->onDestory->onDetach

7、Activity上有Dialog的时候按Home键时的生命周期

AlertDialog并不会影响Activity的生命周期,按Home键后才会使Activity走onPause->onStop,AlertDialog只是一个组件,并不会使Activity进入后台。

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

前一个Activity的onPause,后一个Activity的onResume

9、前台切换到后台,然后再回到前台,Activity生命周期回调方法。弹出Dialog,生命值周期回调方法。

1)前台切换到后台,会执行onPause->onStop,再回到前台,会执行onRestart->onStart->onResume

2) 弹出Dialog,并不会影响Activity生命周期

10、Activity的四种启动模式对比

1)standard:标准启动模式(默认),每启动一次Activity,都会创建一个实例,即使从ActivityA startActivity ActivityA,也会再次创建A的实例放于栈顶,当回退时,回到上一个ActivityA的实例。

2) singleTop:栈顶复用模式,每次启动Activity,如果待启动的Activity位于栈顶,则不会重新创建Activity的实例,即不会走onCreate->onStart,会直接进入Activity的onPause->onNewIntent->onResume方法

3) singleInstance: 单一实例模式,整个手机操作系统里只有一个该Activity实例存在,没有其他Actvity,后续请求均不会创建新的Activity。若task中存在实例,执行实例的onNewIntent()。应用场景:闹钟、浏览器、电话

4) singleTask:栈内复用,启动的Activity如果在指定的taskAffinity的task栈中存在相应的实例,则会把它上面的Activity都出栈,直到当前Activity实例位于栈顶,执行相应的onNewIntent()方法。如果指定的task不存在,创建指定的taskAffinity的task,taskAffinity的作用,进入指写taskAffinity的task,如果指定的task存在,将task移到前台,如果指定的task不存在,创建指定的taskAffinity的task. 应用场景:应用的主页面

11、Activity状态保存于恢复

Activity被主动回收时,如按下Back键,系统不会保存它的状态,只有被动回收时,虽然这个Activity实例已被销毁,但系统在新建一个Activity实例时,会带上先前被回收Activity的信息。在当前Activity被销毁前调用onSaveInstanceState(onPause和onStop之间保存),重新创建Activity后会在onCreate后调用onRestoreInstanceState(onStart和onResume之间被调用),它们的参数Bundle用来数据保存和读取的。

保存View状态有两个前提:View的子类必须实现了onSaveInstanceState; 必须要设定Id,这个ID作为Bundle的Key

12、fragment各种情况下的生命周期

正常情况下的生命周期:onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume->onPause->onStop->onDestoryView->onDestory->onDetach

1)Fragment在Activity中replace  onPause(旧)->onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume->onStop(旧)->onDestoryView(旧)

如果添加到backStack中,调用remove()方法fragment的方法会走到onDestoryView,但不会执行onDetach(),即fragment本身的实例是存在的,成员变量也存在,但是view被销毁了。如果新替换的Fragment已在BackStack中,则不会执行onAttach->onCreate

13、Fragment状态保存onSaveInstanceState是哪个类的方法,在什么情况下使用?

在对应的FragmentActivity.onSaveInstanceState方法会调用FragmentController.saveAllState,其中会对mActive中各个Fragment的实例状态和View状态分别进行保存.当Activity在做状态保存和恢复的时候, 在它其中的fragment自然也需要做状态保存和恢复.

参考文章:Android Fragment使用(三) Activity, Fragment, WebView的状态保存和恢复

14、Fragment.startActivityForResult是和FragmentActivity的startActivityForResult?

如果希望在Fragment的onActivityResult接收数据,就要调用Fragment.startActivityForResult, 而不是Fragment.getActivity().startActivityForResult。Fragment.startActivityForResult->FragmentActivitymHost.HostCallbacks.onStartActivityFromFragment->FragmentActivity.startActivityFromFragment。如果request=-1则直接调用FragmentActivity.startActivityForResult,它会重新计算requestCode,使其大于0xfffff。

参考文章:彻底搞懂startActivityForResult在FragmentActivity和Fragment中的异同

15、如何实现Fragment的滑动?

ViewPager+FragmentPagerAdapter+List<Fragment>

16、fragment之间传递数据的方式?

1)在相应的fragment中编写方法,在需要回调的fragment里获取对应的Fragment实例,调用相应的方法;

2)采用接口回调的方式进行数据传递;

a) 在Fragment1中创建一个接口及接口对应的set方法; b) 在Fragment1中调用接口的方法;c)在Fragment2中实现该接口;

3)利用第三方开源框架EventBus

17、service和activity怎么进行数据交互?

1)通过bindService启动服务,可以在ServiceConnection的onServiceConnected中获取到Service的实例,这样就可以调用service的方法,如果service想调用activity的方法,可以在service中定义接口类及相应的set方法,在activity中实现相应的接口,这样service就可以回调接口言法;

2)通过广播方式

18、说说ContentProvider、ContentResolver、ContentObserver 之间的关系

ContentProvider实现各个应用程序间数据共享,用来提供内容给别的应用操作。如联系人应用中就使用了ContentProvider,可以在自己应用中读取和修改联系人信息,不过需要获取相应的权限。它也只是一个中间件,真正的数据源是文件或SQLite等。

ContentResolver内容解析者,用于获取内容提供者提供的数据,通过ContentResolver.notifyChange(uri)发出消息

ContentObserver内容监听者,可以监听数据的改变状态,观察特定Uri引起的数据库变化,继而做一些相应的处理,类似于数据库中的触发器,当ContentObserver所观察的Uri发生变化时,便会触发它。

19、请描述一下广播BroadcastReceiver的理解

BroadcastReceiver是一种全局监听器,用来实现系统中不同组件之间的通信。有时候也会用来作为传输少量而且发送频率低的数据,但是如果数据的发送频率比较高或者数量比较大就不建议用广播接收者来接收了,因为这样的效率很不好,因为BroadcastReceiver接收数据的开销还是比较大的。

20、广播的分类

1)普通广播:完全异步的,可以在同一时刻(逻辑上)被所有接收者接收到,消息传递的效率比较高,并且无法中断广播的传播。

2)有序广播:发送有序广播后,广播接收者将按预先声明的优先级依次接收Broadcast。优先级高的优先接收到广播,而在其onReceiver()执行过程中,广播不会传播到下一个接收者,此时当前的广播接收者可以abortBroadcast()来终止广播继续向下传播,也可以将intent中的数据进行修改设置,然后将其传播到下一个广播接收者。 sendOrderedBroadcast(intent, null);//发送有序广播

3)粘性广播:sendStickyBroadcast()来发送该类型的广播信息,这种的广播的最大特点是,当粘性广播发送后,最后的一个粘性广播会滞留在操作系统中。如果在粘性广播发送后的一段时间里,如果有新的符合广播的动态注册的广播接收者注册,将会收到这个广播消息,虽然这个广播是在广播接收者注册之前发送的,另外一点,对于静态注册的广播接收者来说,这个等同于普通广播。

21、广播使用的方式和场景

1)App全局监听:在AndroidManifest中静态注册的广播接收器,一般我们在收到该消息后,需要做一些相应的动作,而这些动作与当前App的组件,比如Activity或者Service的是否运行无关,比如我们在集成第三方Push SDK时,一般都会添加一个静态注册的BroadcastReceiver来监听Push消息,当有Push消息过来时,会在后台做一些网络请求或者发送通知等等。

2)组件局部监听:这种主要是在Activity或者Service中使用registerReceiver()动态注册的广播接收器,因为当我们收到一些特定的消息,比如网络连接发生变化时,我们可能需要在当前Activity页面给用户一些UI上的提示,或者将Service中的网络请求任务暂停。所以这种动态注册的广播接收器适合特定组件的特定消息处理。

22、在manifest 和代码中如何注册和使用BroadcastReceiver?

1)mainfest中注册:静态注册的广播接收者就是一个常驻在系统中的全局监听器,也就是说如果你应用中配置了一个静态的BroadcastReceiver,而且你安装了应用而无论应用是否处于运行状态,广播接收者都是已经常驻在系统中了。

<receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
        <action android:name="com.smilexie.test.intent.mybroadcastreceiver"/>
    </intent-filter>
</receiver>

2) 动态注册:动态注册的广播接收者只有执行了registerReceiver(receiver, filter)才会开始监听广播消息,并对广播消息作为相应的处理。

IntentFilter fiter = new IntentFilter("com.smilexie.test.intent.mybroadcastreceiver");
MyBroadcastReceiver receiver = new MyBroadcastReceiver();
registerReceiver(receiver, filter);

//撤销广播接受者的动态注册
unregisterReceiver(receiver);

23、本地广播和全局广播有什么差别?

1)LocalBroadcastReceiver仅在自己的应用内发送接收广播,也就是只有自己的应用能收到,数据更加安全。广播只在这个程序里,而且效率更高。只能动态注册,在发送和注册的时候采用LocalBroadcastManager的sendBroadcast方法和registerReceiver方法。

2)全局广播:发送的广播事件可被其他应用程序获取,也能响应其他应用程序发送的广播事件(可以通过 exported–是否监听其他应用程序发送的广播 在清单文件中控制) 全局广播既可以动态注册,也可以静态注册。

24、AlertDialog,popupWindow,Activity区别

(1)Popupwindow在显示之前一定要设置宽高,Dialog无此限制。

(2)Popupwindow默认不会响应物理键盘的back,除非显示设置了popup.setFocusable(true);而在点击back的时候,Dialog会消失。

(3)Popupwindow不会给页面其他的部分添加蒙层,而Dialog会。

(4)Popupwindow没有标题,Dialog默认有标题,可以通过dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);取消标题

(5)二者显示的时候都要设置Gravity。如果不设置,Dialog默认是Gravity.CENTER。

(6)二者都有默认的背景,都可以通过setBackgroundDrawable(new ColorDrawable(android.R.color.transparent));去掉。
(7)Popupwindow弹出后,取得了用户操作的响应处理权限,使得其他UI控件不被触发。而AlertDialog弹出后,点击背景,AlertDialog会消失。

25、Application 和 Activity 的 Context 对象的区别

1)Application Context是伴随应用生命周期;不可以showDialog, startActivity, LayoutInflation

可以startService\BindService\sendBroadcast\registerBroadcast\load Resource values

2)Activity Context指生命周期只与当前Activity有关,而Activity Context这些操作都可以,即凡是跟UI相关的,都得用Activity做为Context来处理。

一个应用Context的数量=Activity数量+Service数量+1(Application数量)

26、Android属性动画特性

1)可以对非View的对象进行动画操作;

2)补间动画只能实现移动、缩放、旋转、淡入淡出,而无法扩展,如对背景颜色进行改变等,但是属性动画就可以;

3)属性动画真正改变了对象的属性

27、如何导入外部数据库?

android系统下数据库应该存放在/data/data/com..(package name)/目录下,通过FileInputStream读取原数据库,再用FileOutputStream把读取到的东西写入到那个目录下。在原数据库放在项目源码的/res/raw/目录下,建立一个DBManager类,

28、谈谈对接口与回调的理解

原理:先创建一个对象,再创建一个控制器对象,将回调对象需要被调用的方法告诉控制器对象,控制器对象负责检查某个场景是否出现或某个条件是否满足,当满足时,自动调用回调对象方法。

29、介绍下SurfView

SurfaceView拥有独立的绘图表面Surface,它不与其宿主窗口共享一个Surface,由于拥有独立的Surface,所以SurfaceView的UI就可以在一个单独的线程中进行绘制。绘图流程:在绘图表面的基础上创建一块画布,即获得一个Canvas对象;利用Canvas类提供的绘图接口在前面获得的画布上绘制任意的UI;将已填充好了的UI数据的画布缓冲区提交给SurfaceFlinger服务,以便SurfaceFlinger服务可以将它合成到屏幕上。

优点:1)可以在非UI线程中绘制;2)SurfaceView的频率可以操作60FPS; 3) 在要求实时性比较高的游戏开发中,view的onDraw一般满足不了要求,这时只能用SurfaceView

使用步骤:

1)继承自SurfaceView; 2)初始化时,拿到SurfaceHolder,给SurfaceHolder设置Callback; 3) 在Callback中写几个回调方法surfaceCreated\surfaceChanged\surfaceDestoryed;4)在surfaceCreated中起一个线程,在线程中拿到SurfaceHolder去锁定Canvas进行绘图。

RecycleView的使用

序列化的作用,以及Android两种序列化的区别

差值器

实现Interpolator接口,根据时间来计算当前属性需要改变的百分比值。设置值的变化趋势,SDK中包含了匀速插值器LinearInterpolator、加速插值器、减速插值器DecelerateInterpolator、先加速再减速AccelerateDecelerateInterpolator、弹

  • 估值器

根据属性变化的百分比值来计算改变后的属性值。需要实现TypeEvaluatior接口

  • Android中数据存储方式

1)SharedPreferences:用来存储一些简单的配置信息,如登录的用户名、密码,采用map数据结构存储。写入的时候先调用edit()使其处于编辑态,再修改数据,最后用commit()来提交修改的数据。采用XML格式将数据存储到设备上/data/data/<package name>/shares_prefs下。只能在同一个包内使用,不能在不同包之间使用。

2) 数据库SQLite

3)SD卡,本地存储:openFileInput()和openFileOutput()

4)使用ContentProvider存储数据:可向其他应用共享数据,

(二)Android源码相关分析

1、Android属性动画实现原理

工作原理:在一定时间间隔内,通过不断对值进行改变,并不断将该值赋给对象的属性,从而实现该对象在该属性上的动画效果。

1)ValueAnimator:通过不断控制值的变化(初始值->结束值),将值手动赋值给对象的属性,再不断调用View的invalidate()方法,去不断onDraw重绘view,达到动画的效果。

主要的三种方法:

a) ValueAnimator.ofInt(int values):估值器是整型估值器IntEaluator

b) ValueAnimator.ofFloat(float values):估值器是浮点型估值器FloatEaluator

c) ValueAnimator.ofObject(ObjectEvaluator, start, end):将初始值以对象的形式过渡到结束值,通过操作对象实现动画效果,需要实现Interpolator接口,自定义估值器  

估值器TypeEvalutor,设置动画如何从初始值过渡到结束值的逻辑。插值器(Interpolator)决定值的变化模式(匀速、加速等);估值器(TypeEvalutor)决定值的具体变化数值。

// 自定义估值器,需要实现TypeEvaluator接口
public class ObjectEvaluator implements TypeEvaluator{  

// 复写evaluate(),在evaluate()里写入对象动画过渡的逻辑
    @Override  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        // 参数说明
        // fraction:表示动画完成度(根据它来计算当前动画的值)
        // startValue、endValue:动画的初始值和结束值

        ... // 写入对象动画过渡的逻辑
        
        return value;  
        // 返回对象动画过渡的逻辑计算后的值
    } 

2) ObjectAnimator:直接对对象的属性值进行改变操作,从而实现动画效果

ObjectAnimator继承自ValueAnimator类,底层的动画实现机制还是基本值的改变。它是不断控制值的变化,再不断自动赋给对象的属性,从而实现动画效果。这里的自动赋值,是通过调用对象属性的set/get方法进行自动赋值,属性动画初始值如果有就直接取,没有则调用属性的get()方法获取,当值更新变化时,通过属性的set()方法进行赋值。每次赋值都是调用view的postInvalidate()/invalidate()方法不断刷新视图(实际调用了onDraw()方法进行了重绘视图)。

//Object 需要操作的对象; propertyName 需要操作的对象的属性; values动画初始值&结束值,
//如果是两个值,则从a->b值过渡,如果是三值,则从a->b->c
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String propertyName, float ...values);

如果采用ObjectAnimator类实现动画,操作的对象的属性必须有get()和set()方法。

其他用法:

1)AnimatorSet组合动画 

AnimatorSet.play(Animator anim)   :播放当前动画
AnimatorSet.after(long delay)   :将现有动画延迟x毫秒后执行
AnimatorSet.with(Animator anim)   :将现有动画和传入的动画同时执行
AnimatorSet.after(Animator anim)   :将现有动画插入到传入的动画之后执行
AnimatorSet.before(Animator anim) :  将现有动画插入到传入的动画之前执行

2) ViewPropertyAnimator直接对属性操作,View.animate()返回的是一个ViewPropertyAnimator对象,之后的调用方法都是基于该对象的操作,调用每个方法返回值都是它自身的实例

View.animate().alpha(0f).x(500).y(500).setDuration(500).setInterpolator()

3)设置动画监听

Animation.addListener(new AnimatorListener() {
          @Override
          public void onAnimationStart(Animation animation) {
              //动画开始时执行
          }
      
           @Override
          public void onAnimationRepeat(Animation animation) {
              //动画重复时执行
          }

         @Override
          public void onAnimationCancel()(Animation animation) {
              //动画取消时执行
          }
    
          @Override
          public void onAnimationEnd(Animation animation) {
              //动画结束时执行
          }
      });

2、补间动画实现原理

主要有四种AlpahAnimation\ ScaleAnimation\ RotateAnimation\ TranslateAnimation四种,对透明度、缩放、旋转、位移四种动画。在调用View.startAnimation时,先调用View.setAnimation(Animation)方法给自己设置一个Animation对象,再调用invalidate来重绘自己。在View.draw(Canvas, ViewGroup, long)方法中进行了getAnimation(), 并调用了drawAnimation(ViewGroup, long, Animation, boolean)方法,此方法调用Animation.getTranformation()方法,再调用applyTranformation()方法,该方法中主要是对Transformation.getMatrix().setTranslate/setRotate/setAlpha/setScale来设置相应的值,这个方法系统会以60FPS的频率进行调用。具体是在调Animation.start()方法中会调用animationHandler.start()方法,从而调用了scheduleAnimation()方法,这里会调用mChoreographer.postCallback(Choregrapher.CALLBACK_ANIMATION, this, null)放入事件队列中,等待doFrame()来消耗事件。

当一个 ChildView 要重画时,它会调用其成员函数 invalidate() 函数将通知其 ParentView 这个 ChildView 要重画,这个过程一直向上遍历到 ViewRoot,当 ViewRoot 收到这个通知后就会调用ViewRoot 中的 draw 函数从而完成绘制。View::onDraw() 有一个画布参数 Canvas, 画布顾名思义就是画东西的地方,Android 会为每一个 View 设置好画布,View 就可以调用 Canvas 的方法,比如:drawText, drawBitmap, drawPath 等等去画内容。每一个 ChildView 的画布是由其 ParentView 设置的,ParentView 根据 ChildView 在其内部的布局来调整 Canvas,其中画布的属性之一就是定义和 ChildView 相关的坐标系,默认是横轴为 X 轴,从左至右,值逐渐增大,竖轴为 Y 轴,从上至下,值逐渐增大。

çªå£åæ ç³»

Android 补间动画就是通过 ParentView 来不断调整 ChildView 的画布坐标系来实现的,在ParentView的dispatchDraw方法会被调用。

dispatchDraw() 
{ 
.... 
Animation a = ChildView.getAnimation() 
Transformation tm = a.getTransformation(); 
Use tm to set ChildView's Canvas; 
Invalidate(); 
.... 
}

这里有两个类:Animation 和 Transformation,这两个类是实现动画的主要的类,Animation 中主要定义了动画的一些属性比如开始时间、持续时间、是否重复播放等,这个类主要有两个重要的函数:getTransformation 和 applyTransformation,在 getTransformation 中 Animation 会根据动画的属性来产生一系列的差值点,然后将这些差值点传给 applyTransformation,这个函数将根据这些点来生成不同的 Transformation,Transformation 中包含一个矩阵和 alpha 值,矩阵是用来做平移、旋转和缩放动画的,而 alpha 值是用来做 alpha 动画的(简单理解的话,alpha 动画相当于不断变换透明度或颜色来实现动画),调用 dispatchDraw 时会调用 getTransformation 来得到当前的 Transformation。某一个 View 的动画的绘制并不是由他自己完成的而是由它的父 view 完成。

1)补间动画TranslateAnimation,View位置移动了,可是点击区域还在原来的位置,为什么?

View在做动画是,根据动画时间的插值,计算出一个Matrix,不停的invalidate,在onDraw中的Canvas上使用这个计算出来的Matrix去draw view的内容。某个view的动画绘制并不是由它自己完成,而是由它的父view完成,使它的父view画布进行了移动,而点击时还是点击原来的画布。使得它看起来变化了。

参考文章:Android 动画框架详解,第 1 部分

3、Android各个版本API的区别

Android API版本对照表及各个版本特性简单描述

主要记住一些大版本变化:

android3.0 代号Honeycomb, 引入Fragments, ActionBar,属性动画,硬件加速

android4.0 代号Ice Cream,API14:截图功能,人脸识别,虚拟按键,3D优化驱动

android5.0 代号Lollipop API21:调整桌面图标及部件透明度等

android6.0 代号M Marshmallow API23,软件权限管理,安卓支付,指纹支持,App关联,

android7.0 代号N Preview API24,多窗口支持(不影响Activity生命周期),增加了JIT编译器,引入了新的应用签名方案APK Signature Scheme v2(缩短应用安装时间和更多未授权APK文件更改保护),严格了权限访问

android8.0 代号O  API26,取消静态广播注册,限制后台进程调用手机资源,桌面图标自适应

android9.0 代号P API27,加强电池管理,系统界面添加了Home虚拟键,提供人工智能API,支持免打扰模式

4、Requestlayout,onlayout,onDraw,DrawChild区别与联系

requestLayout()方法 :会导致调用measure()过程 和 layout()过程 。 说明:只是对View树重新布局layout过程包括measure()和layout()过程,如果view的l,t,r,b没有必变,那就不会触发onDraw;但是如果这次刷新是在动画里,mDirty非空,就会导致onDraw。

onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局)

onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)

drawChild()去重新回调每个子视图的draw()方法

5、invalidate和postInvalidate的区别及使用

View.invalidate(): 层层上传到父级,直到传递到ViewRootImpl后触发了scheduleTraversals(),然后整个View树开始重新按照View绘制流程进行重绘任务。

invalidate:在ui线程刷新view
postInvalidate:在工作线程刷新view(底层还是handler)其实它的原理就是invalidate+handler

View.postInvalidate最终会调用ViewRootImpl.dispatchInvalidateDelayed()方法

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

这里的mHandler是ViewRootHandler实例,在该Handler的handleMessage方法中调用了view.invalidate()方法。

case MSG_INVALIDATE:
                    ((View) msg.obj).invalidate();
                    break;

6、Activity-Window-View三者的差别

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

7、谈谈对Volley的理解

8、如何优化自定义View

1)在要在onDraw或是onLayout()中去创建对象,因为onDraw()方法可能会被频繁调用,可以在view的构造函数中进行创建对象;

2)降低view的刷新频率,尽可能减少不必要的调用invalidate()方法。或是调用带四种参数不同类型的invalidate(),而不是调用无参的方法。无参变量需要刷新整个view,而带参数的方法只需刷新指定部分的view。在onDraw()方法中减少冗余代码。

3)使用硬件加速,GPU硬件加速可以带来性能增加。

4)状态保存与恢复,如果因内存不足,Activity置于后台被杀重启时,View应尽可能保存自己属性,可以重写onSaveInstanceState和onRestoreInstanceState方法,状态保存。

9、低版本SDK如何实现高版本api?

使用@TargetApi注解·

当代码中有比AndroidManifest中设置的android:minSdkVersion版本更高的方法,此时编译器会提示警告,解决方法是在方法上加上@SuppressLint("NewApi")或者@TargetApi()。但它们仅是屏蔽了android lint错误,在方法中还要判断版本做不同的操作。

@SuppressLint("NewApi")屏蔽一切新api中才能使用的方法报的android lint错误

@TargetApi() 只屏蔽某一新api中才能使用的方法报的android lint错误,如@TargetApi(11)如果在方法中用了只有API14才开始有的方法,还是会报错。

10、描述一次网络请求的流程

1)域名解析

浏览器会先搜索自身DNS缓存且对应的IP地址没有过期;若未找到则搜索操作系统自身的DNS缓存;若还未找到则读本地的hotsts文件;还未找到会在TCP/IP设置的本地DNS服务器上找,如果要查询的域名在本地配置的区域资源中,则完成解析;否则根据本地DNS服务器会请求根DNS服务器;根DNS服务器是13台根DNS,会一级一级往下找。

2)TCP三次握手

客户端先发送SYN=1,ACK=0,序列号seq=x报文;(SYN在连接建立时用来同步序号,SYN=1,ACK=0代表这是一个连接请求报文,对方若同意建立连接,则应在响应报文中使SYN=1,ACK=1)

服务器返回SYN=1,ACK=1,seq=y, ack=x+1;

客户端再一次确认,但不用SYN了,回复服务端, ACK=1, seq=x+1, ack=y+1

3)建立TCP连接后发起HTTP请求

客户端按照指定的格式开始向服务端发送HTTP请求,HTTP请求格式由四部分组成,分别是请求行、请求头、空行、消息体,服务端接收到请求后,解析HTTP请求,处理完成逻辑,最后返回一个具有标准格式的HTTP响应给客户端。

4)服务器响应HTTP请求

服务器接收处理完请求后返回一个HTTP响应消息给客户端,HTTP响应信息格式包括:状态行、响应头、空行、消息体

5)浏览器解析HTML代码,请求HTML代码中的资源

浏览器拿到html文件后,就开始解析其中的html代码,遇到js/css/image等静态资源时,向服务器发起一个http请求,如果返回304状态码,浏览器会直接读取本地的缓存文件。否则开启线程向服务器请求下载。

6)浏览器对页面进行渲染并呈现给用户

7)TCP的四次挥手

当客户端没有东西要发送时就要释放连接(提出中断连接可以是Client也可以是Server),客户端会发送一个FIN=1的没有数据的报文,进入FIN_WAIT状态,服务端收到后会给客户端一个确认,此时客户端不能发送数据,但可接收信息。

11、HttpUrlConnection 和 okhttp关系

两者都可以用来实现网络请求,android4.4之后的HttpUrlConnection的实现是基于okhttp

  • Bitmap对象的理解
  • looper架构
  • ActivityThread,AMS,WMS的工作原理
  • 自定义View如何考虑机型适配

在onMeasure()的getDefaultSize()的默认实现中,当view的测量模式是AT_MOST或EXACTLY时,View的大小都会被设置成子View MeasureSpec的specSize.子view的MeasureSpec值是根据子View的布局参数和父容器的MeasureSpec值计算得来。当子view的布局参数是wrap_content时,对应的测量模式是AT_MOST,大小是parentSize,

  • 自定义View的事件
  • AstncTask+HttpClient 与 AsyncHttpClient有什么区别?
  • LaunchMode应用场景
  • AsyncTask 如何使用?
  • SpareArray原理
  • 请介绍下ContentProvider 是如何实现数据共享的?
  • AndroidService与Activity之间通信的几种方式
  • IntentService原理及作用是什么?

原理:IntentService是继承Service的一个抽象类,它在onCreate()方法中创建了一个HandlerThread,并启动该线程。HandlerThread是带有自己消息队列和Looper的线程,根据HandlerThread的looper创建一个Handler,这样IntentService的ServiceHandler的handleMessage()方法就运行在子线程中。handleMessage中调用了onHandleIntent()方法,它是一个抽象方法,继承IntentService类需要实现该方法,把耗时操作放在onHandleIntent()方法中,等耗时操作运行完成后,会调用stopSelf()方法,服务会调用onDestory()方法消毁自己。如果onHandleIntent()中的耗时操作未运行完前就调用了stopSelf()方法,服务调用onDestory()方法,但耗时操作会继续运行,直至运行完毕。如果同时多次启动IntentService,任务会放在一个队列中,onCreate()和onDestory()方法都只会运行一次。

作用:用来处理后台耗时操作,如读取数据库或是本地文件等。

  • 说说Activity、Intent、Service 是什么关系
  • ApplicationContext和ActivityContext的区别
  • SP是进程同步的吗?有什么方法做到同步?
  • 谈谈多线程在Android中的使用
  • 进程和 Application 的生命周期
  • 封装View的时候怎么知道view的大小
  • RecycleView原理
  • AndroidManifest的作用与理解

(三)常见的一些原理性问题

  • Handler机制和底层实现
  • Handler、Thread和HandlerThread的差别

1)Handler线程的消息通讯的桥梁,主要用来发送消息及处理消息。

2)Thread普通线程,如果需要有自己的消息队列,需要调用Looper.prepare()创建Looper实例,调用loop()去循环消息。

3)HandlerThread是一个带有Looper的线程,在HandleThread的run()方法中调用了Looper.prepare()创建了Looper实例,并调用Looper.loop()开启了Loop循环,循环从消息队列中获取消息并交由Handler处理。利用该线程的Looper创建Handler实例,此Handler的handleMessage()方法是运行在子线程中的。即Handler利用哪个线程的Looper创建的实例,它就和相应的线程绑定到一起,处理该线程上的消息,它的handleMessage()方法就是在那个线程中运行的,无参构造默认是主线程。HandlerThread提供了quit()/quitSafely()方法退出HandlerThread的消息循环,它们分别调用Looper的quit和quitSafely方法,quit会将消息队列中的所有消息移除,而quitSafely会将消息队列所有延迟消息移除,非延迟消息派发出去让Handler去处理。

HandlerThread适合处理本地IO读写操作(读写数据库或文件),因为本地IO操作耗时不长,对于单线程+异步队列不会产生较大阻塞,而网络操作相对比较耗时,容易阻塞后面的请求,因此HandlerThread不适合加入网络操作。

  • handler发消息给子线程,looper怎么启动?
  • 关于Handler,在任何地方new Handler 都是什么线程下?
  • ThreadLocal原理,实现及如何保证Local属性?
  • 请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系
  • 请描述一下View事件传递分发机制
  • Touch事件传递流程
  • 事件分发中的onTouch 和onTouchEvent 有什么区别,又该如何使用?
  • View和ViewGroup分别有哪些事件分发相关的回调方法
  • View刷新机制
  • View绘制流程
  • 自定义控件原理
  • 自定义View如何提供获取View属性的接口?
  • Android代码中实现WAP方式联网
  • AsyncTask机制
  • AsyncTask原理及不足
  • 如何取消AsyncTask?
  • 为什么不能在子线程更新UI?
  • ANR产生的原因是什么?
  • ANR定位和修正
  • oom是什么?

oom(Out Of Memory)内存溢出,

  • 什么情况导致oom?
  • 有什么解决方法可以避免OOM?
  • Oom 是否可以try catch?为什么?

可以,当

  • 内存泄漏是什么?

内存泄露就是指该被GC垃圾回收的,但被一个生命周期比它长的对象仍然在引用它,导致无法回收,造成内存泄露,过多的内存泄露会导致OOM。

  • 什么情况导致内存泄漏?

1)非静态内部类、匿名内部类:非静态内部类、匿名内部类 都会持有外部类的一个引用,如果有一个静态变量引用了非静态内部类或者匿名内部类,导致非静态内部类或者匿名内部类的生命周期比外部类(Activity)长,就会导致外部类在该被回收的时候,无法被回收掉,引起内存泄露, 除非外部类被卸载。

解决办法:将非静态内部类、匿名内部类 改成静态内部类,或者直接抽离成一个外部类。 如果在静态内部类中,需要引用外部类对象,那么可以将这个引用封装在一个WeakReference中。
2)静态的View:当一个Activity经常启动,但是对应的View读取非常耗时,我们可以通过静态View变量来保持对该Activity的rootView引用。这样就可以不用每次启动Activity都去读取并渲染View了。但View attach到我们的Window上,就会持有一个Context(即Activity)的引用。而我们的View有事一个静态变量,所以导致Activity不被回收。

解决办法: 在使用静态View时,需要确保在资源回收时,将静态View detach掉
3)Handler:在Activity中定义Handler对象,那么Handler持有Activty的引用。而每个Message对象是持有Handler的引用的(Message对象的target属性持有Handler引用),从而导致Message间接引用到了Activity。如果在Activty destroy之后,消息队列中还有Message对象,Activty是不会被回收的。
解决办法: 将Handler放入单独的类或者将Handler放入到静态内部类中(静态内部类不会持有外部类的引用)。如果想要在handler内部去调用所在的外部类Activity,可以在handler内部使用弱引用的方式指向所在Activity,在onDestory时,调用相应的方法移除回调和删除消息。
4)监听器(各种需要注册的Listener,Watcher等):当我们需要使用系统服务时,比如执行某些后台任务、为硬件访问提供接口等等系统服务。我们需要把自己注册到服务的监听器中。然而,这会让服务持有 activity 的引用,如果程序员忘记在 activity 销毁时取消注册,那就会导致 activity 泄漏了。

解决办法:在onDestory中移除注册
5. 资源对象没关闭造成内存泄漏:当我们打开资源时,一般都会使用缓存。比如读写文件资源、打开数据库资源、使用Bitmap资源等等。当我们不再使用时,应该关闭它们,使得缓存内存区域及时回收。

解决办法:使用try finally结合,在try块中打开资源,在finally中关闭资源
6. 属性动画:在使用ValueAnimator或者ObjectAnimator时,如果没有及时做cancel取消动画,就可能造成内存泄露。因为在cancel方法里,最后调用了endAnimation(); ,在endAnimation里,有个AnimationHandler的单例,会持有属性动画对象的引用。

解决办法:在onDestory中调用动画的cancel方法

7. RxJava:在使用RxJava时,如果在发布了一个订阅后,由于没有及时取消,导致Activity/Fragment无法销毁,导致的内存泄露。

解决办法:使用RxLifeCycle

  • 内存泄漏和内存溢出区别?

(1)内存泄漏

1)内存泄漏:指程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统奔溃等严重后果。

2)一次内存泄漏似乎不会有大的影响,但内存泄漏后堆积的结果就是内存溢出。

3)内存泄漏具有隐蔽性,积累性的特征,比其他内存非法访问错误更难检测。这是因为内存泄漏产生的原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏不会直接产生可观察的错误,而是逐渐积累,降低系统的整体性性能。

4)如何有效的进行内存分配和释放,防止内存泄漏,是软件开发人员的关键问题,比如一个服务器应用软件要长时间服务多个客户端,若存在内存泄漏,则会逐渐堆积,导致一系列严重后果。

(2)内存溢出

指程序在申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,就会导致内存不够用,报错OOM,即出现内存溢出的错误。

  • LruCache默认缓存大小

4MB

  • ContentProvider的权限管理(解答:读写分离,权限控制-精确到表级,URL控制)
  • 如何通过广播拦截和abort一条短信?
  • 广播是否可以请求网络?
  • 广播引起anr的时间限制是多少?
  • 计算一个view的嵌套层级
  • Activity栈
  • Android线程有没有上限?

理论上是没有上限的,但按一般写法一般是线程最多开到2*CPU个数+1

  • 线程池有没有上限?

要根据用户调用不同的线程池构造函数。

  • ListView重用的是什么?
  • Android为什么引入Parcelable?
  • 有没有尝试简化Parcelable的使用?

(四)开发中常见的一些问题

  • ListView 中图片错位的问题是如何产生的?
  • 混合开发有了解吗?
  • 知道哪些混合开发的方式?说出它们的优缺点和各自使用场景?(解答:比如:RN,weex,H5,小程序,WPA等。做Android的了解一些前端js等还是很有好处的);
  • 屏幕适配的处理技巧都有哪些?
  • 服务器只提供数据接收接口,在多线程或多进程条件下,如何保证数据的有序到达?
  • 动态布局的理解
  • 怎么去除重复代码?
  • 画出 Android 的大体架构图
  • Recycleview和ListView的区别
  • ListView图片加载错乱的原理和解决方案

ListView item缓存机制:为了使得性能更优,ListView会缓存行item(某行对应的View)。ListView通过adapter的getView函数获得每行的item。滑动过程中,1)如果某行item已经滑出屏幕,若该item不在缓存内,则put进缓存,否则更新缓存;
2)获取滑入屏幕的行item之前会先判断缓存中是否有可用的item,如果有,做为convertView参数传递给adapter的getView。

出现的问题:

1)行item图片显示重复,当前行item显示了之前某行item的图片。
比如ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行,且滑动过程中该图片加载结束,第2行已不在屏幕内,根据上面介绍的缓存原理,第2行的view可能被第14行复用,这样我们看到的就是第14行显示了本该属于第2行的图片,造成显示重复。

2)行item图片显示闪烁
如果第14行图片又很快加载结束,所以我们看到第14行先显示了第2行的图片,立马又显示了自己的图片进行覆盖造成闪烁错乱。

解决方法
通过上面的分析我们知道了出现错乱的原因是异步加载及对象被复用造成的,如果每次getView能给对象一个标识,在异步加载完成时比较标识与当前行item的标识是否一致,一致则显示,否则不做处理即可。

  • 动态权限适配方案,权限组的概念
  • Android系统为什么会设计ContentProvider?
  • 下拉状态栏是不是影响activity的生命周期
  • 如果在onStop的时候做了网络请求,onResume的时候怎么恢复?
  • Bitmap 使用时候注意什么?
  • Bitmap的recycler()
  • Android中开启摄像头的主要步骤
  • ViewPager使用细节,如何设置成每次只初始化当前的Fragment,其他的不初始化?
  • 点击事件被拦截,但是想传到下面的View,如何操作?
  • 微信主页面的实现方式
  • 微信上消息小红点的原理
  • CAS介绍

三、混合开发面试题

大厂除了技术深度之外,还要求你具备一些广度的知识,比如你要会前端知识,会混合开发,至少会一种脚本语言,C c++更不用说了,也是必会的。

  • Hybrid做过吗?
  • Hybrid通信原理是什么,有做研究吗?
  • react native有多少了解?讲一下原理。
  • weex了解吗?如何自己实现类似技术?
  • flutter了解吗?内部是如何实现跨平台的?
  • Dart语言有研究贵吗?
  • 快应用了解吗?跟其她方式相比有什么优缺点?
  • 说说你用过的混合开发技术有哪些?各有什么优缺点?
  • Python会吗?
  • 会不会PHP?
  • Gradle了解多少?groovy语法会吗?

四、高端技术面试题

这里讲的是大公司需要用到的一些高端Android技术,这里专门整理了一个文档,希望大家都可以看看。这些题目有点技术含量,需要好点时间去研究一下的。

(一)图片

1、图片库对比

2、LRUCache原理

LruCache是个泛型类,主要原理是:把最近使用的对象用强引用存储在LinkedHashMap中,当缓存满时,把最近最少使用的对象从内存中移除,并提供get/put方法完成缓存的获取和添加。LruCache是线程安全的,因为使用了synchronized关键字。

当调用put()方法,将元素加到链表头,如果链表中没有该元素,大小不变,如果没有,需调用trimToSize方法判断是否超过最大缓存量,trimToSize()方法中有一个while(true)死循环,如果缓存大小大于最大的缓存值,会不断删除LinkedHashMap中队尾的元素,即最少访问的,直到缓存大小小于最大缓存值。当调用LruCache的get方法时,LinkedHashMap会调用recordAccess方法将此元素加到链表头部。

3、图片加载原理

4、自己去实现图片库,怎么做?

5、Glide源码解析

1)Glide.with(context)创建了一个RequestManager,同时实现加载图片与组件生命周期绑定:在Activity上创建一个透明的ReuqestManagerFragment加入到FragmentManager中,通过添加的Fragment感知Activty\Fragment的生命周期。因为添加到Activity中的Fragment会跟随Activity的生命周期。在RequestManagerFragment中的相应生命周期方法中通过liftcycle传递给在lifecycle中注册的LifecycleListener

2)RequestManager.load(url) 创建了一个RequestBuilder<T>对象 T可以是Drawable对象或是ResourceType等

3) RequestBuilder.into(view)

-->into(glideContext.buildImageViewTarget(view, transcodeClass))返回的是一个DrawableImageViewTarget, Target用来最终展示图片的,buildImageViewTarget-->ImageViewTargetFactory.buildTarget()根据传入class参数不同构建不同的Target对象,这个Class是根据构建Glide时是否调用了asBitmap()方法,如果调用了会构建出BitmapImageViewTarget,否则构建的是GlideDrawableImageViewTarget对象。

-->GenericRequestBuilder.into(Target),该方法进行了构建Request,并用RequestTracker.runRequest()

Request request = buildRequest(target);//构建Request对象,Request是用来发出加载图片的,它调用了buildRequestRecursive()方法以,内部调用了GenericRequest.obtain()方法
target.setRequest(request);
lifecycle.addListener(target);
requestTracker.runRequest(request);//判断Glide当前是不是处于暂停状态,若不是则调用Request.begin()方法来执行Request,否则将Request添加到待执行队列里,等暂停态解除了后再执行

-->GenericRequest.begin()

1)onSizeReady()--> Engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
            priority, isMemoryCacheable, diskCacheStrategy, this) --> a)先构建EngineKey; b) loadFromCache从缓存中获取EngineResource,如果缓存中获取到cache就调用cb.onResourceReady(cached); c)如果缓存中不存在调用loadFromActiveResources从active中获取,如果获取到就调用cb.onResourceReady(cached);d)如果active中也不存在,调用EngineJob.start(EngineRunnable), 从而调用decodeFromSource()/decodeFromCache()-->如果是调用decodeFromSource()-->ImageVideoFetcher.loadData()-->HttpUrlFetcher()调用HttpUrlConnection进行网络请求资源-->得于InputStream()后,调用decodeFromSourceData()-->loadProvider.getSourceDecoder().decode()方法解码-->GifBitmapWrapperResourceDecoder.decode()-->decodeStream()先从流中读取2个字节判断是GIF还是普通图,若是GIF调用decodeGifWrapper()来解码,若是普通静图则调用decodeBitmapWrapper()来解码-->bitmapDecoder.decode()

6、Glide使用什么缓存?

1) 内存缓存:LruResourceCache(memory)+弱引用activeResources

Map<Key, WeakReference<EngineResource<?>>> activeResources正在使用的资源,当acquired变量大于0,说明图片正在使用,放到activeResources弱引用缓存中,经过release()后,acquired=0,说明图片不再使用,会把它放进LruResourceCache中

2)磁盘缓存:DiskLruCache,这里分为Source(原始图片)和Result(转换后的图片)

第一次获取图片,肯定网络取,然后存active\disk中,再把图片显示出来,第二次读取相同的图片,并加载到相同大小的imageview中,会先从memory中取,没有再去active中获取。如果activity执行到onStop时,图片被回收,active中的资源会被保存到memory中,active中的资源被回收。当再次加载图片时,会从memory中取,再放入active中,并将memory中对应的资源回收。

之所以需要activeResources,它是一个随时可能被回收的资源,memory的强引用频繁读写可能造成内存激增频繁GC,而造成内存抖动。资源在使用过程中保存在activeResources中,而activeResources是弱引用,随时被系统回收,不会造成内存过多使用和泄漏。

7、Glide内存缓存如何控制大小?

Glide内存缓存最大空间(maxSize)=每个进程可用最大内存*0.4(低配手机是   每个进程可用最大内存*0.33)

磁盘缓存大小是250MB   int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;

(二)网络和安全机制

  • 网络框架对比和源码分析
  • 自己去设计网络请求框架,怎么做?
  • okhttp源码
  • 网络请求缓存处理,okhttp如何处理网络缓存的

(1)网络缓存优先考虑强制缓存,再考虑对比缓存

  • 首先判断强制缓存中的数据的是否在有效期内。如果在有效期,则直接使用缓存。如果过了有效期,则进入对比缓存。
  • 在对比缓存过程中,判断ETag是否有变动,如果服务端返回没有变动,说明资源未改变,使用缓存。如果有变动,判断Last-Modified。
  • 判断Last-Modified,如果服务端对比资源的上次修改时间没有变化,则使用缓存,否则重新请求服务端的数据,并作缓存工作。

(2)okhttp缓存

开启使用Okhttp的缓存其实很简单,只需要给OkHttpClient对象设置一个Cache对象即可,创建一个Cache时指定缓存保存的目录和缓存最大的大小即可。

//新建一个cache,指定目录为外部目录下的okhttp_cache目录,大小为100M
Cache cache = new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 100 * 1024 * 1024);
//将cache设置到OkHttpClient中,这样缓存就开始生效了。
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();

相关的类有:

1)CacheControl( HTTP中的Cache-Control和Pragma缓存控制):指定缓存规则

2)Cache(缓存类)

3)DiskLruCache(文件化的LRU缓存类)

(1)读取缓存:先获限OkHttpClient的Cache缓存对象,就是上面创建OkHttpClient设置的Cahce; 传Request请求到Cache的get方法查找缓存响应数据Response;构造一个缓存策略,再调用它的get去决策使用网络请求还是缓存响应。若使用缓存,它的cacheResponse不为空,networkRequest为空,用缓存构造响应直接返回。若使用请求,则cacheResponse为空,networkRequest不为空,开始网络请求流程。

Cache的get获取缓存方法,计算request的key值(请求url进行md5加密),根据key值去DisLruCache查找是否存在缓存内容,存则则创建绘存Entry实体。ENTRY_METADATA代表响应头信息,ENTRY_BODY代表响应体信息。如果缓存存在,在指定目录下会有两个文件****.0    *****.1分别存储某个请求缓存响应头和响应体信息。

CacheStrategy的get方法:1)若缓存响应为空或 2)请求是https但缓存响应没有握手信息;3)请求和缓存响应都是不可缓存的;4)请求是onCache,并且又包含if-Modified-Since或If-None-Match则不使用缓存; 再计算请求有效时间是否符合响应的过期时间,若响应在有效范围内,则缓存策略使用缓存,否则创建一个新的有条件的请求,返回有条件的缓存策略。

(2)存储缓存流程:从HttpEngine的readResponse()发送请求开始,判断hasBody(userResponse),如果缓存的话,maybeCache()缓存响应头信息,unzip(cacheWritingResponse(storeRequest, userResponse))缓存响应体。

  •  
  • 从网络加载一个10M的图片,说下注意事项
  • TCP的3次握手和四次挥手
  • TCP与UDP的区别
  • TCP与UDP的应用
  • HTTP协议
  • HTTP1.0与2.0的区别
  • HTTP报文结构
  • HTTP与HTTPS的区别以及如何实现安全性
  • 如何验证证书的合法性?
  • https中哪里用了对称加密,哪里用了非对称加密,对加密算法(如RSA)等是否有了解?
  • client如何确定自己发送的消息被server收到?
  • 谈谈你对WebSocket的理解
  • WebSocket与socket的区别
  • 谈谈你对安卓签名的理解。
  • 请解释安卓为啥要加签名机制?
  • 视频加密传输
  • App 是如何沙箱化,为什么要这么做?
  • 权限管理系统(底层的权限是如何进行 grant 的)?

(三)数据库

  • sqlite升级,增加字段的语句
  • 数据库框架对比和源码分析
  • 数据库的优化
  • 数据库数据迁移问题

(四)算法

  • 排序算法有哪些?
  • 最快的排序算法是哪个?
  • 手写一个冒泡排序
  • 手写快速排序代码
  • 快速排序的过程、时间复杂度、空间复杂度
  • 手写堆排序
  • 堆排序过程、时间复杂度及空间复杂度
  • 写出你所知道的排序算法及时空复杂度,稳定性
  • 二叉树给出根节点和目标节点,找出从根节点到目标节点的路径
  • 给阿里2万多名员工按年龄排序应该选择哪个算法?
  • GC算法(各种算法的优缺点以及应用场景)
  • 蚁群算法与蒙特卡洛算法
  • 子串包含问题(KMP 算法)写代码实现
  • 一个无序,不重复数组,输出N个元素,使得N个元素的和相加为M,给出时间复杂度、空间复杂度。手写算法
  • 万亿级别的两个URL文件A和B,如何求出A和B的差集C(提示:Bit映射->hash分组->多文件读写效率->磁盘寻址以及应用层面对寻址的优化)
  • 百度POI中如何试下查找最近的商家功能(提示:坐标镜像+R树)。
  • 两个不重复的数组集合中,求共同的元素。
  • 两个不重复的数组集合中,这两个集合都是海量数据,内存中放不下,怎么求共同的元素?
  • 一个文件中有100万个整数,由空格分开,在程序中判断用户输入的整数是否在此文件中。说出最优的方法
  • 一张Bitmap所占内存以及内存占用的计算

一张图片(bitmap)占用的内存影响因素:图片原始长、宽,手机屏幕密度,图片存放路径下的密度,单位像素占用字节数

bitmapSize=图片长度*(inTargetDensity手机的density / inDensity图片存放目录的density)*宽度*(手机的inTargetDensity / inDensity目标存放目录的density)*单位像素占用的字节数(图片长宽单位是像素)

1)图片长宽单位是像素:单位像素字节数由其参数BitmapFactory.Options.inPreferredConfig变量决定,它是Bitmap.Config类型,包括以下几种值:ALPHA_8图片只有alpha值,占用一个字节;ARGB_4444 一个像素占用2个字节,A\R\G\B各占4bits;ARGB_8888一个像素占用4个字节,A\R\G\B各占8bits(高质量图片格式,bitmap默认格式);ARGB_565一个像素占用2字节,不支持透明和半透明,R占5bit, Green占6bit, Blue占用5bit. 从Android4.0开始该项无效。

2) inTargetDensity 手机的屏幕密度(跟手机分辨率有关系)

inDensity原始资源密度(mdpi:160;   hdpi:240;   xhdpi:320;   xxhdpi:480; xxxhdpi:640)

当Bitmap对象在不使用时,应该先调用recycle(),再将它设置为null,虽然Bitmap在被回收时可通过BitmapFinalizer来回收内存。但只有系统垃圾回收时才会回收。Android4.0之前,Bitmap内存分配在Native堆中,Android4.0开始,Bitmap的内存分配在dalvik堆中,即Java堆中,调用recycle()并不能立即释放Native内存。

  • 2000万个整数,找出第五十大的数字?
  • 烧一根不均匀的绳,从头烧到尾总共需要1个小时。现在有若干条材质相同的绳子,问如何用烧绳的方法来计时一个小时十五分钟呢?
  • 求1000以内的水仙花数以及40亿以内的水仙花数
  • 5枚硬币,2正3反如何划分为两堆然后通过翻转让两堆中正面向上的硬8币和反面向上的硬币个数相同
  • 时针走一圈,时针分针重合几次
  • N*N的方格纸,里面有多少个正方形
  • x个苹果,一天只能吃一个、两个、或者三个,问多少天可以吃完?

(五)插件化、模块化、组件化、热修复、增量更新、Gradle

  • 对热修复和插件化的理解
  • 插件化原理分析
  • 模块化实现(好处,原因)
  • 热修复,插件化
  • 项目组件化的理解
  • 描述清点击 Android Studio 的 build 按钮后发生了什么

(六)架构设计和设计模式

  • 谈谈你对Android设计模式的理解
  • MVC MVP MVVM原理和区别
  • 你所知道的设计模式有哪些?
  • 项目中常用的设计模式
  • 手写生产者/消费者模式
  • 写出观察者模式的代码
  • 适配器模式,装饰者模式,外观模式的异同?
  • 用到的一些开源框架,介绍一个看过源码的,内部实现过程。
  • 谈谈对RxJava的理解

RxJava是基于响应式编程,基于事件流、实现异步操(类似于Android中的AsyncTask、Handler作用)作的库,基于事件流的链式调用,使得RxJava逻辑简洁、使用简单。RxJava原理是基于一种扩展的观察者模式,有四种角色:被观察者Observable 观察者Observer 订阅subscribe 事件Event。RxJava原理可总结为:被观察者Observable通过订阅(subscribe)按顺序发送事件(Emitter)给观察者(Observer), 观察者按顺序接收事件&作出相应的响应动作。

RxJava中的操作符:

1)defer():直到有观察者(Observer)订阅时,才会动态创建被观察者对象(Observer)&发送事件,通过Observer工厂方法创建被观察者对象,每次订阅后,都会得到一个刚创建的最新的Observer对象,可以确保Observer对象里的数据是最新的。defer()方法只会定义Observable对象,只有订阅操作才会创建对象。

Observable<T> observable = Observable.defer(new Callable<ObservableSource<? extends T>>() {
    @Override
    public ObservableSource<? extends T> call() throws Exception {
        return Observable.just();
    }
}

2)timer() 快速创建一个被观察者(Observable),延迟指定时间后,再发送事件

Observable.timer(2, TimeUnit.SECONDS)//也可以自定义线程timer(long, TimeUnit, Scheduler)
    .subscribe(new Observer<Long>() {
        @Override
        public void onSubscribe(Disposable d) {
        }
        ...

     });

3) interval() intervalRange() 快速创建一个被观察者对象(Observable),每隔指定时间就发送事件

//interval三个参数,参数1:第一次延迟时间  参数2:间隔时间数字   参数3:时间单位
Observable.interval(3, 1, TimeUnit.SECONDS).subscribe();
//intervalRange五个参数,参数1:事件序列起始点  参数2:事件数量  参数3:第一次延迟时间 参数4:间隔时间数字   参数5:时间单位
Observable.intervalRange(3, 10, 2, 1, TimeUnit.SECONDS).subscribe();
  • RxJava的功能与原理实现

Rxjava发送事件步骤:

1)创建被观察者对象Observable&定义需要发送的事件

Observable.create(new ObservableOnSubscribe<T>(){
    @Override
    public void subscribe(ObservableEmitter<T> emitter) throws Exception {
        //定义发送事件的行为
    }
});

Observable.create()方法实际创建了一个ObservableCreate对象,它是Observable的子类,传入一个ObservableOnSubscribe对象,复写了发送事件行为的subscribe()方法。

2)创建观察者对象Observer&定义响应事件的行为

Observer observer = new Observer<T>() {

    @Override
    public void onSubscribe(Disposable d){//Disposable对象可用于结束事件
        //默认最先调用
    }
    
    @Override
    public void onNext(T t){
    
    }

    @Override
    public void onError(Throwable d){
    
    }

    @Override
    public void onComplete(){
    
    }

}

3)通过subscribe()方法使观察者订阅被观察者

Observable.subscribe(Observer observer);//实际调用的是ObservableCreate.subscribeActual()方法,具体实现如下

protected void subscribeActual(Observer<? super T> observer) {

              // 1. 创建1个CreateEmitter对象用于发射事件(封装成1个Disposable对象)
            CreateEmitter<T> parent = new CreateEmitter<T>(observer);
            // 2. 调用观察者(Observer)的onSubscribe()
            observer.onSubscribe(parent);
            try {
                // 3. 调用source对象的(ObservableOnSubscribe对象)subscribe()
                source.subscribe(parent);
            } catch (Throwable ex) {
                Exceptions.throwIfFatal(ex);
                parent.onError(ex);
            }
    }
  • RxJava的作用,与平时使用的异步操作来比的优缺点
  • 说说EventBus作用,实现方式,代替EventBus的方式
  • 从0设计一款App整体架构,如何去做?
  • 说一款你认为当前比较火的应用并设计(比如:直播APP,P2P金融,小视频等)
  • 谈谈对java状态机理解
  • Fragment如果在Adapter中使用应该如何解耦?
  • Binder机制及底层实现
  • 对于应用更新这块是如何做的?(解答:灰度,强制更新,分区域更新)?
  • 实现一个Json解析器(可以通过正则提高速度)
  • 统计启动时长,标准

(七)性能优化

  • 如何对Android 应用进行性能分析以及优化?
  • ddms 和 traceView
  • 性能优化如何分析systrace?
  • 用IDE如何分析内存泄漏?
  • Java多线程引发的性能问题,怎么解决?
  • 启动页白屏及黑屏解决?
  • 启动太慢怎么解决?

在冷启动系统要执行三个任务:加载和启动应用程序;在app启动后立即显示一个空白窗体;创建APP进程。

应用启动后会执行:创建应用程序对象;启动主线程;创建Main Activity;初始化构造View;在屏幕上布局;执行初始化绘制操作;

应用启动,空白窗口会一直存在直到系统完成了应用的首次绘制操作,此时,系统会替换启动窗口,让用户能和APP进行交互。

对于热启动,如果应用的Activity驻留在内存中,应用就可避免重复进行对象初始化。如果系统执行了内存回收并触发GC,如onTrimMemory(),热启动时对象仍需重建,这样系统进程也会一直显示白屏直到应用完成Activity的渲染。

测量应用启动时间:1) 可通过logcat中查看Displayed中显示启动类耗时; 2) 通过adb shell am start -S -W 包名/启动类全限定名,-S表示重启当前应用,命令进行检测启动app的耗时。3) 使用reportFullyDrawn()方法来测量应用启动到所有资源和视图层次结构完整显示之间的所经过的时间。

优化方法:1)减少view的层级,减少复用和布局嵌套使布局扁平化,不要加载对用户不可见的布局,如使用ViewStub;

2)将需要在主线程中初始化但可不立即完成的延迟加载,部分组件放到子线程中初始化。

3)减少Application.onCreate和启动页和第一个界面onCreate中方法的耗时操作。

4)设置闪屏页,将闪屏页设置为启动页的activity窗口背景windowBackground属性,为启动屏幕提供一个背景,

  • 怎么保证应用启动不卡顿?

应用卡顿的主要原因有:速度曲线不够流畅,掉帧、触摸响应速度

Android显示机制:app->SurfaceFlinger->Display

开发者选项->GPU呈现模式分析->在屏幕上显示为条形图,可以在屏幕上看到显示每帧绘制花费的时间,有条基准线是16ms,超过这条基线很可能出现掉帧。如果蓝线很长,则说明软件draw太费时,可通过traceview来继续分析draw的java代码。如果中间红色部分很长则说明是openGL ES绘制太费时,用gltrace来分析OpernGL ES调用过程。用systrace分析,用traceview来看代码,

  • App启动崩溃异常捕捉
  • 自定义View注意事项
  • 现在下载速度很慢,试从网络协议的角度分析原因,并优化(提示:网络的5层都可以涉及)。
  • Https请求慢的解决办法(提示:DNS,携带数据,直接访问IP)
  • 如何保持应用的稳定性

1)需求明确清楚,编码时明确知道要实现的功能和实现方法,技术选型等,对一些库进行封装再使用。防止代码冗余、避免多线程导致的问题;

2)异常崩溃处理捕获,在使用对象前做判空处理等

3)提高编码质量,用lint\findbugs进行代码静态分析;

4)OOM和内存泄漏检测

5)框架测试,兼容性测试、单元测试、monkey测试

6)发布维度,灰度,选择部分渠道进行发布,收集问题;

7)热更新。

  • RecyclerView和ListView的性能对比
  • ListView的优化
  • RecycleView优化
  • View渲染
  • Bitmap如何处理大图,如一张30M的大图,如何预防OOM
  • java中的四种引用的区别以及使用场景
  • 强引用置为null,会不会被回收?

如果对象没有被GC Roots引用到,会被GC回收到,但什么时候回收需根据JVM的特性什么时候触发GC操作。即使调用了System.gc() JVM也不一定会触发GC。

(八)NDK、jni、Binder、AIDL、进程通信有关

  • 请介绍一下NDK
  • 什么是NDK库?
  • jni用过吗?
  • 如何在jni中注册native函数,有几种注册方式?
  • Java如何调用c、c++语言?
  • jni如何调用java层代码?

1)Java类中要调用jni方法,需要在java类中声明本地方法,public native void methodName();//本地方法。还要在类的静态代码块中导入so库 static { System.loadLibrary("MyJni");}

2)在C/C++获取类的对象的方法有两种:

a)通过c/c++创建java对象,通过对象获取类,通过类获取类的构造方法的ID,基于方法ID和类,创建新对象。

JNIEXPORT void JNICALL JAVA_nativeMethod(JNIEnv *env, jobject this, jint i) {
    jclass clazz = (*env).GetObjectClass(thiz);
    jmethodID mid = (*env).GetMethodID(clazz, "<init>","()V");
    jobject obj = (*env).NewObject(clazz, mid);
}

b) 通过c/c++创建不同类对象,通过FindClass方法获取需要的类;通过类获取类的构造方法的ID,基于方法ID和类,创建对象

JNIEXPORT void JNICALL JAVA_nativeMethod(JNIEnv *env, jobject this, jint i) {
    jclass clazz = (*env).FindClass("com/packagepath/className");
    jmethodID mid = (*env).GetMethodID(clazz, "<init>","()V");
    jobject obj = (*env).NewObject(clazz, mid);
}

调用java方法跟上面调用构造函数类似,获取类的方法ID,基于对象的方法id调用Java方法

JNIEXPORT void JNICALL JAVA_nativeMethod(JNIEnv *env, jobject thiz, jint i) {
    jclass clazz = (*env).GetObjectClass(thiz);
    m_Object = (*env).NewGlobalRef(thiz);
    m_mid = (*env).GetMethodID(clazz, "methodName", "()V");//获取Java方法的ID
    m_fid = (*env).GetFieldID(clazz, "a","I");//获取Java变量的ID
    (*env).SetIntField(m_Object, m_fid, i);
    (*env).CallVoidMethod(m_Object, m_mid);
}
  • 进程间通信的方式?
  • Binder机制
  • 简述IPC?
  • 什么是AIDL?
  • AIDL解决了什么问题?
  • AIDL如何使用?
  • Android 上的 Inter-Process-Communication 跨进程通信时如何工作的?
  • 多进程场景遇见过么?
  • Android进程分类?

前台进行(当前正在前台运行的进程,说明用户当前正在与该进程交互), 满足以下至少一个条件的叫做 foreground progcess:
  a.有一个Activity在前台获得焦点可与用户互动
  b.有一个 BroadcastReceiver组件正在运行onReceive()方法
  c.有一个Sevice组件正在运行onCreate()/onStart()/onDestory()方法

可见进程(可见,但用户不能直接与之交互)满足以下条件之一称为可见进程:a.有一个Activity能被用户看见但是失去焦点(处于onPause()状态) b.有一个 Service调用了startForeground()方法 c.绑定了一个Service,系统将该Service作为一个特殊的用户知道的功能使用如自动更换壁纸,输入法服务等。

服务进程(拥有service的进程,一般在后台为用户服务的),通过startService()方法开启的没有绑定在activity上的Service的进程,Service长时间运行 (超过30分钟以上)会被降级到cached process

后台进程(对用户作用不大,缺少该进程一般不会影响用户对系统的体验)

空进程(一般作为缓存机制服务的)

  • 进程和 Application 的生命周期?
  • 进程调度
  • 谈谈对进程共享和线程安全的认识

Android进程共享可通过共享用户ID来实现,

对于SharedPreferences想实现多进程共享需要设置MODE_MULTI_PROCESS,设置了这个Flag后,每次调用Context.getSharedPreferences时系统会重新从SP

SharedPreferences myPrefs = context.getSharedPreferences(MY_FILE_NAME, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE);
  • 谈谈对多进程开发的理解以及多进程应用场景
  • 什么是协程?

(九)framework层、ROM定制、Ubuntu、Linux之类的问题

  • java虚拟机的特性
  • 谈谈对jvm的理解
  • JVM内存区域,开线程影响哪块内存
  • 对Dalvik、ART虚拟机有什么了解?
  • Art和Dalvik对比
  • 虚拟机原理,如何自己设计一个虚拟机(内存管理,类加载,双亲委派)
  • 谈谈你对双亲委派模型理解
  • JVM内存模型,内存区域
  • 类加载机制
  • 谈谈对ClassLoader(类加载器)的理解
  • 谈谈对动态加载(OSGI)的理解
  • 内存对象的循环引用及避免
  • 内存回收机制、GC回收策略、GC原理时机以及GC对象
  • 垃圾回收机制与调用System.gc()区别

System.gc()只是通知垃圾回收器要进行垃圾回收操作,但并没有立即执行垃圾回收。它只是建议JVM安排GC运行,还有可能被拒绝。

  • Ubuntu编译安卓系统
  • 系统启动流程是什么?(提示:Zygote进程 –> SystemServer进程 –> 各种系统服务 –> 应用进程)
  • 大体说清一个应用程序安装到手机上时发生了什么
  • 简述Activity启动全部过程

1)Activity.startActivity-->startActivityForResult()

2)-->Instrumentation.execStartActivity()-->execStartActivity()

3)ActivityManager.getService().startActivity()通过Binder到ActivityManagerService.startActivity()

4)-->ActivityStarter.startActivityMayWait()-->startActivityLocked()-->startActivityUnchecked()

5)--ActivityStackSupervisor.resumeFocusedStackTopActivityLocked()-->ApplicationThread$scheduleLaunchActivity

6)ApplicationThread.schedulelaunchActivity()通过ActivityThread.sendMessage,再处理消息,进入handleLaunchActivity

-->Instrumentation$newActivity创建Activity的实例,使用类加载器创建Activity对象。

-->makeApplication创建Application对象,调用它的Application.onCreate()方法

Instrumentation这个类就是完成对Application和Activity初始化和生命周期的工具类。

  • App启动流程,从点击桌面开始

1)点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发送startActivity()

2)system_server进程向Zygote发送创建进程的请求(AMS中通过startActivity()方法,调用startProcessLocked()函数),Zygote通过socket通信的方式让Zygote进程 fork一个新进程出来作为App进程;

3)App进程通过Binder IPC向system_server进程发起attachApplication请求(ActivityThread的main()函数里会创建Application,还调用ActivityStackSupervisor.attachApplicationLocked);

4)system_server进程收到请求后,进行一系列准备工作,再通过binder IPC向APP进程发送scheduleLaunchActivity请求;

5)App进程的binder线程(ApplicationThread)收到请求后,通过handler向主线程发送LAUNCHER_ACTIVITY消息;

6)主线程收到Message消息后,通过反射机制创建出Activity并回调Activity.onCreate()等方法;

7)App正式启动,开始进入Activity的生命周期。

  • 逻辑地址与物理地址,为什么使用逻辑地址?
  • Android为每个应用程序分配的内存大小是多少?

根据应用实际使用情况分配,初始给进程分配为8M,应用最大可分配的可能是64M\128M\256M等

  • Android中进程内存的分配,能不能自己分配定额内存?

进程内存分配跟手机配置有关,不同手机可能不一样,有64M\128M\256M等,heapgrowthlimit是一个普通应用的内存限制,可通过ActivityManager.getLargeMemoryClass()获得,在mainfest中设置了largeHeap=true后,可以使应用可用内存增大到原来两倍。并不能自己定额分配内存,android系统是根据应用所需要内存大小,先分配初始大小heapstartsize,当应用申请更多内存,系统会再进行分配,但不得超过最大内存,超过了会报OOM。

  • 进程保活的方式

1)模拟前台进程,startForeground()将当前进程伪装成前台进程,将进程优先级提高到最高,现在前台进程会显示在通知栏中,取消不掉。

2)JobScheduler机制唤醒,系统会根据自己实现定时去调用改接口传递进程去实现一些操作,且这个接口被强制停止后仍能正常启动。在调用JobSchedule.schedule()来启动任务。

3)实现Service类时,将onStartCommand()返回值设置为START_STICKY,利用系统机制在service挂掉后自动拉活,但这种方式只适合原生系统,像小米、华为等定制化比较高的第三方厂商,这些都限制了。

4)一像素的Activity,

5)应用之间相互唤醒。

  • 如何保证一个后台服务不被杀死?(相同问题:如何保证service在后台不被kill?)比较省电的方式是什么?
  • App中唤醒其他进程的实现方式

1)启动其他进程的Activity\Service或是发送一条广播给相应的应用(该应用得静态注册此广播)

OOM定位与分析,如何定位哪块原因导致应用最终发生OOM?

OOM发生后,可以用Android Studio自带的Android Monitor dump出HPROF文件,再用SDK中hprof-conv工具转换为标准的Java堆转储文件格式,再用MAT继续分析。切换到histogram视图,按shadow heap降序排序,对实例对象占用内存大小排序,再查看实例到GC ROOT的路径。

一般可能导致的如图片:直接加载超大尺寸图片(对图片尺寸缩放预处理后再加载)、图片加载后未及时释放(利用LRU机制保证图片个数和占用内存);在页面中,加载非常多的图片(避免同时加载大量图片)

JNI层的crash如何捕获?

参考:JNI定位c++错误

通过ndk安装包中的addr2line objdump  ndk-stack工具进行分析crash,ndk针对不同的CPU架构实现了多套相同的工具,在选择add2line objdump时要根据目标机器CPU架构来选择。

一般JNI发生异常,会出现一个Fatal signal信号量,大概知道是哪个函数引起的,再看下面的backtrace日志,backtrace是JNI调用堆栈信息,以“#两位数字 pc”开头,找到对应的函数,再用addr2line进行定位出错的位置。addr2line -C -f -e ./obj/armeabi/xxx.so 000eea70

使用ndk-stack协助我们获取底层崩溃的堆栈信息,adb logcat | ndk-stack -sym ./obj/armeabi/xxx.so

应用卡顿定位

1)使用UI线程的Looper打印日志

Android主线程更新UI,如果1S钟刷新少于60次,即FPS小于60,一帧加载超过16.67ms的话,用户就会产生卡顿的感觉。Android使用消息机制进行UI更新,UI线程中有个Looper,其loop方法会不断提取message,调用其绑定的Handler在UI线程执行。如果handler.dispatchMessage方法中有耗时操作,就会发生卡顿。如果有卡顿,就打印出UI线程的堆栈信息。

优点:用户使用app就可以监控卡顿情况,但因需另开子线程获取堆栈信息,会消耗系统资源。

2)使用Choreographer.FrameCallback监控卡顿

Android系统每16ms会发出SYNC信息,通知界面重绘、渲染,每一次同步的周期为16.6ms,代表一帧的刷新频率可以在两次回调时间周期来判断是否发生卡顿。(Android4.1以上才支持)。可以通过Choreographer.FrameCallback回调doFrame(long)函数,如果两次doFrame之间间隔大于16.67ms说明发生了卡顿。这种方法从app层面来监控卡顿,同时可实时计算帧率和掉帧数,实时监测APP页面的帧率数据,一旦发现帧率过低,可自动保存现场堆栈信息。

卡顿监控系统处理流程:开发修复-》用户上报(后台配置下灰度0.2%的用户量进行卡顿监控和上报,每个用户一天上报一次,上报后删除文件不影响存储空间)-》后台解析(过滤、去重、分类、反解堆栈、入库)-》平台展示-》自动提单

Http2.0有关多路复用:

多路复用原理:HTTP2流和多路复用   HTTP2.0原理详细解析

原理是二进制分帧层+流实现了多路复用,OKHttp是怎么支持的呢,那就是读帧,读流,Okhttp对http2的支持简单分析

ViewStub是怎么实现延时加载的?

ViewStub是一个不可见、大小为0的View。具体体现在ViewStub的构造函数中会进行设置setVisibility(GONE)设置控件不可见,同时会设置setWillNotDraw(true),即本View不会调用onDraw()方法绘制内容。在它的onMeasure函数中会调用setMeasureDimenssion(0, 0)即不会测量视图,直接设置一个大小为0的View. 

对于ViewStub.inflate()机制是:1)调用LayoutInflate.flate(mLayoutResource, parent, false)来加载ViewStub中android:layout设置的布局view(最后一个参数false是代表attachToRoot设置成false,说明忽略android:layout中布局根节点的layoutParams参数);2)从父视图中获取当前ViewStub在父视图中位置(在加载父视图时会用一个占位符来代表ViewStub);3)将当前ViewStub从parent中移除;4)将android:layout中的布局view add到父视图中,如果StubView中设置了layoutParams属性就会用ViewStub中设置的。

应用场景:如网络加载失败页面,评论区域内的listView(当没有评论或是请求失败的时候不加载)

加载方式:findViewById(R.id.stubViewId).setVisibility(View.VISIBLE);或是((ViewStub)findViewById(R.id.StubViewId)).inflate();其实设置Visibility最终也是调用inflate来加载布局的。

如果ViewStub标签下写上width/height,在ViewStub相应的layout xml文件中也进行了宽高定义,会以谁为准?

其实上面也分析过了,会以ViewStub中设置的layoutParams为准。

ViewStub可不可以加载多次呢?

不能,上面也分析过了,ViewStub调用inflate()方法后,会把自己从父视图中移除掉,并把自身所包含的内容添加到父视图中,再重新加载,找不到在父视图了,就会抛出ViewStub must have a non-null ViewGroup viewparent。

merge标签

merge一般可以和include/ViewStub结合使用,当include/ViewStub从外部导入xml结构时,可以将被导入的xml用merge作为根节点表示,当被加载到父布局中可以将它们融合到父级结构中,而不会出现冗余的节点。因为它是直接将其中的子元素添加到merge标签的parent中,这样就保证不会引入额外的层级了。

注意:1)<merge />只可以作为xml layout的根节点;2)当需要扩展的xml layout本身的根节点也是merge时,需要将被导入的xml layout置于ViewGroup中,且设置attachToRoot为true。

猜你喜欢

转载自blog.csdn.net/smileiam/article/details/86667862