Java-Android面试题重难点集锦(中高级)

Java知识点: 
0、数据类型:
 基本数据类型:byte(1个字节8位)、short(2个字节16位)、int(4个字节32位)、long(8个字节64位)、
float(4个字节32位)、double(8个字节64位),boolean、char(2个字节16位)
引用数据类型:类、接口、数组,枚举,注解

访问控制:
四种级别从小到大排列为 
private---default---protected---public
private(类访问):该类下其他成员访问
default(包访问):本包中的其他类访问
protected(子类访问):同一包下的其他类或不同包下该类的子类
public(公共访问):所有的类或不是同一包下的类访问

equals() 方法用于比较字符串中的字符是否相等,==比较字符对象的地址是否相同

String  s1 = "abc";      s1 = new String("abc");
String  s2 = "abc";       String s2 = "abc";
s1 == s2   (true)           s1 == s2   (false)

String s1 = "a"+"b"+"c";         String  s1 = "ab":
String s2 = "abc";                  String s2 = "abc"; 
s1 == s2 (true)                       String s3 = s1+"c";
                                                 s1 == s3 (false)

父类的静态方法能否被子类重写?
不能,父类的静态方法能够被子类继承,但是不能够被子类重写,即使子类中的静态方法与父类中的静态方法完全一样,也是两个完全不同的方法

1、抽象类的定义和特点
   当描述事物时没有足够的信息对事物进行描述,该类就是抽象类
   特点:  1 没有方法体的方法是抽象方法,定义在抽象类中
                2抽象类调用方法用abstract修饰
                3不可以实例化,调用抽象方法无意义
                4抽象类一定要被继承必须有子类覆盖所有的抽象的方法后子类进行实例化
                5抽象类是个父类有构造方法不可以定义抽象方法,不可以和final , private ,static共存
2、接口的特点以及抽象类的异同点
  
   接口特点:1用interface定义    2成员有固定的修饰符  3不能被实例化   4接口必须有子类覆盖所有的抽象方法后才可以实例化                           5接口中的成员都是public修饰的
      共同点:都是不断抽取而来的
      不同点:1 接口中可以有常量不能有变量,抽象类中可以有常量和变量
                      2接口多实现,抽象类单继承
                      3抽象类中定义抽象内容直接提供给子类使用,接口中只能定义抽象方法,需要子类全部实现。
3、java中实现多态的机制是什么
      重写和重载是java多态性的不同表现。重写是父类和子类之间多态性的一种表现,重载是一个类中多态性的一种表现。
     如果在子类中定义的某个方法和其父类有相同的名称和参数,我们说该方法被重写,子类的对象使用这个方法时将调用子类中的定义,对他而言父类中的定义如同被屏蔽了。
     如果在一个类中定义了多个同名的方法,他们或有不同的参数个数或有不同的参数类型,则称为方法的重载,可以改变返回值的类型。
4、Person p =  new Person 在内存中做了哪些事情?
           A  将Person的class加载到内存
           B  在栈内存中开辟一个变量空间p
           C  在堆内存中创建一个对象person
           D  把peraon中的属性进行默认和初始化
           E  把对象内存地址赋给p变量,让p变量指向该对象
 5、string和stringBuffer的区别以及StringBuffer和StringBuilder的区别
       String重写了equals方法比较的是内容,stringbuffer比较的是地址,内容可变化
      StringBuffer  非同步,单线程访问效率高,不安全
      StringBuilder 同步,多线程访问安全,效率低
6、ArrayList,LinkedList和Vector的异同
        ArrayList和vector相同点底层都是数组结构  ,LinkedList底层是链表结构
        Vector:安全,效率低,被arraylist取代节省空间,ArrayList 每次 resize 增加 1.5 倍的容量,Vector 每次增加 2倍的容量
       ArrayList: 不安全,查询速度快,增删慢,效率高
        LinkedList: 不安全,链表结构,增删快,查询慢,效率高

6.1、为什么ArrayList查询速度快于LinkedList

ArrayList:为数组,也就是内存中一片连续的空间,当get(index)的时候,我可以根据数组的(首地址+偏移量),直接计算出我想访问的第index个元素在内存中的位置。
LinkedList:为链表,在内存中开辟的不是一段连续的空间,每个元素有一个[元素|下一元素地址]这样结构。当get(index)时,只能从首元素开始,依次获得下一个元素地址。

用时间复杂度表示的话,ArrayList的get(n)是o(1),而LinkedList是o(n)。
1、ArrayList具体说下1.7和1.8版本初始化的时候的区别么?
(1)arrayList1.7初始化的时候会调用this(10)才是真正的容量为10,1.7之后默认走了空数组,只有第一次add时候容量变成10。(2)、如果长度不够,是需要扩容的,1.8之后的效率更高了,采用了位运算,右移一位
扩容例子 :arraycopy   比如有下面这样一个数组 a={1,2,3,4,5,6}我需要在index 5的位置去新增一个元素A,源码中复制到一个新的数组,在第5个位置放入A,造成效率更低,若有100万条数据,都要复制,涉及扩容问题,插入效率更慢。

6.2、SparseArray和ArrayMap的优缺点以及应用场景:
 SparseArray优点:
         1、避免存取元素时拆箱和装箱    Java中的自动装箱与拆箱
(int转为Integer类型)它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value
由于key指定为int类型,也可以节省int-Integer装箱拆箱操作带来的性能消耗

int m = 500;

Integer obj = new Integer(m); // 手动装箱
int n = obj.intValue(); // 手动拆箱

Integer obj = m; // 自动装箱
int n = obj; // 自动拆箱
private int[] mKeys;
private Object[] mValues;

2、定期通过gc函数来清理内存,利用率更高

 缺点:1、二分查找的时间复杂度O(log n),大数据量的情况下添加、查找、删除数据都需要先进行一次二分查找,将降低至少50%
              2、key只能是int 或者long
      应用场景:1、item数量为 <1000级别的
                       2、存取的value为指定类型如int可以避免自动装箱和拆箱问题。

存:
put添加数据的时候都需要调用二分查找和之前的key比较当前我们添加的元素的key的大小,然后按照从小到大的顺序排列好, 而在获取数据的时候,也是使用二分查找法判断元素的位置

public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key); //二分查找.

        if (i >= 0) {  //如果当前这个i在数组中存在,那么表示插入了相同的key值,只需要将value的值进行覆盖..
            mValues[i] = value;
        } else {  //如果数组内部不存在的话,那么返回的数值必然是负数.
            i = ~i;  //因此需要取i的相反数.
            //i值小于mSize表示在这之前. mKey和mValue数组已经被申请了空间.只是键值被删除了.那么当再次保存新的值的时候.不需要额外的开辟新的内存空间.直接对数组进行赋值即可.
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            //当需要的空间要超出,但是mKey中存在无用的数值,那么需要调用gc()函数.
            if (mGarbage && mSize >= mKeys.length) {
                gc();
                
                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            //如果需要的空间大于了原来申请的控件,那么需要为key和value数组开辟新的空间.
            if (mSize >= mKeys.length) {
                int n = ArrayUtils.idealIntArraySize(mSize + 1);
                //定义了一个新的key和value数组.需要大于mSize
                int[] nkeys = new int[n];
                Object[] nvalues = new Object[n];

                // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
                //对数组进行赋值也就是copy操作.将原来的mKey数组和mValue数组的值赋给新开辟的空间的数组.目的是为了添加新的键值对.
                System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
                System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
                //将数组赋值..这里只是将数组的大小进行扩大..放入键值对的操作不在这里完成.
                mKeys = nkeys;
                mValues = nvalues;
            }
            //如果i的值没有超过mSize的值.只需要扩大mKey的长度即可.
            if (mSize - i != 0) {
                // Log.e("SparseArray", "move " + (mSize - i));
                System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
                System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
            }
            //这里是用来完成放入操作的过程.
            mKeys[i] = key;
            mValues[i] = value;
            mSize++;
        }
    }

取      
public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        ...
        }
 public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        ...
        }
 

ArrayMap优点:
                1、在数据量少时,内存利用率高,及时的空间压缩机制
                2、迭代效率高,可以使用索引来迭代(keyAt()方法以及valueAt() 方法
   ArrayMap缺点:
                1、存取复杂度高,花费大
                 2、二分查找的O(log n )时间复杂度远远小于HashMap
                 3、ArrayMap没有实现Serializable,不利于在Android中借助Bundle传输。
   ArrayMap应用场景:1、item数量为 <1000 级别的,尤其是在查询多,插入数据和删除数据不频繁的情况
                                 2、Map中包含子Map对象

什么情况下使用二者:
      1、如果key的类型已经确定为int类型,那么使用SparseArray,因为它避免了自动装箱的过程,如果key为long类型,一个LongSparseArray来确保key为long类型时的使用
     2、如果key类型为其它的类型,则使用ArrayMap

 

SpareArray和HashMap对比---内存上---时间复杂度上

HashMap中put操作元素的时间复杂度

第一步:key.hashcode(),时间复杂度O(1)。
第二步:判断桶里是否有元素,如果没有,直接new一个entey节点插入到数组中。时间复杂度O(1)。
第三步:如果桶里有元素,并且元素个数小于6转化为链表,则调用equals方法,比较是否存在相同名字的key,不存在则new一个entry插入都链表尾部。时间复杂度O(1)+O(n)=O(n)
第四步:如果桶里有元素,并且元素个数大于8转化为红黑树,则调用equals方法,比较是否存在相同名字的key,不存在则new一个entry插入都链表尾部。时间复杂度O(1)+O(logn)=O(logn)

通过上面的分析,我们可以得出结论
HashMap的hash操作的时间复杂度是O(1),
HashMap的equals操作时链表的时间复杂度是O(n)
HashMap的equals操作时红黑树的时间复杂度是O(logn)

HashMap 为了应对哈希冲突引入负载因子它的默认值是 0.75。容量达到了指定尺寸的 0.75%,就会开始扩容,也就是必然有 25% 的空间是不存储数据而被浪费的。而 SparseArray 是可以把数组利用到最后一个空间,不会轻易扩容。


7、HashMap和HashTable的区别
       共同点:底层都是hash算法,都是双列集合
       不同点:1 hashmap 线程不安全,效率高,hashtable线程安全,效率低
                       2 hashmap可以存储null键和null值,hashtable不可以
原因描述:Hashtable在我们put 空值的时候会直接抛空指针异常,使用的是安全失败机制,此次读到的数据不一定是最新的数据
如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。
3、初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
4、扩容机制不同:HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1

8、HashMap 和 LinkedHashMap 的区别
HashMap 底层是数组 + 单链表 + 红黑树的数据结构
LinkedHashMap 底层是 数组 + 单链表 + 红黑树 + 双向链表的数据结构
LinkedHashMap 存储元素是无序的,由于双向链表的存在迭代时获取元素的顺序等于元素的添加顺序,通过构造参数 accessOrder 来指定双向链表是否在元素被访问后改变其在双向链表中的位置

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
   map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
//调用 LinkedHashMap 的构造方法,该方法初始化了初始起始容量16,以及加载因子0.75f,
public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;   即迭代顺序不等于访问顺序
}

8.1、Hashmap为什么java8之前是头插法,之后是尾插法?

Hashmap插入
java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去。每一个节点都会保存自身的hash、key、value、以及下个节点。

HashMap的扩容机制:什么时候扩容有两个因素
Capacity:HashMap当前长度。   LoadFactor:负载因子,默认值0.75f。

未扩容hash计算公式:tab[i == (n-1) & hash] == null

如何扩容的?
1、创建一个新的Entry空数组,长度是原数组的2倍
2、遍历原Entry数组,把所有的Entry重新Hash到新数组
为什么要重新Hash呢,因为长度扩大以后,Hash的规则也随之改变,位置发生变化。
 扩容后hash计算公式:newTab[e.hash & (newCap -1)]


java8之前是头插法:
举个例子:现在我们要在容量为2的容器里面用不同线程插入A,B,C,假如我们在resize之前打个断点,那意味着数据都插入了但是还没resize那扩容前可能是这样的。

我们可以看到链表的指向A->B->C

因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上

就可能出现下面的情况,大家发现问题没有?
B的下一个指针指向了A,一旦几个线程都调整完成,就可能出现环形链表Infinite Loop



java8之后是尾插法:
因为java8之后链表有红黑树的部分(平衡二叉树,左右保持平衡),红黑树的引入将原本O(n)的时间复杂度降低到了O(logn)
使用头插会改变链表的上的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了
就是说原本是A->B,在扩容后那个链表还是A->B

8.2、简述 HashMap 的工作原理 JDK 1.8后做了哪些优化

1、JDK 1.7 采用单链表 + 数组的存储,JDK1.8之后 单链表长度超过 8之后会使用红黑树替换单链表来提高效率

2、HashMap 通过键值对的 key 的 hashCode 值经过扰动函数处理后确定存储的数组角标位置,1.7 中扰动函数使用了 4次位运算 + 5次异或运算,1.8 中降低到 1次位运算 + 1次异或运运算

3、HashMap 扩容的时候会增加原来数组长度两倍,并对所存储的元素节点hash 值的重新计算,1.7中 HashMap 会重新调用 hash 函数计算新的位置,
而 1.8中第6行    if ((p = tab[i = (n - 1) & hash]) == null)   // 如果没有hash碰撞则直接插入元素
如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样该位置数据为null,这线程A、B都会进入第6行代码中。
假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全

4、HashMap 在多线程使用前提下,在jdk1.7中扩容时会造成环形链或数据丢失,在jdk1.8中会发生数据覆盖的情况。

8.3、ConcurrentHashMap 工作原理及jdk1.7和1.8区别       ConcurrentHashMap原理解析保证线程安全

从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树

1、JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,JDK1.8的实现降低锁的粒度,就是HashEntry(首节点)

2、JDK1.8版本的数据结构变得更加简单,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了

3、JDK1.8使用红黑树来优化链表遍历效率是很快的,JDK1.7基于链表的遍历是一个很漫长的过程,而代替一定阈值的链表

JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点:

(1)、因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
(2)、JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大
(3)、在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存
 

Segment要找到值经过几次hash?
ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表

(1) 从1.7到1.8版本,由于HashEntry从链表 变成了红黑树所以 concurrentHashMap的时间复杂度从O(n)到O(log(n))
(2)  HashEntry最小的容量为2


jdk1.8put数据的流程:
1、根据 key 计算出 hashcode     int hash = spread(key.hashCode())

2、判断是否需要进行初始化。  
      if (tab == null || (n = tab.length) == 0)
           tab = initTable()

3、即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))

4、如果当前位置的 hashcode == MOVED ==则需要进行扩容。
      else if ((fh = f.hash) == MOVED)
          tab = helpTransfer(tab, f)


5、如果都不满足,则利用 synchronized 锁写入数据。
      synchronized (f) {}

6、如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
      if (binCount >= TREEIFY_THRESHOLD)
           treeifyBin(tab, i)


8.4、HashMap初始容量为什么是2的n次幂及扩容为什么是2倍的形式;

 第一个截图是向HashMap中添加元素putVal()方法的部分源码,向集合中添加元素时,会使用(n - 1) & hash的计算方法来得出该元素在集合中的位置;而第二个截图是HashMap扩容时调用resize()方法中的部分源码,可以看出会新建一个tab,然后遍历旧的tab,将旧的元素进过e.hash & (newCap - 1)的计算添加进新的tab中,也就是(n - 1) & hash的计算方法,其中n是集合的容量,hash是添加的元素进过hash函数计算出来的hash值。

  HashMap的容量为什么是2的n次幂,和这个(n - 1) & hash的计算方法有着千丝万缕的关系,符号&是按位与的计算与添加元素的hash值进行位运算时,能够充分的散列,添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

  当HashMap的容量是16时,它的二进制是10000,(n-1)的二进制是01111,与hash值得计算结果如下:

上面四种情况我们可以看出,不同的hash值,和(n-1)进行位运算后,能够得出不同的值,使得添加的元素能够均匀分布在集合中不同的位置上,避免hash碰撞。

 下面就来看一下HashMap的容量不是2的n次幂的情况,当容量为10时,二进制为01010,(n-1)的二进制是01001,向里面添加同样的元素,结果为:

有三个不同的元素进过&运算得出了同样的结果,严重的hash碰撞了
HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!

8.5、HashMap默认加载因子为什么是0.75?

提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,

HashMap有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容、rehash操作(即重建内部数据结构),扩容后的哈希表将具有两倍的原容量。

加载因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但同时也增加了查询时间成本;
加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数。

在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash操作次数,所以,一般在使用HashMap时建议根据预估值设置初始容量,减少扩容操作。时间和空间成本上寻求的一种折中选择.


9、反射通过3种方式获取字节码对象         反射原理解析
对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性
      (1)对象.getClass()                  Object类的getClass()方法,判断两个方法是否是同一个字节码文件
      (2)类名.class                            静态属性class锁对象
      (3)class.forName()                  class类中静态方法forName(),读取配置文件
通过反射创建对象:

1、无参构造方法:使用newInstance()方法

    //传入要实例化类完整的"包,类"名称
    Class clazz = Class.forName("e.com.javatest.Person");
   //实例化person对象
    Person p = (Person) clazz.newInstance();

2、有参的构造方法:使用getConstructors()获取全部构造方法

       Class clazz = Class.forName("e.com.javatest.Person");
        //通过反射获取全部构造方法
        Constructor[] cons = clazz.getConstructors();
        //传递参数,实例化person对象,只有一个构造方法,所以数组角标为0
        Person p = (Person) cons[0].newInstance("小明");

通过反射访问属性:

在反射机制中,属性的操作是通过Field类实现的,如果访问是私有的
使用Field类中的setAccessible()方法将需要操作的属性设置为可以被外界所访问。

        Class clazz = Class.forName("e.com.javatest.Person");
        Person p = (Person) clazz.newInstance();
        //获取person类中指定名称的属性
        Field name = clazz.getDeclaredField("name");
        //通过反射访问该属性时取消权限检查
        name.setAccessible(true);

通过反射调用方法

通过class对象的getMethod()方法获取全部方法,通过Method中的invoke()调用对应的方法

        Class clazz = Class.forName("e.com.javatest.Person");
        //获取person类中的sayHello()方法,一个形参name
        Method md = clazz.getMethod("sayHello", String.class);
        或
        Method md = clazz.getDeclaredMethod("sayHello", String.class);
        //调用sayHello方法
        md.invoke(clazz.newInstance(),"小明");


10、Throw和Throws关键字的区别
       throw:用于抛出异常对象,后面跟的是异常对象,用在函数内。
      throws::用于抛出异常类,后面跟的是异常类名,可以跟多个,用在函数上。
11、wait和sleep的区别
       (1)wait线程会释放执行权,释放锁, sleep线程会释放执行权,但是不释放锁
       (2)wait只能在同步控制方法或者同步控制块里面使用,sleep可以在任何地方使用
       (3)sleep方法来自thread,wait方法来自object
       (4)sleep必须捕获异常,而wait,notify,notifyAll不需要捕获异常。

13、集合

14、Java对象的生命周期是什么?

(1)加载:将类的信息加载到JVM的方法区,然后在堆区中实例化一个java.lang.Class对象,作为方法去中这个类的信息入口。

(2)连接:验证:验证类是否合法。准备:为静态变量分配内存并设置JVM默认值,非静态变量不会分配内存。解析:将常量池里的符号引用转换为直接引用。

(3)初始化:初始化类的静态赋值语句和静态代码块,主动引用会被触发类的初始化,被动引用不会触发类的初始化。

(4)使用:执行类的初始化,主动引用会被触发类的初始化,被动引用不会触发类的初始化。

(5)卸载:卸载过程就是清楚堆里类的信息,以下情况会被卸载:① 类的所有实例都已经被回收。② 类的ClassLoader被回 收。③ 类的CLass对象没有被任何地方引用,无法在任何地方通过 反射访问该类。

15、hashcode()和equals()的作用、区别、联系      hashCode()方法和equal()方法的作用其实一样,在Java里都是用来对比两个对象是否相等一致。
      因为重写的equal()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么hashCode()既然效率这么高为什么还要equal()呢?
     因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:
       1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。
       2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
所有对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等,如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性!

16、集合在遍历过程中是否可以删除元素,为什么迭代器就可以安全删除元素

1、集合在使用 for 循环或者高级 for 循环迭代的过程中不允许使用,集合本身的 remove 方法删除元素,如果进行错误操作将会导致 ConcurrentModificationException异常的发生
2、Iterator 可以删除访问的当前元素(current),一旦删除的元素是Iterator 对象中 next 所正在引用的,在 Iterator 删除元素通过 修改 modCount 与 expectedModCount 的值,可以使下次在调用 remove 的方法时候两者仍然相同因此不会有异常产生。

18、Object的方法有哪些?

registerNatives()   //私有方法
getClass()    //返回此 Object 的运行类。
hashCode()    //用于获取对象的哈希值。
equals(Object obj)     //用于确认两个对象是否“相同”。
clone()    //创建并返回此对象的一个副本。
toString()   //返回该对象的字符串表示。
notify()    //唤醒在此对象监视器上等待的单个线程。
notifyAll()     //唤醒在此对象监视器上等待的所有线程。
wait()    //用于让当前线程失去操作权限,当前线程进入等待序列
finalize()    //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。

19、并发编程的3个概念:原子性、可见性、有序性

原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。
可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行的顺序按照代码的先后顺序执行。

Java语言对原子性、可见性、有序性的保证

1、原子性
在java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断,要么执行,要么不执行。
(1)、X=10;  //原子性
(2)、Y = x;  //非原子,去读取x的值,再将y值写入
(3)、X++;  //非原子,包括3个操作:读取x的值,x+1,将x写入
可以通过 synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

2、可见性
当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。

Synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中,

3、有序性

在Java里面,可以通过volatile关键字来保证一定的“有序性”,禁止指令重排。
另外可以通过synchronized和Lock来保证有序性,相当于是让线程顺序执行同步代码,自然就保证了有序性

20、Java:按值传递还是按引用传递   (详解

1、在Java里按值传递是传递的值的拷贝,也就是说传递后就互不相关了
形式:基本类型 + 引号String是按值传递,:String str = “Java语言”;

public class TempTest {
private void test1(int a){
    a = 5;
    System.out.println("test1方法中的a="+a);
}

public static void main(String[] args) {
    TempTest t = new TempTest();
    int a = 3;
    t.test1(a);//传递后,test1方法对变量值的改变不影响这里的a
    System.out.println(”main方法中的a=”+a);
    }
}

运行结果是:
test1方法中的a=5
main方法中的a=3

2、按引用传递是什么
指的是在方法调用时,传递的引用的地址,也就是变量所对应的内存空间的地址
说传递前和传递后都指向同一个引用(也就是同一个内存空间)。

 public class TempTest {

 private void test1(A a){
     a.age = 20;
     System.out.println("test1方法中的age="+a.age);
 }

 public static void main(String[] args) {
     TempTest t = new TempTest();
     A a = new A();
     a.age = 10;
     t.test1(a);
     System.out.println(”main方法中的age=”+a.age);
     }
 }

 class A{
     public int age = 0;
 }

运行结果如下:
test1方法中的age=20
main方法中的age=20


1、多线程
      1.11、继承Thread类创建多线程的步骤
               1 将类声明为thread类的子类   2 在子类中覆盖thread类的方法  3  创建thread类的子类对象  4  调用start方法开启线程
      1.12实现Runnable接口创建多线程的步骤
               1  创建实现Runnable接口的类并实现run方法       2    创建该类的实例对象    
               3  传递给thread类的构造方法创建thread对象      4调用start方法开启线程
      1.13、有T1、T2、T3三个线程,如何怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
             join方法的功能是使异步执行的线程变成同步执行。即调用线程实例的start方法后,该方法会立即返回,如果调用start方法后,需要使用一个由这个线程计算得到的值,就必须使用join方法。如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完。而使用join方法后,直到这个线程退出,程序才会往下执行。
     1.14. Java中怎样唤醒一个阻塞的线程? 
          如果是IO阻塞,创建线程时,加一个数量的阈值,超过该值后则不再创建。或者为每个线程设置标志变量标志该线程是否已经束,三就是直接加入线程组去管理。
如果线程因为调用 wait()、sleep()、或者join()方法而导致的阻塞,你可以中断线程,并且通过抛出InterruptedException来唤醒它

      1.15、死锁怎么导致的?如何定位死锁
            
当以下四个条件同时满足时,就会产生死锁
            (1) 互斥条件。任务所使用的资源中至少有一个是不能共享的。
            (2) 任务必须持有一个资源,同时等待获取另一个被别的任务占有的资源。
            (3) 资源不能被强占。
            (4) 必须有循环等待。
要解决死锁问题,必须打破上面四个条件的其中之一。在程序中,最容易打破的往往是第四个条件
     
2、网络编程
        2.1、网络体系结构 
        

应用层      (HTTP、FTP、DNS、SMTP等等)
运输层      (TCP、UDP)
网络层      (IP等)
数据链路层  (ARP等)
物理层
  • 1.应用层:如http协议,它实际上是定义了如何包装和解析数据,应用层是http协议的话,则会按照协议规定包装数据,如按照请求行、请求头、请求体包装,包装好数据后将数据传至运输层。
  • 2.运输层:运输层有TCP和UDP两种协议,分别对应可靠的运输和不可靠的运输,关于这一层都是和Socket打交道,Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。我们平常使用Socket进行连接建立的时候,一般都要指定端口号,所以这一层指定了把数据送到对应的端口号。
  • 3.网络层:这一层IP协议,以及一些路由选择协议等等,所以这一层的指定了数据要传输到哪个IP地址。中间涉及到一些最优线路,路由选择算法等等。
  • 4.数据链路层:印象比较深的就是ARP协议,负责把IP地址解析为MAC地址,即硬件地址,这样就找到了对应的唯一的机器。
  • 5.物理层:这一层就是最底层了,提供二进制流传输服务,也就是也就是真正开始通过传输介质(有线、无线)开始进行数据的传输了。

2.2 、http1.1和http1.0的区别

  • http1.1默认支持长连接,而http1.0需要使用keep-alive参数来告知服务器端要建立一个长连接。
  • 管线化,客户端可以同时发送多个HTTP请求,而不用一个个等待响应。
  • 支持传送内容的一部分,当客户端已经有一部分资源后只需要跟服务器端请求另一部分资源即可。支持文件断点续传
  • 1.1有host域这个参数而1.0没有。web server上的多个虚拟站点可以共享同一个ip和端口

http2.0和http1.1的主要区别

  • 多路复用:http2.0采用了多路复用技术,实现了一个连接并发处理多个请求
  • 数据压缩:HTTP1.1不支持header数据的压缩,数据体积小了网络上传输就会更快。
  • 服务器推送:当我们对支持http2.0的web server请求数据时,服务器会顺便把客户端需要的其他资源一起推送给客户端,避免再次创建连接发送请求


2.3、HTTPS为什么安全
        Https保证了我们数据传输的安全,Https=Http+Ssl  利用了非对称加密算法RSA,精华就是:公钥加密的信息只能用私钥解开,私钥加密的信息只能被公钥解开。由于客户端这个用公钥加密的数据只有私钥能解密,而这个私钥只有服务端有,所以数据传输就安全了。

2.4、Http和Https的区别?
          (1)Https是ssl加密传输,Http是明文传输
          (2)Https是使用端口443,而Http使用80
          (3)HttpsSSL+HTTP协议构建的可进行加密传输、身份认证的网络协议要比Http协议安全
          (4)Https协议需要到CA认证机构申请证书,是因为传输过程容易被监听者勾线监听、伪造服务器,而 HTTPS 协议主要解决的便是网络传输的安全性问题

  2.5、HTTPS 的实现原理  
HTTPS 在内容传输的加密上使用的是对称加密,非对称加密只作用在证书验证阶段。

① 证书验证阶段:

1)浏览器发起 HTTPS 请求;
2)服务端返回 HTTPS 证书;
3)客户端验证证书是否合法,如果不合法则提示告警。

② 数据传输阶段:

1)当证书验证合法后,在本地生成随机数;
2)通过公钥加密随机数,并把加密后的随机数传输到服务端;
3)服务端通过私钥对随机数进行解密;
4)服务端通过客户端传入的随机数构造对称加密算法,对返回结果内容进行加密后传输。

为什么数据传输是用对称加密?

非对称加密的加解密效率是非常低的,而http 的应用场景中通常端与端之间存在大量的交互,非对称加密的效率是无法接受的。另外:在 HTTPS 的场景中只有服务端保存了私钥,一对公私钥只能实现单向的加解密,所以 HTTPS 中内容传输加密采取的是对称加密,而不是非对称加密。


2.6、请求报文以及响应报文
           http 请求由三部分组成,分别是:请求行、请求头、请求体       
        请求行:以一个方法符号开头,以空格分开,格式如下: Method Request-URI HTTP-Version CRLF

名称说明
Method请求方法如 post/get
Request-URI资源标识符(请求路径)
HTTP-Version请求的HTTP协议版本
CRLF回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)

     请求头:

Header解释示例
Accept指定客户端能够接收的内容类型Accept: text/plain, text/html,application/json
Accept-Charset浏览器可以接受的字符编码集Accept-Charset: iso-8859-5
Accept-Encoding指定浏览器可以支持的web服务器返回内容压缩编码类型。Accept-Encoding: compress, gzip
Accept-Language浏览器可接受的语言Accept-Language: en,zh

例如--请求行--请求头---请求体

1 GET/sample.jspHTTP/1.1
2 Accept:image/gif.image/jpeg,*/*
3 Accept-Language:zh-cn
4 Connection:Keep-Alive
5 Host:localhost
6 User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0)
7 Accept-Encoding:gzip,deflate
8
9 username=jinqiao&password=1234

第一行为http请求行,包含方法,URI 和http版本
2-7为请求头,包含浏览器,主机,接受的编码方式和压缩方式
第8行表示一个空行 表示请求头结束 这个空行是必须的
第9行是数据体,比如是需要查询的信息。

响应报文:

名称组成
状态行状态码如 200、协议版本等
响应头即返回的 header
响应体响应的正文数据
2XX 成功
    200 OK,表示从客户端发来的请求在服务器端被正确处理
    204 No content,表示请求成功,但响应报文不含实体的主体部分
    206 Partial Content,进行范围请求
3XX 重定向
    301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
    302 found,临时性重定向,表示资源临时被分配了新的 URL
    303 see other,表示资源存在着另一个 URL,应使用 GET 方法丁香获取资源
    304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
    307 temporary redirect,临时重定向,和 302 含义相同
4XX 客户端错误
    400 bad request,请求报文存在语法错误
    401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
    403 forbidden,表示对请求资源的访问被服务器拒绝
    404 not found,表示在服务器上没有找到请求的资源
5XX 服务器错误
    500 internal sever error,表示服务器端在执行请求时发生了错误
    503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求


3、JVM相关知识,GC机制
基础数据类型:boolean、byte、short、char、int、long、float、double
引用数据类型:类、接口、数组,枚举,注解

      

1、从上图可知,JVM主要包括四个部分:类加载器,执行引擎,内存区,本地方法接口

1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。
2.执行引擎:负责执行class文件中包含的字节码指令;
4.本地方法接口:主要是调用C或C++实现的本地方法及返回结果。
3.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域

(1)方法区:用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。
(2)堆:Java 程序在运行时new所有类型对象和数组都存储在堆中,这块是GC的主要区域,方法区和堆是被所有java线程共享

(3)虚拟机栈:基本数据类型和堆内存访问地址,每启动一个线程,JVM都会为它分配一个Java栈,用于存放方法中的局部变量,操作数以及异常数据等。Java栈中的数据是线程私有的,也就是为什么多线程编程时,两个相同线程执行同一方法时,对方法内的局部变量是不需要数据同步的原因。

(4)本地方法栈:和java栈的作用差不多,只不过是为JVM使用到的native方法服务的
(5)程序计数器:保存当前线程执行的内存地址。由于JVM程序是多线程执行的,所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方

1、为什么栈要用来存储基本变量信息和对象引用?
java虚拟机的基本架构就是采用栈来进行设计的。当一个程序需要运行的时候,由于要预先内存空间和运行的生命周期,所以需要进行指针的变动,来进行内存大小的分配

2、为什么说栈的提取速度比堆要快?

1.栈里面的内存大小一般都是程序启动的时候由系统分配好的。
2.堆的内存大小需要在使用的时候才去申请,消耗性能,开销较大。
3.cpu里面会有专门的寄存器来操作栈,堆里面都是使用间接寻址的方式来进行对象查找的,所以栈会快一些。

public void test(){
  Person p = new Person();
  int b = 1;
}
public class Person{}

堆内存中new Person()对象
栈内存中开辟一个变量空间p地址,p是person对象的引用存放在栈中
b是基础数据类型所以在栈中.

2、 GC机制
       有3个是不需要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。
垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?一般有以下几种方法:

引用计数法:
给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:

可达性分析算法:

以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集GC Roots 的对象包括:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象
2、方法区中类静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中 JNI 引用的对象
总之,JVM在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器回收。一般回收算法也有如下几种:

     1).标记-清除      2).复制      3).标记-整理        4).分代收集算法

1、标记清除算法
      分为 “标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

它主要有两个不足的地方:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而得不到提前触发另一次垃圾收集动作。

2、复制算法
    为了解决效率问题,“复制”算法应运而生,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效不足之处是,将内存缩小为原来的一半,代价太高。

3、标记—整理算法
     复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。所以在老年代一般不能直接选用复制收集算法。

根据老年代的特点,“标记—整理” 算法应运而生。
标记过程仍然与 “标记—清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4、分代收集算法
一般是把 Java 堆分为新生代和老年代,

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记—清除” 或者 “标记—整理” 算法来进行回收。当前商业虚拟机的垃圾收集都采用 “分代收集” 算法。

 

Android知识点:
性能优化------快/稳/省/小

数据存储的方式:
1:文件存储:以IO流方式将数据存入手机内存或者SD卡,存储大数据,例如音乐,图片或者视频。
2:SharedPreference: 本质是Xml文件,以Map形式存入手机内存,用于存储简单的参数设置。
3:SQLite数据库:轻量级跨平台占用内存小所有信息存储在单一信息内。
4 :ContentProvider:以数据库形式存入手机内存,共享自己的数据给其他数据使用,统一了数据访问方式,更规范。
5:网络存储:将数据存储到服务器,使用时直接去服务器取,安全。

SharedPreference提交的commit与apply的区别

1、sp的最大存储值是16kb;
2、sp文件存储路径为/data/data/package_name/shared_prefs/路径下
3、sp中getX/putX方法都是同步的,sp是线程安全的;get操作,会锁定SharedPreferences对象,互斥其他操作
put操作会锁定Editor对象,写入磁盘更会锁定一个写入锁。SP操作并发时,耗时会徒增。
4、apply()是通过线程池执行的,每次执行时在单线程池中异步写入磁盘,而commit()方法时同步执行的,如果你的sp需要存储数据量比较多时,需要考虑使用apply()方法从而避免ANR的情况。
————————————————

commit方法:
1、commit方法有返回值,设置成功为ture,
2、同步操作,线程安全,性能慢,一般在当前线程完成文件操作

apply方法
1、apply没有返回值,存储是否成功无从知道。
2、线程不安全,性能高,异步处理I/O操作,一般在singleThreadExecutor中执行,没有返回值

二者最主要的不同是在执行enqueueDiskWrite方法时,apply方法传入了一个Runnable对象,而commit方法直接传入了null值
传入null则执行同步方法,非null则执行异步方法,这也印证了你在使用commit()方法时,对主线程产生阻塞的可能了。

//apply操作
public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    Runnable postWriteRunnable = new Runnable() {
                public void run() {
                    awaitCommit.run();
                    QueuedWork.remove(awaitCommit);
                }
            };
            
    //代码省略        
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    

commit操作
 public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
    
    //代码省略
    return mcr.writeToDiskResult;
 private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //先判断是否为异步传入                          
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //这里讲修改的对象写入到文件中
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (SharedPreferencesImpl.this) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

    // 如果是同步方法,那么直接writeToDiskRunnable#run方法
    if (isFromSyncCommit) {
         writeToDiskRunnable.run();
         return;
        }
    }
    
    //如果是异步方法,那么使用线程池执行writeToDiskRunnable
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}


强引用<---------软引用<----------弱引用<------虚引用

强引用特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。 当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为obj = null,这样一来的话,JVM在合适的时间就会回收该对象

软引用特点:软引用通过SoftReference类实现。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。

弱引用特点:通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
应用场景:如果一个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么应该用 Weak Reference 来记住此对象。或者想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候就应该用弱引用,这个引用不会在对象的垃圾回收判断中产生任何附加的影响

虚引用特点:虚引用也叫幻象引用,通过PhantomReference类来实现。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

SoftReference的源码

在SoftReference中有两个比较重要的变量:
clock:在JVM发生GC时,也会更新clock的值,所以clock会记录上次GC发生的时间点
timestamp:记录对象被访问时最近一次GC的时间

当GC开始时clock变量设置为当前时间 clock = now,
当垃圾回收器遍历完所有的GC Roots之后,在执行对象清理之前,会调用 ReferenceProcessor:process_discovered_references
主要作用判断当前系统中如果内存不够,则调用policy的函数移除软引用,软引用回收策略:

LRUCurrentHeapPolicy  使用 get_head_at_last_gc() 获取的是当前可用堆的大小
LRUMaxHeapPolicy       使用 get_heap_used_at_last_gc() 获取的是最大堆大小

是否需要清理 软引用 跟几个条件有关:
interval:当前时间与上次GC回收的执行时间
_max_interval: 最大回收间隔

GC对于存活时间大于 _max_interval 的软引用会进行回收。
公式:clock - timestamp <= heap_size * SoftRefLRUPolicPerMB
我们平时说软引用会在内存不足时被GC回收。不仅仅是指空间大小,还有时间的限制。
这就解释了为什么在文章开始的例子中睡眠2s之后,再执行GC 软引用 就被回收了!

android中的动画有哪几类,它们的特点和区别是什么  

在Android3.0之前有两种动画,一种方式是补间动画 Tween Animation、另一种叫逐帧动画 Frame Animation

Android3.0以后增加了属性动画 Property Animation。这样动画被分为两部分:1:Tween Animation、Frame Animation只能用于View,被归类为View Animation                     2:Property   Animation

属性动画与视图动画的区别:

1:属性动画是真实地改变控件的属性               补间动画是改变控件的绘制(通过Matrix)
2:属性动画出现在android 3.0,api11,         补间动画出现与API1
3:属性动画可以作用于View和非View             补间动画只能作用于View

动画的原理,底层如何给上层信号?
   动画分两种,补间动画和属性动画。补间动画是通过不断调用invalidate()通知父容器,父容器来处理子控件的动画。
属性动画是通过差值器和估值器计算中间过程,不断地设置属性形成的动画。  插值器和估值器解析

插值器:Interpolator 实现加速或者减速变化趋势的接口
Android内置了 9 种插值器
动画加速进行                                  
快速完成动画,超出再回到结束样式            
先加速再减速                                       
先退后再加速前进                                    
先退后再加速前进,超出终点后再回终点     
最后阶段弹球效果                                    
周期运动                                                    
减速                                                    
匀速     

布局文件xml中使用
 android:interpolator="@android:anim/overshoot_interpolator"
   
在 Java 代码中设置
  // 创建动画对象和插值器类对象
Animation alphaAnimation = new AlphaAnimation(1,0);
Interpolator overshootInterpolator = new OvershootInterpolator();
alphaAnimation.setInterpolator(overshootInterpolator);
        
 自定义插值器
根据动画的进度(0%-100%)计算出当前属性值改变的百分比
具体使用:自定义插值器需要补间动画实现 Interpolator / 属性动画TimeInterpolator接口 
                & 复写getInterpolation()

public interface Interpolator {  
    // input值变化范围0-1,且随着动画进度0%-100%均匀变化,开始时,input值 = 0;结束时input = 1
     float getInterpolation(float input) {  
         
       // 使用正弦函数来实现先减速后加速的功能,逻辑如下:
            if (input <= 0.5) {
                   float   result = (float) (Math.sin(Math.PI * input)) / 2;
              } else {
                 result = (float) (2 - Math.sin(Math.PI * input)) / 2;
              }
      return result;
    }  

 估值器:TypeEvaluator 实现加速或减速具体变化值的接口

ObjectAnimator anim = ObjectAnimator.ofObject(myView2, "height", new Evaluator(),1,3);
// 在第4个参数中传入对应估值器类的对象,系统内置的估值器有3个:
 IntEvaluator:FloatEvaluator:ArgbEvaluator: 
以整型,浮点型,Argb类型形式从初始值 - 结束值 进行过渡

自定义估值器:
当前属性值 = 初始值 + (结束值-初始值)* 当前属性进行值
具体使用:自定义估值器需要实现 TypeEvaluator接口 & 复写evaluate()

public interface TypeEvaluator {  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        // fraction:插值器中getInterpolation()的返回值,计算当前动画的值

          已经实现好的系统内置差值器:浮点型插值器:FloatEvaluator
         float startFloat = ((Number) startValue).floatValue();  
         float  value = startFloat + fraction * (((Number) endValue).floatValue() - startFloat); 
          return value;   返回对象动画过渡的逻辑计算后的值
    }  
}  

 activity的启动模式有哪些?是什么含义?
  [1]standard 标准模式 ,每当启动一个新的activity,他会进入任务栈并处于栈顶,先进后出原则。
  [2]singleTop 单一顶部模式  在创建这个activity的实例的时候 就会检查当前任务栈的栈顶,如果发现该实例已经创建就不会创建            新的实例,会直接复用 
  [3]singleTask 如果配置这种启动模式,当创建这个类的实例的时候会检查当前的任务栈是否有该实例存在如果有直接复用,并且还会把这个实例上面的actvity都清空.
 [4] singleInstance 如果需要activity在整个系统中只有一个实例,会为这个activity单独创建一个任务栈,采用这种模式分两种情况:
1要启动的activity不存在创建一个新的任务栈在创建该activity实例,加入栈顶。
2要启动的activity存在,无论位于哪个任务栈中都会将该任务栈转到前台显示activity

 应用场景:
  [1]大多数都是使用标准的启动模式
  [2] singleTop 浏览器的书签页面 
  [3] singleTask 浏览器的浏览页面
  [4 ]singleInstance 来电页面  

 

TaskAffinity:任务相关性
默认情况下所有的Activity所需要的任务栈的名字为应用的包名,我们可以为每个Activity都单独指定TaskAffinity
主要和singleTask启动模式或者allowTaskReparenting属性配合使用

当TaskAffinity和singleTask启动模式时具有该模式Activity目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中

当TaskAffinity和allowTaskReparentiing结合的时候,会产生特殊的效果,当一个应用A启动了应用B的某一个Activity后,如果这个Activity会直接从应用A的任务栈转移到应用B的任务栈中,这还是很抽象的,再具体点,比如现在有2个应用A和B,A启动了B的一个Activity C ,然后按Home键回到桌面,然后再单击B的桌面图标,这个时候并不是启动; B的主Activity,而是重新显示了已经被应用A启动的Activity C,或者说,C从A的任务栈转移到了B的任务栈中,可以这么理解,由于A启动了C,这个时候C只能运行在A的任务栈中,但是C属于B应用,正常情况下,他的TaskAffinity值肯定不可能和A的任务栈相同(因为包名不同),所以,当B启动后,B会创建自己的任务栈,这个时候系统发现C原本所想要的任务栈已经被创建出来了,所以就把C从A的任务栈中转移过来。

activity在屏幕旋转时的生命周期
1、不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次
2、设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次
3、设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法

当Activity B 覆盖A导致A完全不可见时:

//【1】部署程序
D/MainActivity: onCreate------A
D/MainActivity: onStart-------A
D/MainActivity: onResume------A

//【2】点击A中的按钮开始跳转到B
D/MainActivity: onPause-------A
D/SecondActivity: onCreate----B
D/SecondActivity: onStart-----B
D/SecondActivity: onResume----B
D/MainActivity: onStop--------A

//【3】然后点击返回键从B返回A
D/SecondActivity: onPause-----B
D/MainActivity: onRestart-----A
                onStart-------A
D/MainActivity: onResume------A
D/SecondActivity: onStop------B
D/SecondActivity: onDestroy---B

当Activity B背景被设置为透明(相当于发生跳转后,A部分可见)

//【1】部署程序
D/MainActivity: onCreate------A
D/MainActivity: onStart-------A
D/MainActivity: onResume------A

//【2】点击A中的按钮开始跳转到B
D/MainActivity: onPause-------A
D/SecondActivity: onCreate----B
D/SecondActivity: onStart-----B
D/SecondActivity: onResume----B

//【3】然后点击返回键从B返回A
D/SecondActivity: onPause-----B
D/MainActivity: onResume------A
D/SecondActivity: onStop------B
D/SecondActivity: onDestroy---B

Activity  A启动一个完全透明的Dialog 回调哪些方法
A界面==onCreate()---> A界面==onStart()------> A界面==onResume()

ABC三个页面activity,A是standard模式,B是SingleTask模式,C是SingleTop模式,依次点击按钮跳转,生命周期为:

A---onCreate
A---onStart
A---onResume
A---onPause
B---onCreate
B---onStart
B---onResume
A---onStop
B---onPause
C---onCreate
C---onStart
C---onResume
B---onStop

因为只是启动了一次,所以设置的启动模式并不会影响正常的生命周期。
 

Activity依次A→B→C→B,其中B启动模式为singleTask,AC都为standard
(1)生命周期分别怎么调用?
(2)如果B启动模式为singleInstance又会怎么调用?
(3)B启动模式为singleInstance不变,A→B→C的时候点击两次返回,生命周期如何调用。

1)A→B→C→B,B启动模式为singleTask

启动A的过程,生命周期调用是 (A)onCreate→(A)onStart→(A)onResume
再启动B的过程,生命周期调用是 (A)onPause→(B)onCreate→(B)onStart→(B)onResume→(A)onStop
B→C的过程同上
C→B的过程,由于B启动模式为singleTask,所以B会调用onNewIntent,并且将B之上的实例移除,也就是C会被移出栈。所以生命周期调用是 (C)onPause→(B)onNewIntent→(B)onRestart→(B)onStart→(B)onResume→(C)onStop→(C)onDestory

2)A→B→C→B,B启动模式为singleInstance

如果B为singleInstance,那么C→B的过程,C就不会被移除,因为B和C不在一个任务栈里面。所以生命周期调用是 (C)onPause→(B)onNewIntent→(B)onRestart→(B)onStart→(B)onResume→(C)onStop

3)A→B→C,B启动模式为singleInstance,点击两次返回键

如果B为singleInstance,A→B→C的过程,生命周期还是同前面一样正常调用。但是点击返回的时候,由于AC同任务栈,所以C点击返回,会回到A,再点击返回才回到B。所以生命周期是:(C)onPause→(A)onRestart→(A)onStart→(A)onResume→(C)onStop→(C)onDestory。

再次点击返回,就会回到B,所以生命周期是:(A)onPause→(B)onRestart→(B)onStart→(B)onResume→(A)onStop→(A)onDestory。

onActivityResult 在哪两个生命周期之间回调?

onActivityResult 方法的注释中就写着答案:
「You will receive this call immediately before onResume() when your activity is re-starting.」 

会发现 onActivityResult 回调先于该 Activity 的所有生命周期回调,从 B Activity 返回 A Activity 的生命周期调用为:
B.onPause -> A.onActivityResult -> A.onRestart -> A.onStart -> A.onResume

 onCreate 方法里写死循环会 ANR 吗?

 ANR 的四种场景:

Service TimeOut:  未在规定时间执行完成:前台服务 20s,后台 200s
BroadCastQueue TimeOut: 未在规定时间内未处理完广播:前台广播 10s 内, 后台 60s 内
ContentProvider TimeOut:  publish 在 10s 内没有完成
Input Dispatching timeout:  5s 内未响应键盘输入、触摸屏幕等事件

我们可以看到,Activity 的生命周期回调的阻塞并不在触发 ANR 的场景里面,所以并不会直接触发 ANR只不过死循环阻塞了主线程,如果系统再有上述的四种事件发生,就无法在相应的时间内处理从而触发 ANR。

Activity的onNewIntent()方法何时会被调用?

1、当ActivityA为SingleTop时,如果ActivityA在栈顶,且现在要再启动ActivityA,这时会调用onNewIntent()方法 
      调用顺序依次为A.onPause() -> A.onNewIntent() -> A.onResume()

2、当ActivityA为SingleTask时,如果已经ActivityA已经在堆栈中,那么此时会调用onNewIntent()方法
      调用顺序依次是A.onStop() -> A.onNewIntent() -> A.onRestart() -> A.onStart() -> A.onResume()

onNewIntent被回调,Intent内容是最新的么?那其他回调里的Intent是最新的么?
     
在onNewIntent方法中的Intent是最新,但是其他回调里不是。如果想让其他回调也是最新的intent,需要在onNewIntent里,setIntent(),对最新的intent进行赋值。


注册广播有几种方式,这些方式有何优缺点?请谈谈Android引入广播机制的用意。
  第一种:在清单文件中声明,添加
<receive android:name=".IncomingSMSReceiver " >
<intent-filter>
   <action android:name="android.provider.Telephony.SMS_RECEIVED")
<intent-filter>
<receiver>
第二种使用代码进行注册如:
IntentFilter filter =  new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
IncomingSMSReceiver receiver = new IncomgSMSReceiver();
registerReceiver(receiver.filter);
两种注册类型的区别是:
1)第一种是常驻型广播,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行
2)第二种是非常驻型,也就是说广播跟随程序的生命周期。

Service的两种启动方式和对应的生命周期
startService:  onCreate()---->onStartCommand()------->onDestory()
1.onCreate():该方法在整个生命周期中只会在创建Service时调用一次!
2.onDestory():该方法只会当Service被关闭时会回调一次!
3.onStartCommand(intent,flag,startId):当客户端调用startService(Intent)方法时会回调,可多次调用StartService方法, 但不会再创建新的Service对象,而是继续复用前面产生的Service对象,但会继续回调 onStartCommand()方法!

流程
1、第一次调用startService()方法后,首先调用onCreate()和onStartCommand()方法,此时Service进入运行状态;如果这个时候再次调用startService()方法,直接复用已经存在的Service,可以理解为Service是单实例模式,只能在系统中存在一个实例。
由于启动方式打开的Service和调用者没必然联系,所以调用者destroy之后,Service还是会继续运行的。
Service并不是运行在一个独立的进程中的,而是依赖于创建服务时所在的应用程序进程,当某个应用程序进程被杀死的时候,依赖于该应用程序进程的Service也会被杀掉。
无论启动了多少次Service,只需调用一次StopService即可停掉Service

BindService:  onCreate------>onBind()------>onUnbind------->onDestory
流程:
1、首先系统会在调用bindService之后实例化一个Service并绑定,然后紧接着调用onCreate和onBind()方法。
2、如果再次使用bindService绑定Service,系统只会直接把IBinder对象传递给后来增加的客户端!
3、调用unbindService(),此时若只有一个客户端绑定了此服务,那么onUnbind和onDestory方法将会被调用
若是多个客户端绑定同一个Service的话, 当所有的客户端都和service解除绑定后,系统会销毁service。绑定模式下的Service和调用者共存亡
 

进程保活实现的方式
黑色保活
:不同的app进程,用广播相互唤醒(包括利用系统提供的广播进行唤醒)
                 1、开机、网络切换、拍照视频,利用系统产生的广播唤醒app
                 2、接入第三方sdk也会唤醒相应的app进程
                 3、支付宝,淘宝,UC等阿里系app相互调用

白色保活:启动前台Service
调用系统api启动一个前台的Service进程,在系统的通知栏生成一个Notification,优先级最高,不会杀死,或者加入白名单中保证进程不死。

灰色保活:利用系统的漏洞启动前台Service          具体代码案例
利用系统的漏洞来启动一个前台的Service进程,与普通的启动方式区别在于,它不会在系统通知栏处出现一个Notification,看起来就如同运行着一个后台Service进程一样。
实现思路:
思路一:API < 18,启动前台Service时直接传入new Notification();
思路二:API >= 18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理

FragmentPagerAdapter与FragmentStatePagerAdapter的区别:
FragmentPagerAdapter:对于不再需要的fragment,选择调用detach方法,仅销毁视图,并不会销毁fragment实例。
FragmentStatePagerAdapter:会销毁不再需要的fragment,销毁时,会将其onSaveInstanceState(Bundle outState)中的bundle信息保存下来,当用户切换回来,可以通过该bundle恢复生成新的fragment。
如上所说,使用FragmentStatePagerAdapter当然更省内存,一般情况下,如果你是制作主页面3、4个Tab,那么可以选择使用FragmentPagerAdapter,如果你是用于ViewPager展示数量特别多的条目时,那么建议使用FragmentStatePagerAdapter。

Fragment实现懒加载
只有可见时,我们isVisibleToUser为true,所以我们可以重写setUserVisibleHint方法,然后在可见时进行网络加载数据:
public void setUserVisibleHint(boolean isVisibleToUser) {
     if (isVisibleToUser) {
        pullData();
      }
    super.setUserVisibleHint(isVisibleToUser);
}

setUserVisibleHint(boolean isVisibleToUser)方法是比onCreate更早调用的,假如拉取数据是秒回,但是我们还没有进行UI绑定,或者是Adapter初始化等,那么我们就无法更新UI了,所以Fragment给我们提供了另一个方法getUserVisibleHint(),它就是用来判断当前Fragment是否可见:
public void onStart() {
    super.onStart();
    if(getUserVisibleHint()) {
        pullData();
    }}

如果你的网络请求并不需要涉及UI更新,那么就可以直接在setUserVisibleHint()里操作,最终还是根据各自的实际用途来使用
 
Q :①在 Listview的优化中 ,我们为何使用ConvertView?②为何使用ViewHolder?③你认为哪个更 能解决问题?④你认为view.inflate和view.findviewById哪个更耗时,为什么?⑤如果这两个AP让 你重新写,你怎么写? 

①使用ConvertView可以实现对view的复用,这样大大节约了每次创建对象的时间,提升了 ListView的显示效率。
②使用ViewHolder作为内部类 ,可以将view的子控件封装在ViewHolder类 中,然后通过View.setTag(ViewHolder)将view和ViewHolder进行绑定 ,这样我们就不用每次都调 用view的findViewById(id)方法来查找控件。
③使用ConvertView解决了一大部分问题,使用 ViewHolder实现了控件换时间的问题,因为给View对象设置一个Tag本身就是占用内存的,因此 ViewHolder的使用还是需要区分不同的应用场景的 ,没有绝对的好与不好。如果内存足够需要高则 ViewHolder建议使用,否则不建议使用。
④当然是view.inflate耗时,这个函数完成的功能把xml 布局文件通过pullParser的形式给解析到内存中需要io,需要递归子节点。⑤我其实还不太相信我 写出来的代码比Google官方写的好,如果让我写的话我可能会这样考虑,当用户在使用view.inflate 的时候将多个id作为数组添加到形参中,这样在初始化view的使用我就可以给这个view直接调用 setTag方法绑定需要的子控件。不过这个原生方法其实也应该保留共不同的需求使用。 

Android中的对话框有两种:PopupWindow和AlertDialog区别

(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));去掉。

其中最本质的差别就是:AlertDialog是非阻塞式对话框:AlertDialog弹出时,后台还可以做事情;而PopupWindow是阻塞式对话框:PopupWindow弹出时,程序会等待,在PopupWindow退出前,程序一直等待,只有当我们调用了dismiss方法的后,PopupWindow退出,程序才会向下执行。这两种区别的表现是:AlertDialog弹出时,背景是黑色的,但是当我们点击背景,AlertDialog会消失,证明程序不仅响应AlertDialog的操作,还响应其他操作,其他程序没有被阻塞,这说明了AlertDialog是非阻塞式对话框;PopupWindow弹出时,背景没有什么变化,但是当我们点击背景的时候,程序没有响应,只允许我们操作PopupWindow,其他操作被阻塞。

volatile 与synchronized区别:
 1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

Volley能否下载电影以及加载100M的图片?

不可以,,Volley适合数据量小,频率快的请求

#Volley的网络请求线程池默认大小为4。意味着可以并发进行4个请求,大于4个,会排在队列中。

Request#getBody() 方法返回byte[]类型,作为 Http.POST 和 Http.PUT body 中的数据。这就意味着需要把用 http 传输的数据一股脑读取到内存中。如果文件过大,内存不足容易oom。

考虑这样一个场景: 你同时上传4个文件,这四个文件都很大,这时候你的内存占用就很高,很容易oom。 这时候,你发网络请求,调用普通api。 所有的网络线程都被上传文件的任务占满了,你的网络请求只有在文件上传完毕后才能得到执行。

Rxjava异步(线程调度)  源文路径

异步是相对于主线程来讲的子线程操作,在这里我们不妨使用线程调度这个概念更加贴切。
首先介绍一下RxJava的线程环境有哪些选项:

            //new Observable.just()执行在新线程
  Observable.just(getFilePath())
           //指定在新线程中创建被观察者
          .subscribeOn(Schedulers.newThread())
          //将接下来执行的线程环境指定为io线程
          .observeOn(Schedulers.io())
            //map就处在io线程
          .map(mMapOperater)
            //将后面执行的线程环境切换为主线程,
            //但是这一句依然执行在io线程
          .observeOn(AndroidSchedulers.mainThread())
          //指定线程无效,但这句代码本身执行在主线程
          .subscribeOn(Schedulers.io())
          //执行在主线程
          .subscribe(mSubscriber);

实际上线程调度只有subscribeOn()和observeOn()两个方法。对于初学者,只需要掌握两点:

  • subscribeOn()它指示Observable在一个指定的调度器上创建(只作用于被观察者创建阶段)。只能指定一次,如果指定多次则以第一次为准

  • observeOn()指定在事件传递(加工变换)和最终被处理(观察者)的发生在哪一个调度器。可指定多次,每次指定完都在下一步生效。

在Android中切换线程
首先找到AndroidSchedulers,发现一个Scheduler的具体实现类:LooperScheduler

    private AndroidSchedulers() {
    ...
      mainThreadScheduler = new LooperScheduler(Looper.getMainLooper());
      ...
    }
    public static Scheduler mainThread() {
        return getInstance().mainThreadScheduler;
    }

LooperScheduler的代码很清晰,内部持有一个Handler,用于线程的切换。在Worker的schedule(Action0 action,...)方法中,将action通过Handler切换到所绑定的线程中执行

class LooperScheduler extends Scheduler {
    private final Handler handler;
    LooperScheduler(Looper looper) {
        handler = new Handler(looper);
    }
    LooperScheduler(Handler handler) {
        this.handler = handler;
    }
    public Worker createWorker() {
        return new HandlerWorker(handler);
    }

    static class HandlerWorker extends Worker {
        private final Handler handler;

        public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) {
        ...
     
            handler.sendMessageDelayed(message, unit.toMillis(delayTime));
        ...
            return scheduledAction;
        }
    }


ListView和RecyclerView的区别:    详细介绍
1、listview两级缓存:mActiveView + mScrapViews
recyclerView四级缓存:mAttachedScrap + mCacheViews + mViewCacheExtension +mRecyclerPool
RecyclerView的优势在于
(1)、mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;
(2)、mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势
2、ListView中可以通过addHeaderView() 与 addFooterView()来添加头部item与底部item,recyclerView通过ViewHolder的Type与View来实现自己的Header,Footter与普通的item。
3、ListView中刷新数据是用notifyDataSetChanged()全局刷新消耗资源,RecyclerView中可以实现局部刷新notifyItemChanged()
4、ListView就没有实现嵌套滚动机制,RecyclerView中,实现的是NestedScrollingChild,所以能实现嵌套滚动机制

MVP如何管理Presenter的生命周期,何时取消网络请求
定义Presenter基类
       interface BasePresenter {
                  void onStart();
                  void onDestroy();
           }
让所有的Presenter都继承BasePresenter,然后在activity中相应的生命周期里面调用在相应的方法里面,初始化,结束异步操作,释放资源,将view=null;
而在activity里面,由于Presenter并没有view的引用了,所以p随着activity的销毁也就跟着销毁了.不会造成上下文的泄漏等.

Presenter业务量过大时如何解决?     MVC-MVP-MVVM例子

有一个方法是在UI层和Presenter之间设置中介者Mediator,将例如数据校验、组装在内的轻量级逻辑操作放在Mediator中;在Presenter和Model之间使用代理Proxy;通过上述两者分担一部分Presenter的逻辑操作,但整体框架的控制权还是在Presenter手中。Mediator和Proxy不是必须的,只在Presenter负担过大时才建议使用

适配刘海屏的两种情况
  1、沉浸式状态栏:适配方案就是将窗口布局下移,预留出状态栏的空间。
       (1):利用fitsSystemWindows属性
           当我们给最外层View设置了android:fitsSystemWindows="true"属性后,就会自动给View添加paddingTop或paddingBottom属性,这样就在屏幕上预留出了状态栏的高度,布局就不会占用状态栏来显示了。

      (2):根据状态栏高度手动设置paddingTop
            本质上和设置fitsSystemWindows是一样的,首先获取状态栏高度,然后设置根布局的paddingTop等于状态栏高度就可以

     (3):在布局中添加一个和状态栏高度相同的View
             在根布局中添加了一个透明的View,高度和状态栏高度相同。这种方法的好处是可以自定义填充状态栏View的背景

2、全屏显示模式:不做适配的话状态栏会呈现一条黑边。适配方案是首先判断系统版本
      Android P以上是API Level在28及以上才可以调用。过DisplayCutout类可以获得安全区域的范围以及刘海区域的信息,
      Android P以下总结了华为、小米、Vivo和Oppo的适配方案,先判断是否有刘海屏,然后处理刘海屏区域。


Picasso和Glide和Fresco性能分析:

Picasso :图片渲染模式是ARGB_8888, 默认加载图片不会压缩,但是可以指定图片加载的宽高,便会依据图片的宽高进行缩放。
Glide:图片渲染模式为RGB_565,内部会自动根据图片的宽高来压缩图片,内存占用会减少一半,专门针对滑动中的图片加载有优化。
 Fresco : 在android5.0以及以下,将图片的放入C++内存中,大大减小了程序出现OOM的几率;支持WebP和Gif动态图片的显示;

缓存
Fresco 三级缓存,分别是 Bitmap缓存,未解码图片缓存, 文件缓存。
在5.0以下系统,Bitmap缓存位于ashmem,这样Bitmap对象的创建和释放将不会引发GC,5.0及其以上系统,相比之下,内存管理有了很大改进,所以Bitmap缓存直接位于Java的heap上。
另外,磁盘缓存还可以通过代码来设置不同手机的缓存容量

public void initFresco(Context context, String diskCacheUniqueName){
    DiskCacheConfig diskCacheConfig = DiskCacheConfig.newBuilder(context)
            .setMaxCacheSize(DISK_CACHE_SIZE_HIGH)
            .setMaxCacheSizeOnLowDiskSpace(DISK_CACHE_SIZE_LOW)
            .setMaxCacheSizeOnVeryLowDiskSpace(DISK_CACHE_SIZE_VERY_LOW)
            .build();
}

Glide缓存    (Glide详细介绍
glide最大的优势就是对bitmap的管理是跟随生命周期去发生改变的,当Activity销毁之前加载的所有图片的内存都释放
Glide虽然只有内存和磁盘缓存,在性能上比不上Fresco;但他也有另外的优点, Fresco缓存的时候,只会缓存原始图像,而Glide则会根据ImageView控件尺寸获得对应的大小的bitmap来展示,从而缓存也可以针对不同的对象:原始图像(source)结果图像(result); 可以通过.diskCacheStrategy()方法设置

public enum DiskCacheStrategy {    
  ALL(true, true),    
  NONE(false, false),    
  SOURCE(true, false),    
  RESULT(false, true);
}

性能分析

1、内存占用

Glide的图片质量不如Fresco, 原因是Glide为了省内存, 采用了RGB_565的格式显示图片, 相对于"ARGB8888"要省了将近一半的内存。但也可以通过代码将图片格式改为ARGB_8888。

2、加载图片速度
Fresco设计中有个有一个image pipeline模块, 可以最大限度节省网络下载图片, 在不考虑缓存的情况下, Fresco也比Glide快很多, Fresco需要3s左右, Glide需要5s左右

3、内存使用情况
因为Fresco在没有做过任何处理的情况下,会将原图片加入到内存中,然后做一些压缩处理显示在控件上; 而Glide则是直接准确获取了控件大小,然后得到一张与控件大小相当尺寸的图片,加载到内存并显示。
Fresco也提供了方法将图片尺寸压缩,得到控件的宽高进行压缩,虽然可以解决内存溢出,但是这样做的前提是必须提前知道控件的宽高,而Glide则无需考虑这个问题。


为何需要进行IPC?多进程通信可能会出现什么问题?
       因为所有运行在不同进程的四大组件,只要它们之间需要通过内存在共享数据,都会共享失败。这是由于Android为每个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这会导致在不同的虚拟机中访问同一个类的对象会产生多份副本。
问题:
 (1)静态变量和单例模式失效:由独立的虚拟机造成
 (2)线程同步机制失效:由独立的虚拟机造成
 (3)SharedPreference的不可靠下降:不支持两个进程同时进行读写操作,即不支持并发读写,有一定几率导致数据丢失
(4)Application多次创建: Android系统会为新的进程分配独立虚拟机,相当于系统又把这个应用重新启动了一次。

Serializable与Parcelable两个序列化区别?
序列化原因:
1.永久性保存对象,保存对象的字节序列到本地文件中;
2.通过序列化对象在网络中传递对象;
3.通过序列化在进程间传递对象。 

但是Serializable与Parcelable两个都是序列化,什么时候用哪个呢?

1.在使用内存的时候,Parcelable 类比Serializable性能高,所以推荐使用Parcelable类。
2.Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。
3.Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。尽管Serializable效率低点, 也不提倡用,但在这种情况下,还是建议你用Serializable 。

AIDL如何工作?能处理哪些类型的数据? 
      AIDL一般用于远程服务,也就是进程间通信。我们可以分服务端和客户端,服务端声明AIDL文件,该文件命名为xxx.aidl,ADT会自动将xxx.aidl生成代码文件,代码文件提供了aidl中接口的实现。客户端如果要使用服务端提供的服务需要将xxx.aidl文件放到客户端源代码目录下,然后生成xxx.java类 , 

    客户端通过bindService的形参ServiceConnection的onServiceConnected获取到Service对象,这个对象通过Stub.asInterface (service)返回aidl的实现类。之后我们就可用调用这个aidl的实现类。 

③ 基本数据类型都可以,复杂对象也可以,只不过需要实现 Parcelable接口。

Android Binder机制是做什么的,为什么选用Binder,原理了解吗?
   (1)继承了IBinder接口
   (2) Binder是一种跨进程通信方式
   (3)是ServiceManager连接各种Manager(ActivityManager,WindowManager等)和相应ManagerService的桥梁
   (4)从Android应用层来说,Binder是客户端和服务端进行通信的媒介。
为什么选用Binder,在讨论这个问题之前,我们知道Android也是基于Linux内核,Linux现有的进程通信手段有以下几种:

  1. 管道:在创建时分配一个page大小的内存,缓存区大小比较有限;
  2. 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
  3. 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
  4. 套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;
  5. 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 信号: 不适用于信息交换,更适用于进程中断控制,如非法内存访问,杀死某个进程等;

既然有现有的IPC方式,为什么重新设计一套Binder机制呢。主要是出于以上三个方面的考量:

  • 高性能:从数据拷贝次数来看Binder只需要进行一次内存拷贝,而管道、消息队列、Socket都需要两次,共享内存不需要拷贝,Binder的性能仅次于共享内存。
  • 稳定性:上面说到共享内存的性能优于Binder,那为什么不适用共享内存呢,因为共享内存需要处理并发同步问题,控制负责,容易出现死锁和资源竞争,稳定性较差。而Binder基于C/S架构,客户端与服务端彼此独立,稳定性较好。
  • 安全性:我们知道Android为每个应用分配了UID,用来作为鉴别进程的重要标志,Android内部也依赖这个UID进行权限管理,包括6.0以前的固定权限和6.0以后的动态权限,传荣IPC只能由用户在数据包里填入UID/PID,这个标记完全 是在用户空间控制的,没有放在内核空间,因此有被恶意篡改的可能,因此Binder的安全性更高。

Android的IPC方式

1、使用Bundle
   Activity,Service,Receiver都是支持在Intent中传递Bundle数据,Bundle实现了Parcelable接口,方便的在不同的进程间传输。

2、使用文件共享
     两个进程通过读写同一个文件交换数据,通过共享的文件也是有局限性的,比如并发读写的问题,如果并发读,那么我们读出的内容具有可能不是最新的,因此文件共享方式是个对数据同步要求不高的进程之间通信。
     本质上来说SharedPreferences属于文件的一种,由于系统对他的读写有一定的缓存策略,即在内存中会有一份文件缓存,因此在多进程模式下,系统对他的读写变得不可靠,当面对高并发访问,SharedPreferences很大几率丢失数据,因此不建议在进程中通信使用SharedPreferences.

3、使用Messenger
    Messenger对AIDL做了封装,底层实现就是AIDL,方便进程间通信,由于一次处理一个请求,因此在服务端不用考虑线程同步问题,不存在并发执行的情形。
    接下里看下使用步骤
    服务端:
     1、建一个handler对象,并实现hanlemessage方法,用于接收来自客户端的消息,并作处理
     2、messenger(送信人),封装handler
     3、messenger的getBinder()方法获取一个IBinder对象,通过onBind返回给客户端
    客户端:
    1.在activity中绑定服务
    2.创建ServiceConnection并在其中使用 IBinder 将 Messenger实例化
    3.使用Messenger向服务端发送消息
    4.解绑服务
    5.服务端中在 hadleMessage() 方法中接收每个 Message

上面实现的仅仅是单向通信,即客户端给服务端发送消息,如果我需要服务端给客户端发送消息又该怎样做呢?
下面就让我们接着上面的步骤来实现双向通信吧
1.在客户端中创建一个Handler对象,用于处理服务端发过来的消息
2.创建一个客户端自己的messenger对象,并封装handler。
3.将客户端的Messenger对象赋给待发送的Message对象的replyTo字段
4.在服务端的Handler处理Message时将客户端的Messenger解析出来,并使用客户端的Messenger对象给客户端发送消息

4、使用AIDL
     使用AIDL进行进程间通信分为服务端和客户端两方面:
    (1)、服务端
     服务端首先创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个接口。
    (2)、客户端
    首先绑定服务端的Service,绑定成功后将服务端返回的Binder对象转成AIDL接口所属的类型,可以调用AIDL中的方法了。
   (3)、AIDL接口的创建
    新建一个后缀为AIDL的文件,声明一个接口两个方法
       interface IBookManager {
           List<Book> getBookList();
             void addBook(in Book book);
            }
   AIDL文件中并不是所有的数据类型都支持的,那么支持数据的类型有:
         基本数据类型(int,long,char,boolean等)
         String和CharSequence
         List:只支持ArrayList
         Map:只支持HashMap
         Parcelable:所有实现Parcelable接口的对象
         AIDL:所有AIDL接口本身也可以在AIDL文件中使用

     (4)远程服务端Service的实现
     (5)客户端的实现
           首先绑定远程服务,绑定成功后将服务端返回的Binder对象转换成AIDL接口,然后可以通过这个接口去调用服务端的远程方法了。
5、使用ContentProvider
      继承ContentProvider,实现6个抽象方法:onCreate,query,update,insert,delete,getType
     (1)ContentProvider主要以表格的形式来组织数据,并且可以包含多个表;
     (2)ContentProvider还支持文件数据,比如图片、视频等,系统提供的MediaStore就是文件类型的ContentProvider;
     (3)ContentProvider对底层的数据存储方式没有任何要求,可以是SQLite、文件,甚至是内存中的一个对象都行;
     (4)要观察ContentProvider中的数据变化情况,可以通过ContentResolver的registerContentObserver方法来注册观察者;

6、使用Socket套接字,分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中TCP和UDP协议。
          TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立需要经过"三次握手"才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传功能,因此具有很高的稳定性
         UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能,在性能上,UDP具有更好的效率,其缺点是不保证数据能够正确传输,尤其是在网络拥塞的情况下。

IPC方式对比
Bundle:          优点:简单易用                 缺点:只传输Bundle支持数据类型                        适用场景:四大组件间进程通信
文件共享:       优点:简单易用                  缺点:不适合高并发,无法做到进程间即时通信     适用场景:无并发,实时性不高的场景
AIDL:             优点:一对多并发实时通信      缺点:使用复杂,处理好线程同步                     适用场景:一对多通信且有RPC需求
Messenger:   优点:一对多并发实时通信      缺点:不适合高并发,只传输Bundle支持数据类型    适用场景:低并发一对多通信
ContentProvider:    优点:一对多并发数据共享         缺点:提供数据源CRUD操作            适用场景:一对多进程间数据共享
Socket:          优点:网络传输字节流,一对多并发实时通信         缺点:细节繁琐                  适用场景:网络数据交换

View事件的滑动三种方式
      第一种通过View本身提供的scrollTo/ScrollBy方法
      第二种通过动画给View施加平移效果实现滑动
      第三种通过改变View的LayoutParams使得view重新布局实现滑动

1、使用scrollTo/scrollBy scrollTo和scrollBy方法只能改变view内容的位置而不能改变view在布局中的位置。 scrollBy是基于当前位置的相对滑动,而scrollTo是基于所传参数的绝对滑动。通过View的getScrollX和getScrollY方法可以得到滑动的距离。

2、使用动画 使用动画来移动view主要是操作view的translationX和translationY属性

3、改变布局参数 通过改变LayoutParams的方式去实现View的滑动是一种灵活的方法。

4、各种滑动方式的对比
              scrollTo/scrollBy:操作简单,适合对View内容的滑动
              动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果
              改变布局参数:操作稍微复杂,适用于有交互的View
 

Activity、 Window、 View 三者的差别?
     Activity是Android应用程序的载体,允许用户在其上创建一个用户界面,并提供用户处理事件的API,如onKeyEvent, onTouchEvent等。 并维护应用程序的生命周期。
当我们调用Acitivity的 setContentView方法的时候实际上是调用的Window对象的setContentView方法,所以我们可以看出Activity中关于界面的绘制实际上全是交给Window对象来做的。
Activity--->Window--->DecorView
Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图) 。
 

在Activity中调用attach,创建了一个Window,是其子类PhoneWindow,在attach中创建PhoneWindow
在Activity中调用setContentView(R.layout.xxx),实际上是调用的getWindow().setContentView()

View的渲染机制
    Android渲染机制

View的生命周期

View 的生命周期为
 [改变可见性] --> 构造View --> onFinishInflate --> onAttachedToWindow --> onMeasure -->  onSizeChanged --> onLayout --> onDraw -->onDetackedFromWindow总的可以归结三点:
(1)  在Activity onCreate方法中初始化了View 的时候, 调用了View 的onFinishInflate
(2)  执行完 Activity的 onResume 方法之后,开始了View的绘制工作:onMeasure -->  onSizeChanged --> onLayout --> onDraw
(3) onMeasure,onSizeChanged,onLayout,onDraw可能由于setVisible或onresume调用多次,而onAttachedToWindow与onDetachedFromWindow在创建与销毁view的过程中只会调用一次
viewroot什么时候创建的
     那么在什么时候viewRoot会被创建呢,答案是执行onResume后,viewRoot就能创建好,这个时候就会执行UI线程检查了,所以在onCreate->onStart->onResume中都可以在子线程中更新UI。
View、surfaceView、GLSurfaceView区别
View
显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等,必须在UI主线程内更新画面,速度较慢
SurfaceView
基于view视图进行拓展的视图类,更适合2D游戏的开发,是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快
GLSurfaceView
基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图,是surfaceView的子类,openGL专用

onTouch()、onTouchEvent()和onClick()关系是怎样的,哪一个先执行?如果设置了onClickListener, 但是onClick()没有调用,可能产生的原因?

onTouch->onTouchEvent->onClick
1、当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener的onTouch方法会被回调。
2、看onTouch的返回值为false,则当前View的onTouchEvent方法会被调用;如果返回true,onTouchEvent方法将不会被调用。
3、如果设置了onClickListener,那onClick方法会被调用。可以看出OnClickListener优先级别最低。

如果设置了onClickListener, 但是onClick()没有调用,可能产生的原因?
1、父View拦截了事件,没有传递到当前View
2、View的Enabled = false,或Clickable = false   不可用状态,会直接返回。
3、View设置了onTouchListener,或TouchDelegate ,消耗了事件。会提前返回。
 

关于ACTION_CANCEL何时触发以及带来的滑动问题解决        详细介绍
我们自定义一个ViewGroup,覆盖它的onInterceptTouchEvent方法。在onInterceptTouchEvent方法中我们只拦截了一次MotionEvent.ACTION_MOVE事件,然后手指移动到当前控件的外面,那么这个Move事件将会转化为Cancel事件传递给子view。

我们知道如果某一个子View处理了Down事件,那么随之而来的Move和Up事件也会交给它处理。但是交给它处理之前,父View还是可以拦截事件的,如果拦截了事件,那么子View就会收到一个Cancel事件,并且不会收到后续的Move和Up事件

滑动问题解决:
1.如果是类似滑动开关当手指不小心移动到开关外,我们可以对cancle进行判断,触发cancle的时候,将用户未做完的动作自动执行
2.类似于自定义progressBar,触发cancle的时候没法进行自动处理.其实我们可以在onTouchEvent里面先调用requestDisallowInterceptTouchEvent(true);,这句代码的意思就是不允许父控件拦截我们的触摸事件,这样action_canlce事件就永远不会被触发.问题也就不存在了.

SurfaceView, TextureView区别?
   
从性能和安全性角度出发,使用播放器优先选SurfaceView。
1.在android 7.0上系统surfaceview的性能比TextureView更有优势,支持对象的内容位置和包含的应用内容同步更新,平移、缩放不会产生黑边。 在7.0以下系统如果使用场景有动画效果,可以选择性使用TextureView
2.SurfaceView优点及缺点优点:可以在一个独立的线程中进行绘制,不会影响主线程,使用双缓冲机制,播放视频时画面更流畅
缺点:Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中。SurfaceView 不能嵌套使用。
3.TextureView优点及缺点
优点:支持移动、旋转、缩放等动画,支持截图
缺点:必须在硬件加速的窗口中使用,占用内存比SurfaceView高,在5.0以前在主线程渲染,5.0以后有单独的渲染线程。

getX /getY   和 getRawX  /getRawY区别?
     getX、getY:触摸点相对于View的位置
     getRawX()、getRawY():触摸点相对于屏幕的位置

图中绿圆点为触摸点位置


tag: getX-----134-----getY65   +getRawX-----536-----getRawY1206
tag: getX-----134-----getY63 +  getRawX-----536-----getRawY1204
tag: getX------347-----getY-1066  + getRawX-----55-----getRawY75

ViewRoot和DecorView       

       ViewRoot的实现是 ViewRootImpl 类,是连接WindowManager和DecorView的纽带,View的三大流程( mearsure、layout、draw) 均是通过ViewRoot来完成。当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建 ViewRootImpl 对象,并将ViewRootImpl 对象和DecorView建立连接,源码如下:

    root = new ViewRootImpl(view.getContext(),display);
    root.setView(view,wparams, panelParentView);

View的绘制流程是从ViewRoot的performTraversals开始的
performTraversals会依次调用 performMeasure 、 performLayout 和performDraw 三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。其中 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。另外两个过程同理。

    Measure完成后, 可以通过getMeasuredWidth 、getMeasureHeight 方法来获取View测量后的宽/高。特殊情况下,测量的宽高不等于最终的宽高,详见后面。
    Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四个定点坐标。

getMeasuredWidth和getWidth区别

①getMeasuredWidth方法获得的值是setMeasuredDimension方法设置的值,它的值在measure方法运行后就会确定
②getWidth方法获得是layout方法中传递的四个参数中的mRight-mLeft,它的值是在layout方法运行后确定的
③一般情况下在onLayout方法中使用getMeasuredWidth方法,而在除onLayout方法之外的地方用getWidth方法。

public class MyViewGroup1 extends ViewGroup {
	public MyViewGroup1(Context context, AttributeSet attrs) {
		super(context, attrs);
      } 
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		View view = getChildAt(0);
		measureChild(view, MeasureSpec.EXACTLY + 50, MeasureSpec.EXACTLY + 100);
	}
 
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		View childView = getChildAt(0);
		childView.layout(0, 0, 200, 200);
	}
}

通过ViewGroup中的measureChild方法为其设置的宽度为50,而通过layout为其设置的宽度为200-0=200。同样在Activity的onWindowFocusChanged方法中打印getWidth和getMeasuredWidth的值。


draw和onDraw和DispatcherDraw的区别
draw()方法:

/*
*      1. Draw the background
*      2. If necessary, save the canvas' layers to prepare for fading
*      3. Draw view's content
*      4. Draw children
*      5. If necessary, draw the fading edges and restore layers
*      6. Draw decorations (scrollbars for instance)
*/

总结一下的话,draw()方法做了这些事情:
(1)绘制背景。由drawBackground()完成
(2)绘制内容。由onDraw()方法完成
(3)绘制子视图。由dispatchView()完成。View的dispatchView()是空方法,ViewGroup的dispatchView()有具体实现,主要是调用子视图的draw()方法。

onDraw()方法
它是个空方法。基本上所有的自定义view都需要实现这个方法,否则无法显示任何内容

protected void onDraw(Canvas canvas) {}

View组件的绘制会调用draw(Canvas canvas)方法,draw过程中主要是先画Drawable背景,drawable的实际大小通过getIntrinsicWidth()和getIntrinsicHeight()获取,当背景比较大时view组件大小等于背景drawable的大小。

 画完背景后,draw过程会调用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw()方法。
值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法,或者自定制一个Drawable,重写它的draw(Canvas c)和 getIntrinsicWidth(),。


事件分发机制



View滑动冲突
滑动冲突的场景以及处理规则
       场景1--外部左右滑动,内部上下滑动----的处理规则:(Viewpager和listView的嵌套滑动)
                       1、当用户左右滑动时,让外部的View拦截点击事件,当用户上下滑动时,让内部的View拦截点击事件;
                       2、判断用户的滑动方向,如果用户手指滑动的水平距离大于垂直距离,则左右滑动,反之,上下滑动                   
       场景2--内外层两层同时上下滑动或者左右滑动----的处理规则:
                      当处于某种状态需要外部View响应用户的滑动,而处于另一种状态时则需要内部View响应用户的滑动                     
       场景3--内层有场景1滑动,外层场景2滑动,外层与中层的滑动方向一致,而中层与内层的滑动方向不一致--的处理规则:
                      场景1的处理规则和场景2的处理规则一起用。

滑动冲突解决方法           
      针对场景一有两种解决滑动冲突的方法:外部拦截法和内部拦截法
      外部拦截法需要重写父容器onInterceptTouchEvent方法,在内部做相应拦截即可,伪代码如下:
                          public  boolean onInterceptTouchEvent (MotionEvent event) {
                               case MotionEvent.ACTION_MOVE:  {
                                       if (父容器需要处理当前事件){
                                         intercepted = true;
                                        } else{
                                           intercepted = false;
                                         }       
                                       break;              
                                     }
内部拦截法指父容器不拦截任何事件,配合requestDisallowInterceptTouchEvent方法,重写子元素的dispathTouchEvent        
                         public  boolean dispatchtTouchEvent (MotionEvent event) {
                                         switch (event.getAction() ) {
                                              case MotionEvent.ACTION_DOWN:  {
                                                      parent.requestDisallowInterceptTouchEvent(true);
                                             case MotionEvent.ACTION_MOVE:  {      
                                                     if (父容器需要处理当前事件){
                                                        parent.requestDisallowInterceptTouchEvent(false);}       

Handler创建源码分析

将 Handler 声明在 Activity 中,然后覆写handleMessage 方法,当子线程调用 handler.sendMessage()方法后会在主线程中执行。 这里面除了 Handler、Message外还有隐藏的 Looper和 MessageQueue对象。 

在主线程中Android默认已经调用了Looper.preper()方法,调用该方法的目的是在 Looper中创建MessageQueue成员变量,并把Looper对象绑定到当前线程中,当调用Handler的sendMessage方法的时候就将 Message对象添加到了 Looper创建的 MessageQueue队列中 ,同时给Message指定了target对象 ,其实 这个target对象就是 Handler对象。
主线程默认执行了 Looper.looper ()方法,该方法从Looper的成员变量 MessageQueue中取出Message,然后调用 Message的target对象的 handleMessage()方法。这样就完成了整个消息机制。

* 创建Handler对象时,在构造方法中会获取Looper和MessageQueue的对象
       public Handler() {
              mLooper = Looper.myLooper();
                      if(mLooper == null){
                              throw new RuntimeException;"can't create handler inside thread that has not called Looper.prepare() "; }
            mQueue = mLooper.mQueue;
            mCallback = null;
        }
* 查看myLooper方法体,发现Looper对象是通过ThreadLocal.get()得到的,在查找ThreadLocal的set方法时发现 Looper是在prepare()方法中直接new出来的,
                     public static final void prepare(){         
                                sThreadLocal.set(new Looper())  
                            }
             并且在Looper的构造方法中,new出了消息队列对象
                private Looper() {
                         mQueue = new MessageQueue();
                         mRun = true;
                         mThread = Thread.currentThread();
            }
* prepare方法是在prepareMainLooper()主线程 方法中调用的
        public static final void prepareMainLooper() {
                  prepare();
              }

* prepareMainLooper被调用是 在主线程main方法中,ActivityThread会被创建
        public static final void main(String[] args) {
                          Looper.prepareMainLooper();
                          Looper.loop();
                   }
* Looper.loop()方法中有一个死循环,为什么不会造成ANR
        while (true) {     //之所以会存在就是防止应用程序停止
            //取出消息队列的消息
             Message msg = queue.next();
        }
looper.loop() 不断地接收,处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,如果while (true)停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。

子线程中能不能 new handler?为什么?

不能,如果在子线程中直接 new Handler()会抛出异常 java.lang.RuntimeException: Can’t create handler inside thread that has not called,在没有调用 Looper.prepare()的时候不能创建 Handler,因为在创建 Handler 的源码中做了如下操作 
Handler 的构造方法中


现在有主、子线程两个线程,在主线程中有创建了handler,然后在子线程中调用handler发送一个message,子和主线程如何切换?

当在主线程中创建handler的时候,同时创建了MessageQueue与Looper,当子线程调用handler发送一个message的时候,会通过msg.target.dispatchMessage(msg);将message插入到handler对应的MessageQueue中,Looper发现MessageQueue中有message便取出,因为Looper.loop()是在主线程中启动的,所以则回到了主线程,达到了从子线程切换到主线程的目的。

Looper.loop(),这里是一个死循环,如果主线程的Looper终止,则应用程序会抛出异常。那么问题来了,既然主线程卡在这里了,那Activity为什么还能启动 ?

startActivity的时候,会向AMS发一个跨进程请求,之后AMS启动对应的Activity;AMS也需要调用Activity的生命周期方法,然后由ActivityThread中的ApplicationThread会来处理,ApplicationThread会通过主线程线程的Handler将执行逻辑切换到主线程。重点来了,主线程的Handler把消息添加到了MessageQueue,Looper.loop会拿到该消息,并在主线程中执行。这就解释了为什么主线程的Looper是个死循环,而Activity还能启动,因为四大组件的生命周期都是以消息的形式通过UI线程的Handler发送,由UI线程的Looper执行的。

主线程的死循环一直运行是不是特别消耗CPU资源呢? 
就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作, 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

ThreadLocal,ReentrantLock,synchronized 区别以及应用场景

线程局部变量(ThreadLocal)就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。 

synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个 synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

 ReentrantLock 意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。

一个Thread可以有几个Looper?几个Handler?
 一个Thread只能有一个Looper,可以有多个Handler, Looper有一个MessageQueue,可以处理来自多个Handler的Message
 MessageQueue有一组待处理的Message,这些Message可来自不同的Handler;Message中记录了负责发送和处理消息的Handler;Handler中有Looper和MessageQueue
 

Handler.postDelayed()的原理    你真的懂Handler.postDelayed()的原理吗?-CSDN博客
Handler.postDelayed()是先delay一定时间然后再放入messageQueue中还是先直接放入MessageQueue中,然后wait delay的时间?

1、假如postDelay()一个10秒钟的Runnable A消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;
2、紧接着post()一个Runnable B消息进队,现在A时间还没到在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;
3、MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
4、Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;
5、直到阻塞时间到或者下一次有Message进队

handler.post和handler.sendMessage的区别和联系
handler.post和handler.sendMessage本质上是没有区别的,都是发送一个消息到消息队列中,而且消息队列和handler都是依赖于同一个线程的。

/**
     * 这里源码注释的意思是:把r这个任务对象添加到消息队列中。
     */
    public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

/**
    
*然后我们接着看post方法中直接调用到的发送延时消息的方法,源码注释*的意思是把这个消息放入消息队列,
     */
    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

AsyncTask中使用的线程池大小?
在AsyncTask内部实现有两个线程池
SerialExecutor:用于任务的排队,默认是串行的线程池,在3.0以前核心线程数为5、线程池大小为128,而3.0以后变为同一时间只能处理一个任务
THREAD_POOL_EXECUTOR:用于真正执行任务。

Invalidate、postInvalidate、requestLayout应用场景与区别
(1)Invalidate和postInvalidate的区别
  postInvalidate() 方法在子线程中调用,通知 UI 线程重绘。
  invalidate() 方法在 UI 线程中调用,重绘当前 UI。
(2)总结
View绘制分三个步骤,顺序是:onMeasure,onLayout,onDraw。
调用invalidate方法只会执行onDraw方法,因为在performTraversals方法中,mLayoutRequested为false;
调用requestLayout方法只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。

所以当我们进行View更新时,若仅View的显示内容发生改变且不影响View的大小、位置,则只需调用invalidate方法;若View宽高、位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法。
requestLayout : 当前布局的宽高发生改变, 此时需要重新调用父view的onMeaure和onLayout, 来给子view重新排版布局
invalidate : 让页面刷新, 重新调用onDraw方法,
postInvalidate : 在子线程来让页面来进行刷新的方法

子线程中如何使用Handler
子线程中没有Looper会抛异常,给他一个Looper就好了
Looper.prepare();
mHandler = new Handler(){
    @Override
  public void handleMessage(Message msg) {
            Log.d(TAG," mHandler is coming");
  handler_main.sendEmptyMessage(1);
  }
};
mHandler.sendEmptyMessage(1);
Looper.loop();
如果在调用之前必须调用Looper.prepare()方法,这个是在当前线程中创建一个looper出来,如果是普通的应用场景可以直接使用HandlerThread,其中是带有Looper的。
第二点值得注意的就是,Looper.loop()这个方法是无限循环的,所以在Looper.loop()后边的程序代码块是无法执行到的。loop()方法的主要作用是一直不断的通过queue.next()方法来读取来自messagequeue中的msg,这个方法是block的状态,如果queue中没有消息的话会一直阻塞在这里。
关于Looper还有一个方法,当我们需要获取Looper实例时,可以直接在对应线程调用Looper looper = Looper.myLooper();来获取,默认情况下,系统只会给MainThread分配一个looper。

为什么不能在子线程刷新UI  ?
    因为Android的UI控件不是线程安全的,多线程并发访问可能会导致UI控件处于不可预期的状态,为什么不加锁?因为加锁机制会让UI访问逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。所以Android采用了高效的单线程模型来处理UI操作。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值