面试技巧

关注公众号 jb51net

关闭
IT专业知识 > IT职场规划 > 面试技巧 >

2019年必备的Android面试题及参考答案收集汇总

小羊子说

前言

找工作、招人必备之良品。后期不断完善中……

如何招聘人,搜集了一些知识点。如何做好应聘准备,也收集了一些主要知识点,供你参考。

 

文章目录

 

1.Java中的==、equals和hashCode的区别

(1)“==”运算符用来比较两个变量的值是否相等,即该运算符用于比较变量之间对应的内存中的地址是否相同,
要比较两个基本类型的数据或两个引用变量是否相等,只能使用“=.=“(注:编译器格式显示问题,是双等号)运算符

(2)equals是Object类提供的方法之一,每个java类都集成自Object类,即每个对象都有equals方法,equals与“==”一样,比较的都是引用,相比运 算符,equals(Object)方法的特殊之处在于其可以被覆盖,所以可以通过覆盖的方法让他比较的不是引用而是数据内容,即堆中的内容是否相等。

(3)hashCode()方法是从Object类继承过来的,他也是用来比较两个对象是否相等,Object类中的hashCode方法,返回对象在内存中地址转换成的一个Int值,所以如果未重写hashCode方法,任何对象的hashCode方法返回的值都是不相等的。

综上而言,==和equals判断的是(基本类型数据)引用是否相等,但equals可以通过覆盖方法使其比较的是数据内容,事实上,Java中很多类库中的类已经覆盖了此方法,而hashCode判断的是对象在内存中地址是否相等。

2.int和integer的区别

Integer是int提供的封装类,而int是java的基本数据类型,Integer的默认值是null,而int的默认值是0,声明Integer的变量需要实例化,而int不需要,Integer是对象,是一个引用指向这个对象,而int是基本数据类型,直接存储数据。

3.String、StringBuffer和StringBuilder的区别

他们的主要区别在于运行速度和线程安全两个方面,运行速度:StringBuilder>StringBuffer>String,String最慢的原因在于String是字符串常量,一旦创建是不可以再更改的,但后两者的对象是变量,是可以更改的,Java中对String对象的操作实际上是一个不断创建新的对象而将旧的对象回收的过程,而后两者因为是变量,所以可以直接进行更改,在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的,因为在StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,而StringBuilder不存在该关键字,所以在线程中并不安全。

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

内部类是定义在另一个类里面的类,与之相对应,包含内部类的类被称为外部类,

内部类的作用有:(1)内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一包中的其他类访问,(2)内部类的方法可以直接访问外部类的所有数据,包括私有的数据,(3)内部类的种类:成员内部类、静态内部类、方法内部类、匿名内部类

5.进程与线程的区别

进程是CPU资源分配的最小单位,而线程是CPU调度的最小单位,进程之间不能共享资源,而线程共享所在进程的地址空间和其他资源,一个进程内可以拥有多个线程,进程可以开启进程、也可以开启线程,一个线程只能属于一个进程,线程可直接使用同进程的资源,线程依赖于进程而存在。

6.final、finally、finalize的区别

final是用于修饰类、成员变量和成员方法,类不可被继承,成员变量不可变,成员方法不可被重写;finally与try…catch…共同使用,确保无论是否出现异常都能被调用到;finalize是类的方法,垃圾回收前会调用此方法,子类可以重写finalize方法实现对资源的回收。

7.Serializable和Parcelable的区别

Serlizable是java序列化接口,在硬盘上读写,读写的过程中有大量临时变量产生,内部执行大量的I/O操作,效率很低;Parcelable是Android序列化接口,效率高,在内存中读写,但使用麻烦,对象不能保存到磁盘中。

8.静态属性和静态方法是否可以被继承?是否可以被重写?

可以继承,但不可以被重写,而是被隐藏,如果子类里面定义了静态方法或属性,那么这时候父类的静态方法或属性称之为隐藏。

9.成员内部类、静态内部类、局部内部类、和匿名内部类的理解

Java中内部类主要分为成员内部类、局部内部类(嵌套在方法和作用域中)、匿名内部类(无构造方法)、静态内部类(由static修饰的类、不能使用任何外围类的非static成员变量和方法、不依赖外围类),每个内部类都能独立的继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类均无影响,因为Java支持实现多个接口,而不支持多继承,我们可以使用内部类提供的、可以继承多个具体的或抽象的类的能力来解决使用接口难以解决的问题,接口只是解决了部分问题,而内部类使得多继承的解决方案变得更加完整。

10.Java的垃圾回收机制及其在何时会被触发

内存回收机制:就是释放掉在内存中已经没有用的对象,要判断怎样的对象是没用的,有两种方法:(1)采用标记数的方法,在给内存中的对象打上标记,对象被引用一次,计数加一,引用被释放,计数就减一,当这个计数为零时,这个对象就可以被回收,但是,此种方法,对于循环引用的对象是无法识别出来并加以回收的,(2)采用根搜索的方法,从一个根出发,搜索所有的可达对象,则剩下的对象就是可被回收的,垃圾回收是在虚拟机空闲的时候或者内存紧张的时候执行的,什么时候回收并不是由程序员控制的,可达与不可达的概念:分配对象使用new关键字,释放对象时,只需将对象的引用赋值为null,让程序不能够在访问到这个对象,则称该对象不可达。

在以下情况中垃圾回收机制会被触发:

(1)所有实例都没有活动线程访问 ;(2)没有其他任何实例访问的循环引用实例;(3)Java中有不同的引用类型。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。

11.Java中的代理是什么?静态代理和动态代理的区别是什么?

代理模式:在某些情况下,一个用户不想或不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用,代理对象可以在客户端和目标对象之间起中介的作用,并且可以通过中介对象去掉用户不能看到的内容和服务,或者添加用户需要的额外服务。

静态代理:即在程序运行前代理类就已经存在,也就是编写代码的时候已经将代理类的代码写好。

动态代理:在程序运行时,通过反射机制动态创建代理类。

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

方法的重写Overriding和重载Overloading是Java多态性的不同表现

重写Overriding 是父类与子类之间多态的一种表现;

重载Overloading是同一个类中多态性的一种表现;

13.Java中反射的相关理解

Java的反射机制是在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,能够调用它的任意一个方法和属性,包括私有的方法和属性,这种动态地获取信息以及动态的调用对象的方法的功能就称之为Java的反射机制。

从对象出发,通过反射(.class类)可以获取到类的完整信息,(类名、class类型、所在包、具有的所有方法Method[]类型、某个方法的完整信息,包括修饰符、返回值类型、异常、参数类型、所有属性Field[ ]、某个属性的完整信息,构造器Constructors,调用类的属性或方法。

14.Java中注解的相关理解

Java注解,英文名为Annotation,一种代码级别的说明,是JDK1.5及以后版本引入的一个特性,与类、接口、枚举在同一个层次,他可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明、注释、作用分类;注解是Java提供的一种元程序中元素关联任何信息和任何元数据(metadata)的途径和方法,Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据

注解的一般格式为:[修饰符]@interface[名称]{元素},元素是无方法体的方法声明,可以有默认值。

注解的作用:

编写文档:通过代码里标识的元数据生成文档[生成文档doc]

代码分析:通过代码里的元数据对代码进行分析[使用反射]

编译检查:通过代码里标识的元数据让编译器能够实现基本的编译检查[Override]

15.对Java中String类的理解

通过对String类的源代码分析可知,(1)String类是final类,即意味着String类不能被继承,并且他的成员方法都默认为final方法;(2)String类在源代码中实际上是通过char[ ]数组来保存字符串的;(3)String对象一旦创建就是固定不便的,对String对象的任何change操作都会生成新的对象。

16.对Java中字符串常量池的理解

字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间成本的,而且字符串在程序中使用得非常多,JVM为了提高性能和减少内存的开销,在实例化字符串的时候会进行一些优化;每当我们创建字符串常量时,JVM首先会检查字符串常量池,如果该字符床已经存在于常量池中,那么就直接返回常量池中的实例引用,如果该字符串不存在,就会实例化该字符串,并将其放在常量池中,由于字符串的不可变性,我们可以十分肯定常量池中一定不存在两个相同的字符串,Java中的常量池实际上分为两种形态:静态常量池和运行时常量池

静态常量池:即*.class文件的常量池,.class文件的常量池不仅仅包括字符串(数字)字面量,还包含类、方法的信息,占用class文件的绝大部分空间。

运行时常量池:常量存入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

17.Java中为什么String类要设计成不可变的

在java中将String类设计成不可变的是综合考虑到各种因素的结果,需要综合内存、同步、数据结构、以及安全等方面的考虑。

字符串常量池的需要:字符串常量池是Java堆内存中一个特殊的存储区域,当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象,假若字符串对象允许改变,那么将会导致各种逻辑的错误,比如改变一个引用的字符串将会导致另一个引用出现脏数据。

允许String对象缓存HashCode:Java中String对象的哈希码被频繁的使用,比如在HashMap等容器中,字符串不变性保证了哈希码的唯一性,因此可以放心地进行缓存,这也是一种性能优化的手段,意味着不必每次都去计算新的哈希码。

安全性:String被许多Java类库用来当作参数,如:网络连接(network connection)、打开文件(opening files)等等,如果String不是不可变的,网络连接、打开文件将会被改变——这将导致一系列的安全威胁,操作的方法本以为连接上一台机器,其实不是,优于反射的参数都是字符串,同样也会引起一系列的安全问题。

18.Java中Hash码(哈希码)的理解

在Java中,哈希码代表了对象的一种特征,例如我们判断某两个字符串是否==,如果其哈希码相等,则这两个字符串是相等的,其次,哈希码是一种数据结构的算法,常见的哈希码的算法有:

Object类的HashCode,返回对象的内存地址经过处理后的结构,由于每个对象的内存地址都不一样,所以哈希码也不一样。

String类的HashCode,根据String类包含的字符串的内容,根据一种特殊的算法返回哈希码,只要字符串的内容相同,返回的哈希码也相同。

Integer类:返回的哈希码就是integer对象里所包含的那个整数的数值。例如

Integer i1=new Integer(100) i1.hashCode的值就是100,由此可见两个一样大小的Integer对象返回的哈希码也一样。

19.Object类的equal方法和hashcode方法的重写

equal和hashCode的关系是这样的:(1)如果两个对象相同(即用equal比较返回true),那么他们的hashcode值一定要相同;(2)如果两个对象的hashcode相同,他们并不一定相同(即用equal比较返回false),因为hashcode的方法是可以重载的,如果不重载,会用Java.long.Object的hashcode方法,只要是不同的对象,hashcode必然不同。

由于为了提高程序的效率才实现了hashcode方法,先进行hashcode的比较,如果不同,就没有必要再进行equal的比较了,这样就大大减少了equals比较的次数,这对需要比较数量很大的运算效率特稿是很多的。

附加:一旦new对象就会在内存中开辟空间,==比较的是对象的地址值,返回的是boolean型,equal方法默认比较的对象的地址值,但是Integer等基本类型包装类以及String类中已经重写了equal()方法,比较的是对象内存中的内容,返回值是boolean型。

20.Java常用集合List与Set,以及Map的区别

Java中的集合主要分为三种类型:Set(集)、List(列表)、Map(映射);

数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),而Java集合是可以存储和操作数目不固定的一组数据,所有的Java集合都位于java.util包中,Java集合只能存放引用类型的数据,不能存放基本数据类型。

Collection是最基本的集合接口,声明了适用于Java集合(只包括Set和List)的通用方法,Set和List都继承了Collection接口。

Set是最简单的一种集合,集合中的对象不按特定的方式排序,并且没有重复对象,Set接口主要实现了两种实现类

TreeSet:TreeSet类实现了SortedSet接口,能够对集合中的对象进行排序;

HashSet:HashSet类按照哈希算法来存取集合中的对象,存取速度比较快;

Set具有与Collection完全一样的接口,因此没有任何额外的功能,实际上Set就是Collection,只是行为不同(这是继承与多态思想的典型应用,表现不同的行为),Set不保存重复的元素,Set接口不保证维护元素的次序。

(2) List列表的特征是其他元素以线性表的方式存储,集合中可以存放重复的对象,其接口主要实现类:

ArrayList( ):代表长度可以改变的数组,可以对元素进行随机的访问,向ArrayList( )中插入与删除元素的速度慢。

LinkedList( ):在实现类中采用链表数据结构,插入和删除的速度快,但访问的速度慢。

对于List的随机访问来说,就是只是随机来检索位于特定位置的元素,List的get(int index)方法返回集合中由参数index指定的索引位置的对象,索引下标从0开始。

Map映射是一种把关键字对象映射的集合,他的每一个元素都包括一堆键对象和值对象,Map没有继承Collection接口,从Map集合中检索元素时只要给出键对象,就会返回对应的值对象。

HashMap:Map基于散列表的实现,插入和查询“键值对”的开销是固定的,可以通过构造器设置容量capacity和负载因子load factor ,以调整容器的性能;

LinkedHashMap:类似于HashMap,但在迭代遍历时,取得“键值对”的顺序是其插入次序,只比HashMap慢一点,而在迭代访问时反而更快,因为它使用链表维护内部次序。

TreeMap:基于红黑树数据结构的实现,查看“键”或“键值对”时,他们会对其排序(次序由Comparabel和Comparator决定)

21.ArrayMap和HashMap的区别

ArrayMap相比传统的HashMap速度更慢,因为其查找方法是二分法,并且当删除或添加数据时,会对空间重新调整,可以说ArrayMap是牺牲了时间来换空间,ArrayMap与HashMap的区别主要在:

存储方式不同:HashMap内部有一个HashMapEntry<K,V>[ ]对象,而ArrayMap是一个<key,value>映射的数据结构,内部使用两个数组进行数据存储,一个数组记录key的hash值,另一个数组记录value值。

添加数据时扩容的处理不一样:HashMap进行了new操作,重新创建对象,开销很大,而ArrayMap用的是copy数据,效率相对高很多。

ArrayMap提供了数组收缩的功能,在clear或remove之后,会重新收缩数组,释放空间。

ArrayMap采用的是二分法查找。

22.HashMap和HashTable的区别

HashMap是基于哈希表实现的,每一个元素是一个key—value对,其内部通过单链表解决冲突的问题HashMap是非线程安全的,只适用于单线程的环境下。多线程的环境下可以采用concurrent并发包下的concurrentHashMap,HsahMap实现了serializable接口,支持序列化,实现了cloneable接口,能被克隆。HashMap内部维持了一个存储数据的Entry数组,HashMap采用链表解决冲突,HashMap中的key和value都允许为null,key为null的键值对永远都放在以table[0]为节点的链表中。

HashTable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足时,同样会自动增大,HashTble是线程安全的,能用在多线程的环境下,HashTable实现了serializable接口,它支持序列化,实现了cloneable接口,能被克隆。

HashMap和HashTable之间的区别有以下几点

继承的父类不同,hashTable继承自Dictionary类,而HashMap继承自AbstractMap类,但二者都实现了Map接口。

线程安全性不同,HashTable中的方法是synchronized的,而HashMap中的方法在缺省的情况下是非ynchronized的,在多线程的环境下,可以直接使用HsahTable,不需要为他的方法实现同步,但使用HashMap时就必须自己增加同步处理。

key和value是否允许为null值:关于其中的key和value都是对象,并且不能包含重复的key,但可以包含重复的value,hashtable中,key和value都不允许出现null值,但在hashmap中,null可以作为键,这样的键只有一个,可以有多个键对应的值为null.

23.HashMap和HashSet的区别

HashMap:其实现了Map接口,HashMap存储键值对,使用put( )方法将元素放入到Map中,HashMap使用键对象来计算hashcode值,HashMap比较快,因为是使用唯一的键来获取对象。

HashSet:实现了Set接口,hashSet仅仅存储对象,使用add()方法将元素放入到set中,hashset使用成员对象来计算hashcode值,对于两个对象来说,hashcode可能相同,所以equal方法用来判断对象的相等性,如果两个对象不同的话,那么返回false,hashSet较hashMap来说较慢。

24.ArrayList和LinkedList的区别

ArrayList和LinkedList,前者是Array(动态数组)的数据结构,后者是Link(链表)的数据结构,此外他们两个都是对List接口的实现

当随机访问List时(get和set操作),ArrayList和LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后查找。

当对数据进行增删的操作时(add和remove),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后的所有数据的下标索引造成影响,需要进行数据的移动

从利用效率来看,ArrayList自由性较低,因为需要手动的设置固定大小的容量,但是他的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用,而LinkedList自由性交给,能够动态的随数据量的变化而变化,但是它不便于使用。

25.数组和链表的区别

数组:是将元素在内存中连续的存储的,因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高,但在存储之前,需要申请一块连续的内存空间,并且在编译的时候就必须确定好他的空间大小。在运行的时候空间的大小是无法随着需要进行增加和减少的,当数据比较大时,有可能出现越界的情况,当数据比较小时,有可能浪费内存空间,在改变数据个数时,增加、插入、删除数据效率比较低。

链表:是动态申请内存空间的,不需要像数组需要提前申请好内存的大小,链表只需在使用的时候申请就可以了,根据需要动态的申请或删除内存空间,对于数据增加和删除以及插入比数组灵活,链表中数据在内存中可以在任意的位置,通过应用来关联数据。

26.Java中多线程实现的三种方式

Java中多线程实现的方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程,其中前两种方式线程执行完没有返回值,只有最后一种是带返回值的。

继承Thread类实现多线程:继承Thread类本质上也是实现Tunnable接口的一个实例,他代表一个线程的实例,并且启动线程的唯一方法是通过Thread类的start()方法,start()方法是一个native方法,他将启动一个新线程,并执行run( )方法。

实现Runnable接口方式实现多线程:实例化一个Thread对象,并传入实现的Runnable接口,当传入一个Runnable target参数给Thread后,Thraed的run()方法就会调用target.run( );

使用ExecutorService、Callable、Future实现有返回结果的多线程:可返回值的任务必须实现Callable接口,类似的无返回值的任务必须实现Runnable接口,执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,在结合线程池接口ExecutorService就可以实现有返回结果的多线程。

27.Java中创建线程的三种方式

Java中使用Thread类代表线程,所有的线程对象都必须时Thread类或其子类的实例,Java中可以用三种方式来创建线程

继承Java中的Thread类创建线程:定义Thread类的子类,并重写其run( )方法,run( )方法也称为线程执行体,创建该子类的实例,调用线程的start()方法启动线程。

实现Runnable接口创建线程:定义Runnable接口的实现类,重写run()方法,run方法是线程的执行体,创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象,调用线程对象的Start方法启动线程。

使用Callable和Future创建线程:Callable接口提供了一个call( )方法,作为线程的执行体,call( )方法可以有返回值,call( )方法可以声明抛出异常,其创建线程并启动的步骤,创建Callable接口的实现类,并实现call( )方法,创建该实现类的实例,使用FutureTask类来包装Callable对象,该FuutureTask对象封装了callable对象的call( )方法的返回值,使用FutureTask对象作为Thread对象的target创建并启动线程,调用FutureTask对象的get( )方法来获得子线程执行结束后的返回值。

28.线程和进程的区别

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务,不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间,注意勿与栈内存混淆,每个线程都拥有单独的栈内存用来存储本地数据。

29.Java中的线程的run( )方法和start()方法的区别

start()方法被用来启动新创建的线程,而且start( )内部调用了run ( )方法,这和直接调用run( )方法的效果不同,当调用run( )方法时,只会是在原来的线程中调用,没有新的线程启动,只有start( )方法才会启动新线程。

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

在Java中常使用Semaphore(信号量)来进行并发编程,Semaphore控制的是线程并发的数量,实例化一个Semaphore对象,如Semaphore semaphore = newSemaphore(5,true) ,其创建了对象semaphore,并初始化了5个信号量,即最多允许5个线程并发访问,在执行的任务中,调用semaphore的acquire()方法请求一个信号量,这时信号量个数就减1,(一旦没有可使用的信号量,再次请求就会阻塞),来执行任务,执行完任务,调用semaphore的release()方法释放一个信号量此时信号量的个数就会加1 。

31.Java中wait和sleep方法的不同

Java程序中wait和sleep都会造成某种形式的暂停,sleep()方法属于Thread类中,而wait( )方法属于Object类中,sleep( )方法是让程序暂停执行指定的时间,释放CPU资源,但不会释放锁,线程的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,而当调用wait( )方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后线程才进入对象锁定池准备,获取对象锁进入运行状态。

32.对Java中wait/notify关键字的理解

wait()、notify()、notifyAll( )都不属于Thread类,而是属于Object基础类,也就是每个对象都有wait( )、notify()、notifyAll( )的功能,因为每个对象都有锁,锁是每个对象的基础。

wait():会把持有该对象线程的对象控制权交出去,然后处于等待状态。

notify():会通知某个正在等待这个对象的控制权的线程可以运行。

notifyAll( ):会通知所有等待这个对象的控制权的线程继续运行,如果有多个正在等待该对象控制权时,具体唤醒哪个线程,就由操作系统进行调度。

33.什么是线程阻塞?线程该如何关闭?

阻塞式方法是指程序会一直等待该方法完成执行,而在此期间不做其他的事情,例如ServerSocket的accept( )方法就是一直等待客户端连接,这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才返回你,此外还有异步和非阻塞式方法在任务完成前就返回。

线程关闭的方法有如下两种:

一种是调用线程的stop( )方法;

另一种是自己自行设计一个停止线程的标记;

34.如何保证线程的安全

使用Synchronized关键字:

调用Object类的wait很notify;

通过ThreadLocal机制实现;

35.实现线程同步的方式

Java允许线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据的不准确,相互之间产生冲突,因此在加入同步锁以避免在该线程没有完成操作之前,被其他线程调用,从而保证该变量的唯一性和准确性,同步的方法有以下几种:

Synchronized关键字修饰的方法:由于Java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法;

Synchronized关键字修饰语句块:被Synchronized关键字修饰的语句块会自动加上内置锁,从而实现同步;

使用特殊域变量(volatile)实现线程同步:当一个共享变量被volatile修饰时,他会保证修改的值立即被更新到主存中,volatile的特殊性在于,内存可见性,就是一个线程对于volatile变量的修改,对于其他线程来说是可见的,即线程每次获取volatile变量的值都是最新的。

36.Java中Synchronized关键字的用法,以及对象锁、方法锁、类锁的理解

Java的内置锁:每个Java对象都可以用作一个实现同步的锁,这些锁称为内置锁,线程进入同步代码块或方法时,会自动获得该锁,在退出同步代码块或方法时会释放该锁,获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

Java的内置锁是一个互斥锁,这即意味着最多只有一个线程获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或阻塞,直到线程B释放该锁,如果B线程不释放该锁,那么A线程将一直等待下去。

Java的对象锁与类锁,对象锁是用于实例方法的,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的,类的对象实例可以有很多个,但每个类只有一个class对象,所以不同对象的实例的对象锁是互不干扰,但是每个类只有一个类锁。

Synchronized的用法:Synchronized修饰方法和修饰代码块。

37.Java中锁与同步的相关知识

锁提供了两种主要的特性:互斥和可见性

互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享的数据;可见性在必须确保锁释放之前对共享对象做出的更改对于随后获得该锁的另一个线程是可见的。

在Java中,为了确保多线程读写数据时的一致性,可以采用两种方式

同步:如使用synchronized关键字,或者使用锁对象;

使用volatile关键字:使变量的值发生改变时尽快通知其他线程;

Volatile关键字详解

编译器为了加快程序的运行的速度,对一些变量的写操作会先在寄存器或者CPU缓存上进行,最后写入内存中,而在这个过程中,变量的新值对于其他线程是不可见的,当对使用volatile标记的变量进行修改时,会将其它缓存中存储的修改前的变量清除,然后重新读取。

38.Synchronized和volatile关键字的区别

Volatile在本质上是告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;Synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程会被阻塞。

Volatile仅能使用在变量级别,synchronized则可以使用在变量、方法和类级别。

Volatile仅能修改变量的可见性,不能保证原子性,而synchronized则可以保证变量的修改可见性和原子性。

Volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞。

Volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

39.Java的原子性、可见性、有序性的理解

原子性:原子是世界上最小的物质单位,具有不可分割性,比如a=0,这个操作是不可分割的,那么我们就会说这个操作是原子操作,再如a++,这个操作实际上是a=a+1,是可以分割的,所以他不是一个原子操作,非原子操作都会存在线程安全的问题,需要使用synchronized同步技术来使其变成一个原子操作,一个操作是原子操作,那么我么称它具有原子性。

可见性:是指线程之间的可见性,一个线程修改的状态对于另一个线程是可见的,比如用volatile修饰的变量就具有可见性,volatile修饰的变量不允许线程内部缓存和重排序,即会直接修改内存,所以对其他线程是可见的,但volatile只能让其被修饰的内容具有可见性,并不能保证它具有原子性,因为volatile仅能使用在变量级别,并不能对方法进行修饰,

有序性:即线程执行的顺序按代码的先后顺序执行,在Java内存模型中,允许编译器和处理器对指令进行重排序,但重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性,在Java里面可以通过volatile关键字来保证一定的“有序性”,另外还可以通过synchronized和Lock来保证有序性。

40.ReentrantLock、Synchronized、Volatile关键字

Synchronized:即互斥锁,即操作互斥,并发线程,串行得到锁,串行执行代码,就像一个房间一把钥匙,一个人进去后,下一个人必须等到第一个人出来得到钥匙才能进去;

ReetrantLock:可重入锁,和同步锁功能类似,不过需要显性的创建和销毁,其特点在于①ReentrantLock有try Lock方法,如果锁被其他线程持有,返回false,可以避免形成死锁,②创建时可自定义是可抢占的,③ReentrantReadWriteLock,用于读多写少,且不需要互斥的场景大大提高性能;

Volatile:只保证同意变量在多线程中的可见,他会强制将对缓存的修改操作立即写入主存,如果是写操作,会导致其他CPU对应的缓存无效;

41.Java中死锁的概念,其产生的四个必要条件

死锁是一种情形,多个线程被阻塞,他们中的一个或者全部都在等待某个资源被释放,由于线程被无限期的阻塞,因此程序不能正常运行,简单的说就是线程死锁时,第一个线程等待第二个线程释放资源,而同时第二个线程又在等待第一个线程释放资源;

死锁产生的四个必要条件:

互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有,此时若有其他请求该资源,则请求进程只能等待;

请求与保持条件:进程已经持有了至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程所占有,此时请求会被阻塞,但对自己获得的资源又保持不放;

不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,只能由获得该资源的进程自己来释放(只能时主动释放);

循环等待条件:即若干进程形成首尾相接的循环等待资源的关系,即形成了一个进程等待环路,环路中每一个进程所占有的资源同时被另一个进程所申请,也就是前一个进程占有后一个进程所申请的资源

这四个条件是死锁产生必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁;

死锁的避免:避免死锁的基本思想是系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配资源后系统可能发生死锁,则不分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。

42.Java中堆和栈的理解

在Java中内存分为两种,一种是栈内存,另一种是堆内存

堆内存:用于存储Java中的对象和数组,当我们new一个对象或创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放,堆内存的特点:先进先出,后今后出,②可以动态的分配内存的大小,生存期不必告诉编译器,但存取速度较慢;

栈内存:主要用来执行程序用,比如基本类型的变量和对象的引用变量,其特点:①先进后出,后进后出,②存取速度比堆快,仅次于寄存器,栈数据可以共享,但其在栈中的数据大小和生存期必须是确定的;

栈内存和堆内存都属于Java内存的一种,系统会自动去回收它,但对于堆内存开发人员一般会自动回收。

栈是一块和线程紧密相关的内存区域,每个线程都有自己的栈内存,用于存储本地变量、方法参数和栈调用一个线程中存储的变量,对于其他线程是不可见的,而堆是所有线程共享的一个公用内存区域,对象都在堆里创建,但为了提升效率,线程会从堆中拷贝一个缓存到自己的栈中,如果多个线程使用该变量,就可能引发问题,这是volatile修饰变量就可以发挥作用,他要求线程从主存中读取变量的值。

43.理解线程间通信

线程是CPU调度的最小单位(进程是CPU分配资源的最小单位),在Android中主线程是不能够做耗时的操作的,子线程是不能更新UI的,而线程间的通信方式有很多,比如广播、接口回掉等,在Android中主要使用handler,handler通过调用sendMessage方法,将保存好的消息发送到MessageQueue中,而Looper对象不断地调用loop方法,从MessageQueue中取出message,交给handler处理,从而完成线程间通信。

44.线程中的join()方法的理解,及如何让多个线程按顺序执行

Thread类的join()方法主要作用是同步,它可以使线程之间的并行执行变为串行执行,join()方法把指定的线程加入到当前线程中,可以将两个交替执行的线程合并为顺序执行的线程,比如在线程B中调用了线程A的join()方法,则会等到A线程执行完,才会继续执行线程B,join方法还可以指定时长,表示等待该线程执行指定时长后再执行其他线程,如A.join(1000),表示等待A线程执行1000毫秒后,再执行其他线程,当有多个线程、要使他们案顺序执行时,可以使用join()方法在一个线程中启动另一个线程,另外一个线程在该线程执行完之后再继续执行。

45.工作者线程(workerThread)与主线程(UI线程)的理解

Android应用的主线程(UI线程)肩负着绘制用户界面,和及时响应用户操作的重任,为避免“用户点击按钮后没有反应”的状况,就要确保主线程时刻保持着较高的响应性,把耗时的任务移除主线程,交予工作者线程(即子线程)完成,常见的工作者线程有AsyncTask(异步任务)、IntentService、HandlerThread,他们本质上都是对线程或线程池的封装。

46.AsyncTask(异步任务)的工作原理

AsyncTask是对Handler和线程池的封装,使用它可以更新用户界面,当然,这里的更新操作还是在主线程中完成的,但由于AsyncTask内部包含了一个Handler,所以可以发送消息给主线程,让他更新UI,另外AsyncTask内还包含了一个线程池,避免了不必要的创建和销毁线程的开销。

47.并发和并行的区别及理解

在单核的机器上,“多进程”并不是真正多个进程同时执行,而是通过CPU时间分片,操作系统快速在进程间切换而模拟出来的多进程,我们通常将这种情况称为并发,也就是多个进程的运行行为是“一并发生”的,但不是同时执行的,因为CPU核数的限制(CPU和通用寄存器只有有一套),严格来说,同一时刻只能存在一个进程的上下文。

但在多核CPU上,能真正实现多个进程并行执行,这种情况叫做并行,因为多个进程是真正“一并执行的”(具体多少个进程可以并行执行取决于CPU的核数),所以可知,并发是一个比并行更加宽泛的概念,在单核情况下,并发只是并发,而在多核情况下,并发就变为并行了。

48.同步和异步的区别、阻塞和非阻塞的区别的理解

同步:所谓同步是一个服务的完成需要依赖其他服务,只有等待被依赖的服务完成后,依赖的服务才能算完成,这是一种可靠的服务序列,要么都成功,要么都失败服务的状态可以保持一致;

异步:所谓异步是一个服务的完成需要依赖其他的服务时,只要通知其依赖的服务开始执行,而不需要等待被依赖的服务完成,此时该服务就算完成了,至于被依赖的服务是否真正完成并不关心,所以这是不可靠的服务序列;

阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务,函数只有在得到结果之后才会返回;

非阻塞:和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立即返回;

阻塞调用和同步调用是不同的,对于同步调用来说,很多时候当前线程可能还是激活的,只是从逻辑上当前函数没有返回值而已,此时这个线程可能也会处理其他消息,所以如下总结:

如果这个线程在等待当前函数返回时,仍在执行其他消息处理,这种情况叫做同步非阻塞;

如果这个线程在等待当前函数返回时,没有执行其他的消息处理,而是处于挂起等待的状态,这种情况叫做同步阻塞;

如果这个线程当前的函数已经返回,并且仍在执行其他的消息处理,这种情况叫做异步非阻塞;

如果这个线程当前的函数已经返回,但没有执行其他的消息处理,而是处于被挂起的等待状态,这种情况叫做异步阻塞;

同步与异步的重点在于的等待依赖的服务是否返回结果(即使没有执行完),也就是结果通知的方式,而不管其依赖的服务是否完成,阻塞与非阻塞的重点在于当前线程等待消息返回时的行为,是否执行其他的消息处理,当前线程是否被挂起;

49.Java中任务调度的理解

大部分操作系统(如windows、Linux)的任务调度采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行,任务执行的一段时间叫做时间片,任务正在执行的状态叫做运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态,等待下一个属于他的时间片的到来,这样每个任务都可以得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速的切换,给人的感觉是多个任务在“同时执行”,这也即我们所说的并发;

50.Java中进程的详细概念

计算机的额核心是CPU,它承担了所有的计算任务,操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机的硬件,应用程序是具有某种功能的程序,其运行在操作系统上,而进程则是一个具有一定独立功能的程序在一个数据集上一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体,进程是CPU资源分配的最小单位(线程是CPU执行的最小的单元),各个进程之间内存地址相互隔离。

51.线程的详细概念

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位,一个进程可以有多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间),线程是一个进程中代码的不同执行路线,线程上下文切换比进程上下文切换要快得多。

52.Android中的性能优化相关问题

由于手机硬件的限制,在Android手机中过多的使用内存,会容易导致oom(out of memory 内存溢出),过多的使用CPU资源,会导致手机卡顿,甚至导致ANR(Application Not Responding 应用程序无响应),Android主要从以下几部分对此进行优化:

布局优化,使用herarchyviewer(视图层次窗口)工具删除无用的空间和层级;

选择性能较低的viewgroup,比如在可以选择RelativeLayout也可以使用LinearLayout的情况下,优先使用LinearLayout,因为相对来说RelativeLayout功能较为复杂,会占用更多的CPU资源;

使用标签重用布局、减少层级、进行预加载(用的时候才加载);

绘制优化:指view在ondraw方法中避免大量的耗时的操作,由于onDraw方法可能会被频繁的调用;

ondraw方法中不要创建新的局部变量,ondraw方法被频繁的调用,很容易引起GC;

ondraw方法不要做耗时的操作;

线程优化:使用线程池来管理和复用线程,避免程序中出现大量的Thread,同时可以控制线 的并发数,避免相互抢占资源,而导致线程阻塞;

53.内存泄漏的相关原因

如果一个无用的对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在堆中所占有的内存单元无法被释放而造成内存空间浪费,这种情况就是内存泄漏,常见的内存泄露的场景有:

单例模式:因为单例的静态特性使得他的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有他的引用,那么在某个应用程序的生命周期他都不能正常回收,从而导致内存泄漏;

静态变量导致内存泄漏:静态变量存储在方法区,他的生命周期从类加载开始,到整个进程结束,一旦静态变量初始化,他所持有的引用只有等到进程结束才会释放;

非静态内部类导致内存泄漏:非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部对象的生命周期长时,就会导致内存泄漏,通常在Android开发中,如果要使用内部类,但又要规避内存泄漏,一般会采用静态内部类+弱引用的方式;

未取消注册或回掉导致内存泄漏:比如在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个广播会一直存在于系统中,同非静态内部类一样持有Activity引用,导致内存泄漏,因此在注册广播后一定要取消注册;

54.通过Handler在线程间通信的原理

Android中主线程是不能进行耗时操作的,子线程不能进行更新UI,所以就有了Handler,其作用就是实现线程之间的通信。

Handler在整个线程通信的过程中,主要有四个对象,Handler、Message、MessageQueue、Looper等,当应用创建时,就会在主线程中创建Handler对象,将要传递的信息保存到Message中,Handler通过调用sendMessage()方法将Message发送到MessageQueue中,Looper对象不断调用Loop( )方法从MessageQueue中取出Message交给handler进行处理,从而实现线程之间的通信。

55.Android中动画的类型:

帧动画:通过指定每一帧图片和播放的时间,有序地进行播放而形成动画效果;

补间动画:通过指定的view的初始状态、变化时间、方式等,通过一系列的算法去进行图形变换从而形成动画效果,主要有Alpha、Scale、Translate、Rotate四种效果;

属性动画:在Android3.0开始支持,通过不断改变view的属性,不断的重绘而形成动画效果;

55.理解Activity、View、Window三者之间的关系

使用一个比喻形容他们的关系,Activity像一个工匠(控制单元),Window像窗户(承载模型)、View像窗花(显示视图)、LayoutInflater像剪刀、XML配置像窗花图纸:

Activity构造的时候会初始化一个Window,准确的说应该是PhoneWindow;

这个PhoneWindow有一个”ViewRoot“,这个”ViewRoot”是一个View或者说是ViewGroup,是最初的根视图;

“ViewRoot”是通过addView方法来添加一个个View的,比如TextView、Button等;

这些view的事件的监听,是由WindowManagerService来接收消息,并且回调Activity函数,比如onClickListener、onKeyDown等;

56.Android中Context详解:

Context是一个抽象基类,译为上下文,也可理解为环境,是用户操作操作系统的一个过程,这个对象描述的是一个应用程序环境的全局信息,通过它可以访问应用程序的资源和相关的权限,简单的说Context负责Activity、Service、Intent、资源、Package和权限等操作。Context层次如下图:

第一层:Context抽象类;

第二层:一个ContextImpl的实现类,里面拥有一个PackageInfo类,PackageInfo类是关于整个包信息的类,一个ContextWraper是一个Context的包装类,里面有一个ContextImpl类的实例,通过整个实例去调用ContextImpl里面的方法;

第三层:Service和Application直接继承ContextWrapper,但是Activity需要引入主题,所以有了ContextThemeImpl类;

总结:在使用Context对象获取静态资源,创建单例对象或者静态方法时,多考虑Context的生命周期,不要使用Activity的Context,要使用生命周期较长的Application的Context对象,但并不是所有的情况都使用Application的Context,在创建Dialog、view等控件的时候,必须使用Activity的Context对象。

57.Java中double和float类型的区别

float是单精度类型,精度是8位有效数字,取值范围是10的-38次方到10的38次方,float占用4个字节的存储空间;

double是双精度类型,精度是10的-308次方到10的308次方,double类型占用8个字节的存储空间;

默认情况下都用double来表示,所以如果要用float,则应在数字对象后加字符f。

58.Android常用的数据存储方式(4种)

使用SharedPreference存储:保存基于xml文件存储的key-value键值对数据,通常用来存储一些简单的配置信息;

文件存储方式:Context提供了两个方法来打开数据文件的文件IO流;

SQLite存储数据:SQLite是轻量级的嵌入式数据库引擎,支持SQL语言;

网络存储数据:通过网络存储数据;

59.ANR的了解及优化

ANR全名Appication NotResponding ,也就是“应用程序无反应”,当操作在一段时间内无法得到系统回应时就会出现ANR错误,造成ANR的主要原因是由于线程的任务在规定的时间内没有完成造成的;

60.Android垃圾回收机制和程序优化System.gc( )

垃圾收集算法的核心思想:对虚拟机的可用内存空间,即堆空间中的对象进行识别,如果对象正在被引用,则称其为存活对象,反之,如果对象不再被引用,则为垃圾对象,可以回收其占据的空间,用于再分配。

JVM进行次GC的频率很高,但因为这种GC占用的时间极短,所以对系统产生的影响不大,但主GC对系统的影响很明显,触发主GC的条件主要有下:

当应用程序空闲时,即没有应用线程在运行时,GC会被调用,因为GC在优先级别最低的线程中进行,所以当应用繁忙时,GC就不会被调用;

Java堆内存不足时,主GC会被调用,当应用线程正在运行,并在运行过程中创建新对象时,若这时内存空间不足,JVM会强制调用主GC,以便回收内存用于新的分配,若GC一次之后仍无法满足,则会继续进行两次,若仍无法满足,则会报OOM错误;

System.gc( )函数的作用只是提醒虚拟机希望进行一次垃圾回收,但并不一定保证会进行,具体什么时候进行取决于虚拟机;

61.Android平台的优势和不足

Android平台手机 5大优势:

开放性:Android平台首先就是其开放性,开发的平台允许任何移动终端厂商加入到Android联盟中来。显著的开放性可以使其拥有更多的开发者;

挣脱运营商的束缚:在过去很长的一段时间,手机应用往往受到运营商制约,使用什么功能接入什么网络,几乎都受到运营商的控制,而Android用户可以更加方便地连接网络,运营商的制约减少;

丰富的硬件选择:由于Android的开放性,众多的厂商会推出千奇百怪,功能特色各具的多种产品。功能上的差异和特色,却不会影响到数据同步、甚至软件的兼容;

开发商不受任何限制:Android平台提供给第三方开发商一个十分宽泛、自由的环境,不会受到各种条条框框的阻扰;

无缝结合的Google应用: Android平台手机将无缝结合这些优秀的Google服务如地图、邮件、搜索等;

Android平台手机几大不足:

安全和隐私:由于手机与互联网的紧密联系,个人隐私很难得到保守。除了上网过程中经意或不经意留下的个人足迹,Google这个巨人也时时站在你的身后,洞穿一切;

过分依赖开发商缺少标准配置:在Android平台中,由于其开放性,软件更多依赖第三方厂商,比如Android系统的SDK中就没有内置音乐播放器,全部依赖第三方开发,缺少了产品的统一性;

同类机型用户很少:在不少手机论坛都会有针对某一型号的子论坛,对一款手机的使用心得交流,并分享软件资源。而对于Android平台手机,由于厂商丰富,产品类型多样,这样使用同一款机型的用户越来越少,缺少统一机型的程序强化。

62.其他讲解:

Activity组件

在Activity的生命周期中,可以将Activity表现为3种状态:

激活态:当Acitivity位于屏幕前端,并可以获得用户焦点、收用户输入时,这种状态称为激活态,也可以称为运行态;

暂停态:当Activity在运行时被另一个Activity所遮挡并获取焦点,此时Activity仍然可见,也就是说另一个Activity是部分遮挡的,或者另一个Activity是透明的或半透明的,此时Activity处于暂停状态;

停止态:当Activity被另一个Activity完全遮挡不可见时处于停止状态,这个Activity仍然存在,它保留在内存中并保持所有状态和成员信息,但是当该设备内存不足时,该Activity可能会被系统杀掉以释放其占用的内存空间,当再次打开该Activity时,它会被重新创建;

Activity生命周期中的7个方法:

onCreate( ):当Activity被创建时调用;

onStart( ):当Activity被创建后将可见时调用;

onResume( ):(继续开始)当Activity位于设备最前端,对用户可见时调用;

onPause( ):(暂停)当另一个Activity遮挡当前Activity,当前Activity被切换到后台时调用;

onRestart( ):(重新启动)当另一个Activity执行完onStop()方法,又被用户打开时调用;

onStop( ):如果另一个Activity完全遮挡了当前Activity时,该方法被调用;

onDestory( ):当Activity被销毁时调用;

Activity的四种启动模式:standard、singleTop、singleTask和singleInstance,他们是在配置文件中通过android:LauchMode属性配置;

standard:默认的启动模式,每次启动会在任务栈中新建一个启动的Activity的实例;

SingleTop:如果要启动的Activity实例已位于栈顶,则不会重新创建该Activity的实例,否则会产生一个新的运行实例;

SingleTask:如果栈中有该Activity实例,则直接启动,中间的Activity实例将会被关闭,关闭的顺序与启动的顺序相同;

SingleInstance:该启动模式会在启动一个Activity时,启动一个新的任务栈,将该Activity实例放置在这个任务栈中,并且该任务栈中不会再保存其他的Activity实例;

Activity任务栈:即存放Activity任务的栈,每打开一个Activity时就会往Activity栈中压入一个Activity

任务,每当销毁一个Activity的时候,就会从Activity任务栈中弹出一个Activity任务,

由于安卓手机的限制,只能从手机屏幕获取当前一个Activity的焦点,即栈顶元素(

最上面的Activity),其余的Activity会暂居后台等待系统的调用;

关于任务栈的更多概念:

当程序打开时就创建了一个任务栈,用于存储当前程序的Activity,当前程序(包括被当前程序所调用的)所有的Activity都属于一个任务栈;

一个任务栈包含了一个Activity的集合,可以有序的选择哪个Activity和用户进行交互,只有任务栈顶的Activity才可以与用户进行交互;

任务栈可以移动到后台,并保留每一个Activity的状态,并且有序的给用户列出他们的任务,而且还不会丢失他们的状态信息;

退出应用程序时,当把所有的任务栈中所有的Activity清除出栈时,任务栈会被销毁,程序退出;

默认Acctivity启动方式的缺点:

每开启一次页面都会在任务栈中添加一个Activity,而只有任务栈中的Activity全部清除出栈时,任务栈被销毁,程序才会退出,这样就造成了用户体验差,需要点击多次返回才可以把程序退出了。

每开启一次页面都会在任务栈中添加一个Activity还会造成数据冗余重复数据太多,会导致内存溢出的问题(OOM)。

Service组件

Service组件主要用于处理不干扰用户交互的后台操作,如更新应用、播放音乐等,Service组件既不会开启新的进程,也不会开启新的线程,它运行在应用程序的主线程中,Service实现的逻辑代码不能阻塞整个应用程序的运行,否则会引起应用程序抛出ANR异常。

Service组件常被用于实现以下两种功能(分别对应两种启动模式):

使用startService()方法启动Service组件,运行在系统的后台在不需要用户交互的前提下,实现某些功能;

使用bindService()方法启动Service组件,启动者与服务者之间建立“绑定关系”,应用程序可以与Service组件交互;

Service中常用的方法:

onBind(Intent intent):抽象方法绑定模式时执行的方法,通过Ibinder对象访问Service;

onCreate():Service组件创建时执行;

onDestroy():Service组件销毁时执行;

onStartCommand( Intent intent ,int flags ,int startId ):开始模式时执行的方法,每次执行startService()方法,该方法都会执行;

onUnBind( ):解除绑定时执行;

stop Self():停止Service组件;

Service组件的生命周期:Service有两种启动模式,startService方法或bindServce方法,启动方法决定了Service组件的生命周期和他所执行的生命周期方法:

通过statrtService方法启动Service组件,如果已经存在目标组件,则会直接调用onStartCommand方法,否则回调哦那Create()方法,创建Service对象,启动后的Service组件会一直运行在后台,与“启动者”无关联,其状态不会受“启动者”的影响,即使“启动者被销毁,Service组件还会继续运行,直到调用StopService()方法,或执行stopSelf()方法;

通过bindService()方法,启动Service组件,如果组件对象已经存在,则与之绑定,否则创建性的Service对象后再与之绑定,这种启动方法把Service组件与“启动者“绑定,Service返回Ibinder对象,启动者借助ServiceConnection对象实例实现与之交互,这种启动方式会将Service组件与”启动者“相关联,Service的状态会受到启动者的影响;

Service的启动模式详解

启动和停止Service组件的方法都位于context类中,再Activity中可以直接调用;

startService(Intent service):以Start模式启动Service组件,参数service是目标组件;

stopService(Intent service):停止start模式启动的Service组件,参数service是目标组件;

bindService(Intent service ,serviceConnection conn ,int flags):以Bind模式启动Service组件,参数service是目标组件,conn是与目标链接的对象,不可为NULL,flags是绑定模式;

unbindService(ServiceConnection conn):解除绑定模式启动的Service组件,conn是绑定时的链接对象;

BoradcastReceiver组件

广播机制是安卓系统中的通信机制,可以实现在应用程序内部或应用程序之间传递消息的作用,发出广播(或称广播)和接收广播是两个不同的动作,Android系统主动发出的广播称为系统广播,应用程序发出广播称为自定义广播,广播实质上是一个Intent对象,接收广播实质上是接收Intent对象。

接收广播需要自定义广播接收器类,继承自BoradcastReceiver类,BoradcastReceiver的生命周期很短,BoradcastReceiver对象在执行完onReceiver()方法后就会被销毁,在onReceiver方法中不能执行比较耗时的操作,也不能在onReceiver方法中开启子线程,因为当父线程被杀死后,他的子线程也会被杀死,从而导致线程不安全。

广播分为有序广播和无序广播

无序广播:通过sendBoradcast()方法发送的广播,普通广播对于接收器来说是无序的,没有优先级,每个接收器都无需等待即可以接收到广播,接收器之间相互是没有影响的,这种广播无法被终止,即无法阻止其他接收器的接收动作。

有序广播:通过sendOrderedBroadcast()方法发送的广播,有序广播对于接收器来说是有序的,有优先级区分的,接收者有权终止广播,使后续接收者无法接收到广播,并且接收者接收到广播后可以对结果对象进行操作。

注册广播接收器的方式:配置文件静态注册和在代码中动态注册。

配置文件中静态注册:BoradcastReceiver组件使用标签配置,写在application标签内部,receiver标签内的标签用于设置广播接收器能够响应的广播动作。

使用代码动态注册:使用context中的registerReceiver(BoradcastReceiver receiver ,IntentFileter filter)方法,参数receiver是需要注册的广播接收器,参数filter是用于选择相匹配的广播接收器,使用这种方法注册广播接收器,最后必须解除注册,解除注册使用context的unregisterReceiver(BoradcastReceiverreceiver)方法,参数receiver为要解除注册的广播接收器。

配置文件静态注册和在代码中动态注册两种方式的区别

静态注册(也称常驻型注册):这种广播接收器需要在Androidmainfest.xml中进行注册,这种方式注册的广播,不受页面生命周期的影响,即使退出了页面,也可以收到广播,这种广播一般用于开机自启动,由于这种注册方式的广播是常驻型广播,所以会占用CPU资源。

动态注册:是在代码中注册的,这种注册方式也叫非常驻型注册,会受到页面生命周期的影响,退出页面后就不会收到广播,我们通常将其运用在更新UI方面,这种注册方式优先级较高,最后需要解绑(解除注册),否则会导致内存泄漏)。

要点:使用广播机制更新UI的思路,在需要更新的Activity内定义一个继承自BroadcastReceiver的 内部类,在Activty中动态注册该广播接收器,通过广播接收器的的onReceiver()方法来更新UI。

ContentProvider(内容提供者)组件

Android系统将所有的数据都规定为私有,无法直接访问应用程序之外的数据,如果需要访问其他程序的数据或向其他程序提供数据,需要用到ContentProvider抽象类,该抽象类为存储和获取数据提供了统一的接口,ContentProvider对数据进行了封装,也在使用时不必关心数据的存储方式。

URI(统一资源标识符):一个Uri一般有三部分组成,分别是访问协议、唯一性标识字符串(或访问权限字符串)、资源部份。

ContentProvider实现对外部程序数据操作的思路:

在一个程序中自定义ContentProvider类(继承自ContentProvider),并实现其中的抽象方法,在配置文件中使用标签对其进行注册(标签中有3个很重要的属性:name为指明自定义的ContentProvider的全路径,authories为访问标识,exported是否允许外部应用访问),在另一个需要操作该程序数据的程序中,通过context类的getContentResolver方法获得ContentResolver类的实例对象,ContentResolver类执行增、删、改、查操作的方法与ContentProvider类的几乎一致,通过调用ContentResolver类实现类的方法来实现数据操作(在这些方法中仍然会用到Uri参数,其应与被操作数据的所在程序中的定义的Uri一致)。

Fragment

Android3.0引入Fragment技术,译为“碎片、片段”,在Activity中可以通过FragmentManager来添加、移除和管理所加入的Fragment,每个Fragment都有自己的布局、生命周期、交互事件的处理,由于Fragment是嵌入到Activity中的,所以Fragment的生命周期又和Activity的生命周期有密切的关联。

Fragment的生命周期的方法:

onAttach( ):当Fagment和Activity产生关联时被调用;

onCreate( ):当Fragment被创建时调用;

onCreateView():创建并返回与Fragment相关的view界面;

onViewCreate():在onCreateView执行完后立即执行;

onActivityCreated( ):通知Fragment,他所关联的Activity已经完成了onCreate的调用;

onStart( ):让Fragment准备可以被用户所见,该方法和Activity的onStart()方法相关联;

onResume():Fragment可见,可以和用户交互,该方法和Activity的onResume方法相关联;

onPause():当用户离开Fragment时调用该方法,此操作是由于所在的Activity被遮挡或者是在Activity中的另一个Fragment操作所引起的;

onStop():对用户而言,Fragment不可见时调用该方法,此操作是由于他所在的Activity不再可见或者是在Activity中的一个Fragment操作所引起的;

onDestroyView():ment清理和它的view相关的资源;

onDestroy():最终清理Fragment的状态;

onDetach():Fragment与Activity不再产生关联;

Fragment加载布局文件是在onCreateView()方法中使用LayoutInflater(布局加载器)的inflate( )方法加载布局文件。

Fragment中传递数据:当Activity加载Fragment的时候,往其内部传递参数,官方推荐使用Fragment的setArguments(Bundle bundle)方式传递参数,具体方法为在Activity中,对要传递参数的Fragment调用setArguments(Bundle bundle)方法,在接收参数的Fragment中调用context的getArguments()方法获取Bundle参数;

管理Fragment:通过调用context的getFragmentManager( )方法或者getsupportFragmentManager( )方法(这两个方法需要导入的包不同)来获取FragmentManager,使用FragmentManager对象,主要可以调用如下方法:

findFragmentById/findFragmentByTag:获取Activity中已经存在的Fragment;

getFragments( ):获取所有加入Activity中的Fragment;

begainTransaction( ):获取一个FragmentTransaction对象,用来执行Fragment的事物;

popBackStack():从Activity的后退栈中弹出Fragment;

addOnBackChagedListerner:注册一个侦听器以监视后退栈的变化;

FragmentTransaction:

在Activity中对Fragment进行添加、删除、替换以及执行其他的动作将引起Fragment的变化,叫做一个事务,事务是通过FragmentTransaction来执行的,可以用add、remove、replace、show、hide( )等方法构成事务,最后使用commit提交事务,参考代码:

getSupportFragmentManager( ).beginTransaction()

             .add(R.id.main_container,tFragment)

            .add(R.id.main_container,eFragment)

             .hide(eFragment).commit( );
 

其中R.id.main_container为布局中容纳Fragment的view的ID。

ViewPager

ViewPager是google SDK自带的一个附加包(android.support.V4)中的一个类,可视为一个可以实现一种卡片式左右滑动的View容器,使用该类类似于ListView,需要用到自定义的适配器PageAdapter,区别在于每次要获取一个view的方式,ViewPager准确的说应该是一个ViewGroup。

PagerAdapter是ViewPager的支持者,ViewPager调用它来获取所需显示的页面,而PagerAdapter也会在数据变化时,通知ViewPager,这个类也是FragmentPagerAdapter和FragmentStatePagerAdapter的基类。

FragmentPageAdapter和FragmentStatePagerAdapter的区别

FragmentPageAdapter:和PagerAdapter一样,只会缓存当前Fragment以及左边一个和右边一个,如果左边或右边不存在Fragment则不缓存;

FragmentStatePagerAdapter:当Fragment对用户不可见时,整个Fragment会被销毁,只会保存Fragment的状态,而在页面需要重新显示的时候,会生成新的页面;

综上,FragmentPagerAdapter适合页面较少的场合,而FragmentStatePagerAdapter则适合页面较多或页面的内容非常复杂(需要占用大量内存)的情况。

当实现一个PagerAdapter时,需要重写相关方法:

getCount( ):获得viewPager中有多少个view ;

destroyItem(viewGroup ,interesting,object):移除一个给定位置的页面;

instantiateItem(ViewGroup ,int ):将给定位置的view添加到viewgroup(容器中),创建并显示出来,返回一个代表新增页面的Object(key),key和每个view要有一一对应的关系;

isviewFromObject( ):判断instantiateItem(ViewGroup,int )函数所返回的key和每一个页面视图是否是代表的同一个视图;

综合使用ViewPager、Fragment和FragmentPagerAdapter:

自定义一个继承FragmentPagerAdapter的子类,重写其相关方法,当在实例化该子类时,可以传入Fragmnt集合(即要使用到的全部Fragmnet的集合),将ViewPager与该自定义的适配器实例绑定,为ViewPager设置OnPagerListener( )监听事件,重写OnPagerChangeListener的onPageSelected( )方法,实现页面的翻转。

关于Fragment中的控件的事件的监听:

在Fragment中的onActivityCreated( )生命周期方法中通过context的getActivity()方法获取到Fragment类相关联的Activity,并就此Activity通过findViewById( )方法来获取相关组件,再为组件添加监听事件。

Android的事件传递(分发)机制

基础概念:

事件分发的对象:点击事件(Touch事件),当用户触摸屏幕时(view或viewGroup派生的控件),将产生点击事件(Touch事件),Touch事件的相关细节(发生触摸的位置、时间等)被封装程MotionEvent对象;

事件的类型:

MotionEvent . ACTION_DOWN 按下view(所有事件的开始)

MotionEvent .ACTION_UP 抬起view(与DOWN对应)

MotionEvent .ACTION_MOVE 滑动view

MotionEvent .ACTION_CANCEL 结束事件(非人为的原因)

事件列:从手指触摸至离开屏幕,这个过程中产生的一系列事件为事件列,一般情况下,事件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件;

事件分发的本质:将点击事件(MotionEvent)传递到某个具体的View&处理的整个过程,即事件的传递过程=分发过程;

事件在哪些对象之间传递:Activity、viewGroup、view等对象;

事件分发过程中协作完成的方法:

dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()方法;

Android事件传递机制跟布局的层次密切相关,一个典型的布局包括根节点ViewGroup、子ViewGroup、以及最末端的view(如下图):

在这种结构中最上层的是RootViewGroup,下层是子view,当我们点击子view的时候,点击事件从上层往下层依次传递(即下沉传递Activity——ViewGroup——View),传递的过程中调用dispatchTouchEvent和onInterceptTouchEvent函数,当事件传递到被点击的子view之后,停止事件的传递,开始改变方向(即冒泡响应View ——ViewGroup——Activity),依次向上层响应,响应的过程调用onTouch或onTouchEvent方法。

传递过程中的协作方法:

public boolean dispatchTouchEvent(MotionEvent ev):事件分发处理函数,通常会在Activity层根据UI的情况,把事件传递给相应的ViewGroup;

public boolean onIntereptTouchEvent(MotionEvent ev):对分发的事件进行拦截(注意拦截ACTION_DOWN和其他ACTION的差异),有两种情况:

第一种情况:如果ACTION_DOWN的事件没有被拦截,顺利找到了TargetView,那么后续的MOVE和UP都能够下发,如果后续的MOVE与UP下发时还有继续拦截的话,事件只能传递到拦截层,并且发出ACTION_CANCEL;

第二种情况:如果ACTION_DOWN的事件下发时被拦截,导致没有找到TargetView,那么后续的MOVE和UP都无法向下派发了,在Activity层就终止了传递。

public boolean onTouchEvent(MotionEvent ev):响应处理函数,如果设置对应的Listener的话,这里还会与OnTouch、onClick、onLongClick相关联,具体的执行顺序是:

onTouch()——>onTouchEvent()——>onClick()——>onLongClick(),是否能够顺序执行,取决于每个方法返回值是true还是false。

Bitmap的使用及内存优化

位图是相对于矢量图而言的,也称为点阵图,位图由像素组成,图像的清晰度由单位长度内的像素的多少来决定的,在Android系统中,位图使用Bitmap类来表示,该类位于android.graphics包中,被final所修饰,不能被继承,创建Bitmap对象可使用该类的静态方法createBitmap,也可以借助BitmapFactory类来实现。Bitmap可以获取图像文件信息,如宽高尺寸等,可以进行图像剪切、旋转、缩放等操作.

BitmapFactory是创建Bitmap的工具类,能够以文件、字节数组、输入流的形式创建位图对象,BitmapFactory类提供的都是静态方法,可以直接调用,BitmapFactory. Options类是BitmapFactory的静态内部类,主要用于设定位图的解析参数。在解析位图时,将位图进行相应的缩放,当位图资源不再使用时,强制资源回收,可以有效避免内存溢出。

缩略图:不加载位图的原有尺寸,而是根据控件的大小呈现图像的缩小尺寸,就是缩略图。

将大尺寸图片解析为控件所指的尺寸的思路:

实例化BitmapFactory . Options对象来获取解析属性对象,设置BitmapFactory. Options的属性inJustDecodeBounds为true后,再解析位图时并不分配存储空间,但可以计算出原始图片的宽度和高度,即outWidth和outHeight,将这两个数值与控件的宽高尺寸相除,就可以得到缩放比例,即inSampleSize的值,然后重新设置inJustDecodeBounds为false,inSampleSize为计算所得的缩放比例,重新解析位图文件,即可得到原图的缩略图。

获取控件宽高属性的方法:可以利用控件的getLayoutParams()方法获得控件的LayoutParams对象,通过LayoutParams的Width和Height属性来得到控件的宽高,同样可以利用控件的setLayoutParams( )方法来动态的设置其宽高,其中LayoutParams是继承于Android.view.viewGroup.LayoutParams,LayoutParams类是用于child view(子视图)向parent view(父视图)传递布局(Layout)信息包,它封装了Layout的位置、宽、高等信息。

Bitmap的内存优化:

及时回收Bitmap的内存:Bitmap类有一个方法recycle( ),用于回收该Bitmap所占用的内存,当保证某个Bitmap不会再被使用(因为Bitamap被强制释放后,再次使用它会抛出异常)后,能够在Activity的onStop()方法或onDestory()方法中将其回收,回收方法:

if ( bitmap !=null && !bitmap.isRecycle ( ) ){
        bitmap.recycle( );
        bitmap=null ;
        }

   System.gc( ) ;
 

System.gc()方法可以加快系统回收内存的到来;

捕获异常:为了避免应用在分配Bitmap内存时出现OOM异常以后Crash掉,需在对Bitmap实例化的过程中进行OutOfMemory异常的捕获;

缓存通用的Bitmap对象:缓存分为硬盘缓存和内存缓存,将常用的Bitmap对象放到内存中缓存起来,或将从网络上获取到的数据保存到SD卡中;

压缩图片:即以缩略图的形式显示图片。

使用View绘制视图

View类是Android平台中各种控件的父类,是UI(用户界面)的基础构件,View相当于屏幕上的一块矩形区域,其重复绘制这个区域和处理事件,View是所有Weight类(组件类)的父类,其子类ViewGroup是所有Layout类的父类,如果需要自定义控件,也要继承View,实现onDraw方法和事件处理方法。

View绘制的流程:OnMeasure()——>OnLayout()——>OnDraw()

OnMeasure():测量视图大小,从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。

OnLayout():确定View的位置,进行页面的布局,从顶层父View向子View的递归调用View.Layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放到合适的位置上。

OnDraw():绘制视图,ViewGroup创建一个Canvas对象,调用OnDraw()方法,但OnDraw方法是个空方法,需要自己根据OnMeasure和OnLayout获取得到参数进行自定义绘制。

注意:在Activity的生命周期中没有办法获取View的宽高,原因就是View没有测量完。

Canvas类:Canvas类位于android.graphics包中,它表示一块画布,可以使用该类提供的方法,绘制各种图形图像,Canvas对象的获取有两种方式:一种是重写View.onDraw方法时,在该方法中Canvas对象会被当作参数传递过来,在该Canvas上绘制的图形图像会一直显示在View中,另一种是借助位图创建Canvas,参考代码:

Bitmap  bitmap = Bitmap.CreatBitmap(100,100,Bitmap.Config,ARGB_888);

Canvas  canvas = new Canvas(bitmap);
 

Android中页面的横屏与竖屏操作:

在配置文件中为Activity设置属性 android:screenOrientation = “ string ” ; 其中string为属性值,landscape(横屏)或portrait(竖屏);

在代码中设置:

		setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
 

setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);为防止切换后重新启动当前Activity,应在配置文件中添加android:configChanges=“keyboardHidden|orientation”属性,并在Activity中重写onConfigurationChanged方法。

获取手机中屏幕的宽和高的方法:

通过Context的getWindowManager()方法获得WindowManager对象,再从WindowManager对象中通过getDefaultDisplay获得Display对象,再从Display对象中获得屏幕的宽和高。

Android内存泄漏及管理

内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of Memory 的错误,比如申请了一个integer,但给它存了long才能存下的数,这就是内存溢出,通俗来讲,内存溢出就是内存不够用。

内存泄漏(Memory Leak):是指程序使用new关键字向系统申请内存后,无法释放已经申请的内存空间,一次内存泄漏可以忽略,但多次内存泄漏堆积后会造成很严重的后果,会占用光内存空间。

以发生的方式来看,内存泄漏可以分为以下几类:

常发性内存泄漏:发生内存泄漏的代码会被执行多次,每次执行都会导致一块内存泄漏;

偶发性内存泄漏:发生内存泄露的代码只有在某些特定的环境或情况下才会发生;

一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或是由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏;

隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到程序结束的时候才会释放内存;

常见造成内存泄漏的原因:

单例模式引起的内存泄漏:由于单例的静态特性使得单例的生命周期和程序的生命周期一样长,如果一个对象(如Context)已经不再使用,而单例对象还持有对象的引用就会造成这个对象不能正常回收;

Handler引起的内存泄漏:子线程执行网络任务,使用Handler处理子线程发送的消息,由于Hnadler对象是非静态匿名内部类的实例对象,持有外部类(Activity)的引用,在Handler Message中,Looper线程不断轮询处理消息,当Activity退出时还有未处理或正在处理的消息时,消息队列中的消息持有Handler对象引用,Handler又持有Activity,导致Activity的内存和资源不能及时回收;

线程造成内存泄漏:匿名内部类Runnable和AsyncTask对象执行异步任务,当前Activity隐式引用,当前Activity销毁之前,任务还没有执行完,将导致Activity的内存和资源不能及时回收;

资源对象未关闭造成的内存泄漏:对于使用了BroadcastReceiver、File、Bitmap等资源,应该在Activity销毁之前及时关闭或注销它们,否则这些资源将不会回收,造成内存泄漏;

内存泄漏的检测工具:LeakCanary

LeakCanary是Square公司开源的一个检测内存泄漏的函数库,可以在开发的项目中集成,在Debug版本中监控Activity、Fragment等的内存泄漏,LeakCanaty集成到项目之后,在检测内存泄漏时,会发送消息到系统通知栏,点击后会打开名称DisplayLeakActivity的页面,并显示泄露的跟踪信息,Logcat上面也会有对应的日志输出。

Android设计模式之MVC

MVC即Model-View-Controller,M是模型,V是视图,C是控制器,MVC模式下系统框架的类库被划分为模型(Model)、视图(View)、控制器(Controller),模型对象负责建立数据结构和相应的行为操作处理,视图负责在屏幕上渲染出相应的图形信息,展示给用户看,控制器对象负责截获用户的按键和屏幕触摸事件,协调Model对象和View对象。

用户与视图交互,视图接收并反馈用户的动作,视图把用户的请求传给相应的控制器,由控制器决定调用哪个模型,然后由模型调用相应的业务逻辑对用户请求进行加工处理,如果需要返回数据,模型会把相应的数据返回给控制器,由控制器调用相应的视图,最终由视图格式化和渲染返回的数据,一个模型可以有多个视图,一个视图可以有多个控制器,一个控制器可以有多个模型。

Model(模型):Model是一个应用系统的核心部分,代表了该系统实际要实现的所有功能处理,比如在视频播放器中,模型代表了一个视频数据库及播放视频的程序和函数代码,Model在values目录下通过xml文件格式生成,也可以由代码动态生成,View和Model是通过桥梁Adapter来连接起来的;

View(视图):View是软件应用传给用户的一个反馈结果,它代表了软件应用中的图形展示,声音播放、触觉反馈等,视图的根节点是应用程序的自身窗口,View在Layout目录中通过xml文件格式生成,用findViewById()获取,也可以通过代码动态生成;

Controller(控制器):Controller在软件应用中负责对外部事件的响应,包括:键盘敲击、屏幕触摸、电话呼入等,Controller实现了一个事件队列,每一个外部事件均在事件队列中被唯一标识,框架依次将事件从队列中移出并派发出去;

JVM运行原理详解

JVM简析:说起Java,我们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:

Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示: 运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。

JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。

下面我们从JVM的基本概念和运过程程这两个方面入手来对它进行深入的研究。

2.JVM基本概念

(1)基本概念:JVM是可运行Java代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM是运行在操作系统之上的,它与硬件没有直接的交互。

(2)运行过程:我们都知道Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码 。也就是如下: ① Java源文件—->编译器—->字节码文件;② 字节码文件—->JVM—->机器码

每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

(3)三种JVM:① Sun公司的HotSpot;② BEA公司的JRockit;③ IBM公司的J9JVM;

在JDK1.7及其以前我们所使用的都是Sun公司的HotSpot,但由于Sun公司和BEA公司都被oracle收购,jdk1.8将采用Sun公司的HotSpot和BEA公司的JRockit两个JVM中精华形成jdk1.8的JVM。

3.JVM的体系结构

(1)Class Loader类加载器: 负责加载 .class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定。①定位和导入二进制class文件;② 验证导入类的正确性;③ 为类分配初始化内存;④ 帮助解析符号引用.

(2)Native Interface本地接口:本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体作法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。

(3)Execution Engine 执行引擎:执行包在装载类的方法中的指令,也就是方法。

(4)Runtime data area 运行数据区:虚拟机内存或者Jvm内存,冲整个计算机内存中开辟一块内存存储Jvm需要用到的对象,变量等,运行区数据有分很多小区,分别为:方法区,虚拟机栈,本地方法栈,堆,程序计数器。

4.JVM数据运行区详解(栈管运行,堆管存储):

说明:JVM调优主要就是优化 Heap堆 和 Method Area 方法区。

(1)Native Method Stack本地方法栈:它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。

(2)PC Register程序计数器:每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

(3)Method Area方法区:方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。静态变量+常量+类信息+运行时常量池存在方法区中,实例变量存在堆内存中。

(4)Stack 栈: ① 栈是什么,栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。② 栈存储什么?栈帧中主要保存3类数据:本地变量(Local Variables):输入参数和输出参数以及方法内的变量;栈操作(Operand Stack):记录出栈、入栈的操作; 栈帧数据(Frame Data):包括类文件、方法等等。③栈运行原理,栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈……依次执行完毕后,先弹出后进…F3栈帧,再弹出F2栈帧,再弹出F1栈帧。遵循“先进后出”/“后进先出”原则。

(5) Heap 堆:堆这块区域是JVM中最大的,应用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 gc 主要的回收区,一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:

① 新生区:新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1去也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullGCC),进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二: a.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

养老区:养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。

③ 永久区:永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。 如果出现java.lang.OutOfMemoryError:PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。原因有二:

a.程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。

b.大量动态反射生成的类不断被加载,最终导致Perm区被占满。

方法区和堆内存的异议: 实际而言,方法区和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。

常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。

Android平台的虚拟机Dalvik

Dalvik概述:Dalvik是Google公司自己设计用于Android平台的Java虚拟机。它可以支持已转换为.dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,可以减少整体文件尺寸,提高I/o操作的类查找速度所以适合内存和处理器速度有限的系统。

Dalvik虚拟机(DVM)和Java 虚拟机(JVM)首要差别:Dalvik 基于寄存器,而JVM 基于栈。性能有很大的提升。基于寄存器的虚拟机对于更大的程序来说,在它们编译的时候,花费的时间更短。

寄存器的概念:寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC),在中央处理器的算术及逻辑部件中,包含的寄存器有累加器(ACC)。

栈的概念:栈是线程独有的,保存其运行状态和局部自动变量的(所以多线程中局部变量都是相互独立的,不同于类变量)。栈在线程开始的时候初始化(线程的Start方法,初始化分配栈),每个线程的栈互相独立。每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

DVM进程的设计规则:

每个应用程序都运行在它自己的Linux空间。在需要执行该应用程序时Android将启动该进程,当不再需要该应用程序,并且系统资源分配不够时,则系统终止该进程。

每个应用程序都有自己的(DVM),所以任一应用程序的代码与其他应用程序的代码是相互隔离的。

默认情况下,每个应用程序都给分配一个唯一的Linux用户ID。所以应用程序的文件只能对该应用程序可见。

所以说每个应用程序都拥有一个独立的DVM,而每个DVM在Linux中又是一个进程,所以说DVM进程和Linux进程可以说是一个概念。

Android 应用程序的编译:Android所有类都通过JAVA编译器编译,然后通过Android SDK的“dex文件转换工具”转换为“dex”的字节文件,再由DVM载入执行。

Android ART模式简介:Android4.4引入ART模式来代替Dalvik虚拟机。ART是AndroidRuntime的缩写,它提供了以AOT(Ahead-Of-Time)的方式运行Android应用程序的机制。所谓AOT是指在运行前就把中间代码静态编译成本地代码,这就节省了JIT运行时的转换时间。因此,和采用JIT的Dalvik相比,ART模式在总体性能有了很大的提升,应用程序不但运行效率更高,耗电量更低,而且占用的内存也更少;ART和dalvik相比,系统的性能得到了显著提升,同时占用的内存更少,因此能支持配置更低的设备。但是ART模式下编译出来的文件会比以前增大10%-20%,系统需要更多的存储空间,同时因为在安装时要执行编译,应用的安装时间也比以前更长了;ART和dalvik相比,系统的性能得到了显著提升,同时占用的内存更少,因此能支持配置更低的设备。但是ART模式下编译出来的文件会比以前增大10%-20%,系统需要更多的存储空间,同时因为在安装时要执行编译,应用的安装时间也比以前更长了。

Java的内存分配

Java内存分配主要包括以下几个区域: 1. 寄存器:我们在程序中无法控制;2. 栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中;3. 堆:存放用new产生的数据;4. 静态域:存放在对象中用static定义的静态成员;5. 常量池:存放常量;6. 非RAM(随机存取存储器)存储:硬盘等永久存储空间。

Java内存分配中的栈:在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

Java内存分配中的堆:堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或对象后,还可以 在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!

Java内存分配中的常量池 (constant pool):常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如: 1.类和接口的全限定名;2.字段的名称和描述符; 3.方法和名称和描述符。虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和 floating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。在程序执行的时候,常量池会储存在Method Area,而不是堆中。

堆与栈:Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、 anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。 栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。栈有一个很重要的特殊性,就是存在栈中的数据可以共享

Android中的Binder机制

Binder是Android系统进程间通信(IPC)方式之一。Linux已经拥有的进程间通信IPC手段包括(Internet Process Connection):管道(Pipe)、信号(Signal)和跟踪(Trace)、插口(Socket)、报文队列(Message)、共享内存(Share Memory)和信号量(Semaphore)。Binder可以提供系统中任何程序都可以访问的全局服务。Android的Binder的框架如下:

上图中涉及到Binder模型的4类角色:Binder驱动,ServiceManager,Server和Client。 因为后面章节讲解Binder时,都是以MediaPlayerService和MediaPlayer为代表进行讲解的;这里就使用MediaPlayerService代表了Server,而MediaPlayer则代表了Client。

Binder机制的目的是实现IPC(Inter-ProcessCommunication),即实现进程间通信。在上图中,由于MediaPlayerService是Server的代表,而MediaPlayer是Client的代表;因此,对于上图而言,Binder机制则表现为"实现MediaPlayerService和MediaPlayer之间的通信"。

Android中的缓存机制

移动开发本质上就是手机和服务器之间进行通信,需要从服务端获取数据。反复通过网络获取数据是比较耗时的,特别是访问比较多的时候,会极大影响了性能,Android中可通过缓存机制来减少频繁的网络操作,减少流量、提升性能。

实现原理:把不需要实时更新的数据缓存下来,通过时间或者其他因素 来判别是读缓存还是网络请求,这样可以缓解服务器压力,一定程度上提高应用响应速度,并且支持离线阅读。

Bitmap的缓存:在许多的情况下(像 ListView, GridView 或 ViewPager 之类的组件 )我们需要一次性加载大量的图片,在屏幕上显示的图片和所有待显示的图片有可能需要马上就在屏幕上无限制的进行滚动、切换。像ListView, GridView 这类组件,它们的子项当不可见时,所占用的内存会被回收以供正在前台显示子项使用。垃圾回收器也会释放你已经加载了的图片占用的内存。如果你想让你的UI运行流畅的话,就不应该每次显示时都去重新加载图片。保持一些内存和文件缓存就变得很有必要了。

使用内存缓存:通过预先消耗应用的一点内存来存储数据,便可快速的为应用中的组件提供数据,是一种典型的以空间换时间的策略。LruCache 类(Android v4 Support Library 类库中开始提供)非常适合来做图片缓存任务,它可以使用一个LinkedHashMap 的强引用来保存最近使用的对象,并且当它保存的对象占用的内存总和超出了为它设计的最大内存时会把不经常使用的对象成员踢出以供垃圾回收器回收。给LruCache 设置一个合适的内存大小,需考虑如下因素:

还剩余多少内存给你的activity或应用使用屏幕上需要一次性显示多少张图片和多少图片在等待显示 手机的大小和密度是多少(密度越高的设备需要越大的 缓存) 图片的尺寸(决定了所占用的内存大小) 图片的访问频率(频率高的在内存中一直保存)保存图片的质量(不同像素的在不同情况下显示);

使用磁盘缓存:内存缓存能够快速的获取到最近显示的图片,但不一定就能够获取到。当数据集过大时很容易把内存缓存填满(如GridView )。你的应用也有可能被其它的任务(比如来电)中断进入到后台,后台应用有可能会被杀死,那么相应的内存缓存对象也会被销毁。当你的应用重新回到前台显示时,你的应用又需要一张一张的去加载图片了。磁盘文件缓存能够用来处理这些情况,保存处理好的图片,当内存缓存不可用的时候,直接读取在硬盘中保存好的图片,这样可以有效的减少图片加载的次数。读取磁盘文件要比直接从内存缓存中读取要慢一些,而且需要在一个UI主线程外的线程中进行,因为磁盘的读取速度是不能够保证的,磁盘文件缓存显然也是一种以空间换时间的策略。

使用SQLite进行缓存:网络请求数据完成后,把文件的相关信息(如url(一般作为唯一标示),下载时间,过期时间)等存放到数据库。下次加载的时候根据url先从数据库中查询,如果查询到并且时间未过期,就根据路径读取本地文件,从而实现缓存的效果。

文件缓存:思路和一般缓存一样,把需要的数据存储在文件中,下次加载时判断文件是否存在和过期(使用File.lastModified()方法得到文件的最后修改时间,与当前时间判断),存在并未过期就加载文件中的数据,否则请求服务器重新下载。

Android 中图片的三级缓存策略

三级缓存: ?内存缓存,优先加载,速度最快; ?本地缓存,次优先加载,速度快;?网络缓存,最后加载,速度慢,浪费流量 ;

三级缓存策略,最实在的意义就是 减少不必要的流量消耗,增加加载速度。

三级缓存的原理:

  1. 首次加载的时候通过网络加载,获取图片,然后保存到内存和 SD 卡中。
  2. 之后运行 APP 时,优先访问内存中的图片缓存。
  3. 如果内存没有,则加载本地 SD 卡中的图片。

具体的缓存策略可以是这样的:内存作为一级缓存,本地作为二级缓存,网络加载为最后。其中,内存使用 LruCache ,其内部通过 LinkedhashMap 来持有外界缓存对象的强引用;对于本地缓存,使用 DiskLruCache。加载图片的时候,首先使用 LRU 方式进行寻找,找不到指定内容,按照三级缓存的方式,进行本地搜索,还没有就网络加载。