Android面试题搜集汇总

好记性不如烂笔头,api年年更新年年学。面试题年年出新也得年年会。话不多说,汇总!


线程池的工作原理

使用线程池的优势

  • 降低资源消耗
  • 提升系统响应速度
  • 提高线程的课管理性

线程池执行流程图

线程池的创建

ThreadPoolExecutor 的有许多重载的构造方法,通过参数最多的构造方法来理解创建线程池有哪些需要配置的参数。以下是其中之一

1
2
3
4
5
6
7
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

线程参数说明

  • corePoolSize

表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到 corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了 corePoolSize
则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候所有的核心线程都会被创建并且启动。

  • maximumPoolSize
    表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过 maximumPoolSize 的话,就会创建新的线程来执行任务。

  • keepAliveTime
    空闲线程存活时间。如果当前线程池的线程个数已经超过了 corePoolSize,并且线程空闲时间超过了 keepAliveTime 的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。

  • unit
    时间单位。为 keepAliveTime 指定时间单位。

  • workQueue
    阻塞队列。用于保存任务的阻塞队列,关于阻塞队列可以看这篇文章。可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。

  • threadFactory
    创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。

  • handler
    饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。

    • 采用的策略有这几种:
      AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
      CallerRunsPolicy:只用调用者所在的线程来执行任务;
      DiscardPolicy:不处理直接丢弃掉任务;
      DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务

线程关闭

关闭线程池,可以通过shutdown和shutdownNow这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。
调用了这两个方法的任意一个,isShutdown方法都会返回 true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated方法才会返回 true。

  • shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表
  • shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程

线程池的状态

  • RUNNING, 运行状态,值也是最小的,刚创建的线程池就是此状态。
  • SHUTDOWN,停工状态,不再接收新任务,已经接收的会继续执行
  • STOP,停止状态,不再接收新任务,已经接收正在执行的,也会中断
  • 清空状态,所有任务都停止了,工作的线程也全部结束了
  • TERMINATED,终止状态,线程池已销毁

线程池提交任务的方式

  • execute
    execute是ExecutorService接口定义的。
  • submit
    submit有三种方法重载都在AbstractExecutorService中定义

都是将要执行的任务包装为FutureTask来提交,使用者可以通过FutureTask来拿到任务的执行状态和执行最终的结果,最终调用的都是execute方法,其实对于线程池来说,它并不关心你是哪种方式提交的,因为任务的状态是由FutureTask自己维护的,对线程池透明。

如何合理配置线程池参数?

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  • 任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务。
  • 任务的优先级:高,中和低。
  • 任务的执行时间:长,中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU 密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,如果等待的时间越长 CPU 空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用 CPU。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。、

线程池的线程是如何做到复用的

线程池中的线程在循环中尝试取任务执行,这一步会被阻塞,如果设置了allowCoreThreadTimeOut为true,则线程池中的所有线程都会在keepAliveTime时间超时后还未取到任务而退出。或者线程池已经STOP,那么所有线程都会被中断,然后退出。

线程池是如何做到高效并发的

看整个线程池的工作流程,有以下几个需要特别关注的并发点.
①: 线程池状态和工作线程数量的变更。这个由一个AtomicInteger变量 ctl来解决原子性问题。
②: 向工作Worker容器workers中添加新的Worker的时候。这个线程池本身已经加锁了。
③: 工作线程Worker从等待队列中取任务的时候。这个由工作队列本身来保证线程安全,比如LinkedBlockingQueue等

Synchronized 锁方法和锁静态方法有什么区別

Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”

Java中每个对象都有一个锁,并且是唯一的。假设分配的一个对象空间,里面有多个方法,相当于空间里面有多个小房间,如果我们把所有的小房间都加锁,因为这个对象只有一把钥匙,因此同一时间只能有一个人打开一个小房间,然后用完了还回去,再由JVM 去分配下一个获得钥匙的人。

  • 情况1:同一个对象在两个线程中分别访问该对象的两个同步方法
    结果:会产生互斥。
    解释:因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。

  • 情况2:不同对象在两个线程中调用同一个同步方法
    结果:不会产生互斥。
    解释:因为是两个对象,锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new 一个对象,那么就会出现两个空间,两把钥匙

2.Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”

  • 情况1:用类直接在两个线程中调用两个不同的同步方法
    结果:会产生互斥。
    解释:因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
    注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。

  • 情况2:用一个类的静态对象在两个线程中调用静态方法或非静态方法
    结果:会产生互斥。
    解释:因为是一个对象调用,同上。

  • 情况3:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法
    结果:不会产生互斥。
    解释:因为虽然是一个对象调用,但是两个方法的锁类型不同,调用的静态方法实际上是类对象在调用,即这两个方法产生的并不是同一个对象锁,因此不会互斥,会并发执行。

总结

一定要注意哪个对象正被用于锁定:

  • 1、调用同一个对象中非静态同步方法的线程是互斥的。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。
  • 2、调用同一个类中的静态同步方法的线程将是互斥的,它们都是锁定在相同的Class对象上。
  • 3、静态同步方法和非静态同步方法将永远不是互斥的,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。
  • 4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将是互斥的,在不同对象上锁定的线程将永远不会互斥。

锁的理解,什么是乐观锁,悲观锁,可重入锁

一个锁可以同时是悲观锁、可重入锁、公平锁、可中断锁等等

synchronized与Lock

Java中有两种加锁的方式:一种是用synchronized关键字,另一种是用Lock接口的实现类。synchronized是Java语言内置的关键字,而Lock是一个接口。

形象地说:

  • synchronized关键字是自动档,可以满足一切日常驾驶需求。
  • 但是如果你想要玩漂移或者各种骚操作,就需要手动档了——各种Lock的实现类。

源码关系:

  • ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三个实现类。对应了“可重入锁”、“读锁”和“写锁”
  • ReadWriteLock其实是一个工厂接口,而ReentrantReadWriteLock是ReadWriteLock的实现类,它包含两个静态内部类ReadLock和WriteLock。这两个静态内部类又分别实现了Lock接口.

悲观锁与乐观锁

锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。

  • 悲观锁(Pessimistic Lock)
    就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。

  • 乐观锁(Optimistic Lock)
    就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。

  • 悲观锁阻塞事务,乐观锁回滚重试
    它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

乐观锁的基础——CAS

什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
1、比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
2、设置:如果是,将A更新为B,结束。[1]如果不是,则什么都不做。
上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。

因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!

自旋锁

有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。
刚刚的乐观锁就有类似的无限循环操作,那么它是自旋锁吗?
不是。尽管自旋与 while(true) 的操作是一样的,但还是应该将这两个术语分开。“自旋”这两个字,特指自旋锁的自旋。
然而在JDK中并没有自旋锁(SpinLock)这个类,那什么才是自旋锁呢?请看下面。

synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。

  • 问题:当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?

偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。

可重入锁(递归锁)

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。如果你需要不可重入锁,只能自己去实现了。网上不可重入锁的实现真的很多。

公平锁、非公平锁

如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。

对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。

1
ReentrantLock(boolean fair)

对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁。

可中断锁

可中断锁,字面意思是“可以响应中断的锁”。

这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。

如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。

在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以简单看下Lock接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Lock接口 */
public interface Lock {

void lock(); // 拿不到锁就一直等,拿到马上返回。

void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

void unlock();

Condition newCondition();
}

读写锁、共享锁、互斥锁

读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。
Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁。

如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。

虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。

JDK提供的唯一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。 除了两个接口方法以外,ReentrantReadWriteLock还提供了一些便于外界监控其内部工作状态的方法。

总结

在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。

那JDK并发包里到底有没有乐观锁呢?
有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。

View 的事件分发流程

dispatchTouchEvent代表分发事件,onInterceptTouchEvent()代表拦截事件,onTouchEvent()代表消耗事件,由自己处理。

默认状态下事件是按照从Activity到ViewGroup再到View的顺序进行分发的,分发下去处不处理是另一回事,分发完成后,不处理则向上一层回调,调用上一层的onTouchEvent进行处理事件,若onTouchEvent返回true,则表示在该层消耗了事件,若返回false,表示事件还没被处理,需要再向上回调一层,调用上一层的onTouchEvent方法。

当一个点击事件发生时,从Activity的事件分发开始

点击某个空间所在布局

控件被点击时

总结

View 的三种测量模式理解,什么时候会发生 Excatly

View 从加载到呈现到屏幕上的过程需要经过三个阶段

  • measure 机制测量每个 View 的大小,保证 View 的大小显示正常。
  • layout 机制(ViewGroup特有,一般 View 没有)将每个子view放在合适的位置。
  • draw 机制,精准的绘制每个 View 的内容。

测量模式

  • EXACTLY
    精确值模式,当控件的layout_width和layout_height属性指定为具体数值或match_parent时。
  • AT_MOST
    最大值模式,当空间的宽高设置为wrap_content时。
  • UNSPECIFIED
    未指定模式,View想多大就多大,通常在绘制自定义View时才会用。

measure 过程

  • 父 View 获取子 View 的高宽(就是laytout_width与layout_height中设置的值),然后结合父 View 的测量模式决定子 View 的测量模式与初始的宽高(因为在onMeasure中可以更改)。
    三种宽高模式,最终给子view的模式由父view和子View结合得出。
  • 父 View 调用子view的 measure() 方法,然后转调用 onMeasure() 方法执行测量。
  • 如果自定View没有重写 onMeasure 方法,则会使用基类View的 onMeasure() 方法处理测量。

measure 处理

  • 覆写 onMeasure 方法
    1
    2
    3
     @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    }
  • 分别处理高度和宽度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureWidth(widthMeasureSpec);
    }

    private int measureWidth(int widthMeasureSpec) {
    // 测量宽度
    // 获取宽度模式
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    // 获取父View建议宽度
    int recommendWidth = MeasureSpec.getSize(widthMeasureSpec);
    // 最终的宽度
    int finalWidth = recommendWidth;
    switch (widthMode) {
    case MeasureSpec.EXACTLY:
    // 已经准确的给了值,直接使用即可
    break;
    case MeasureSpec.AT_MOST:
    // 这里对应 wrap_content,但是父view将尺寸设为了自身对应的尺寸,需要我们自行处理
    // 处理逻辑是我们自身设定的最小需要尺寸+对应尺寸的内边距,外边距不用考虑
    finalWidth = radius*2 + getPaddingLeft() + getPaddingRight();
    break;
    case MeasureSpec.UNSPECIFIED:
    // 没有限制尺寸,保持父view大小即可
    break;
    }
    return finalWidth;
    }
  • 调用设置尺寸函数,完成测量
    1
    2
    3
    4
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(measureWidth(widthMeasureSpec),measureHieght(heightMeasureSpec));
    }

注意

重点!!!!不同的 ViewGroup 实现这里有差异,比如可滚动的 ViewGroup 不会去限制子 View 模式为 WRAP_CONTENT 时滚动方向的尺寸(可以参考 RecyclerView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 父 View 获取自身的测量模式和尺寸
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
// 父 View 根据自身的测量模式和子view的测量模式决定子view最终的测量模式和尺寸
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
// 父view是精准模式,对应准确数值和match_parent
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
// 这里,上节我们将自定义view的高度设置为 wrap_content 模式了,父view再这里直接将子view的最终尺寸设置为自身的尺寸。而在自定义view中我们没有处理 onMeasure ,因此自view的最终高度为父view的高度。
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

容易混淆的方法

  • 直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
  • setSelection()方法 :请求重新draw(),但只会绘制调用者本身。
  • setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。
  • setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。

requestLayout()方法 :会导致调用measure()过程 和 layout()过程 。
setVisibility()方法:
当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。

Hashmap相关问题

Hashmap工作原理

  • HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值。

  • 其它关于HashMap比较重要的问题是容量、负荷系数和阀值调整。HashMap默认的初始容量是16,负荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你知道你需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对HashMap进行初始化是个不错的做法。

hashCode()和equals()方法有何重要性

HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则
(1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。
(2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。

为什么重写equals()的时候一定要重写hashcode():

HashMap中,如果要比较key是否相等,要同时使用这两个函数!因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地 址,这样即便有相同含义的两个对象,比较也是不相等的,例如,生成了两个“羊”对象,正常理解这两个对象应该是相等的,但如果你不重写hashcode()方法的话,则比较是不相等的。

put()函数的实现

put函数大致的思路为:

  • 对key的hashCode()做hash,然后再计算index;
  • 如果没碰撞直接放到bucket里;
  • 如果碰撞了,以链表的形式存在buckets后;
  • 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树参考此链接:Java 8:HashMap的性能提升;
  • 如果节点已经存在就替换old value(保证key的唯一性)
  • 如果bucket满了(超过load factor*current capacity),就要resize。

get()函数的实现

大致思路如下:

  • bucket里的第一个节点,直接命中;
  • 如果有冲突,则通过key.equals(k)去查找对应的entry
  • 若为树,则在树中通过key.equals(k)查找,O(logn);
  • 若为链表,则在链表中通过key.equals(k)查找,O(n)。

结构与参数

系统在初始化HashMap时,会创建一个 长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。
在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor):

1
Capacity就是buckets的数目,Load factor就是buckets填满程度的最大比例。如果对迭代性能要求很高的话不要把capacity设置过大,也不要把load factor设置过小。当bucket填充的数目(即hashmap中元素的个数)大于capacity*load factor时就需要调整buckets的数目为当前的2倍。

Hashmap为什么容量是2的幂次

最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较K,而且空间利用率最大。那如何计算才会分布最均匀呢?我们首先想到的就是%运算,哈希值%容量=bucketIndex。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
static int indexFor(int h, int length) {  
return h & (length-1);
}
~~~
这个等式实际上可以推理出来,2^n转换成二进制就是1+n个0,减1之后就是0+n个1,如16 -> 1000015 -> 01111,那根据&位运算的规则,都为1(真)时,才为1,那0≤运算后的结果≤15,假设h <= 15,那么运算后的结果就是h本身,h >15,运算后的结果就是最后三位二进制做&运算后的值,最终,就是%运算后的余数,我想,这就是容量必须为2的幂的原因。

### 总结

#### 1. 什么是HashMap?你为什么用到它?
是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。

#### 2. 你知道HashMap的工作原理吗?
通过hash的方式,以键值对<K,V>的方式存储(put)、获取(get)对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

#### 3. 你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点。

#### 4. 你知道hash的实现吗?为什么要这样实现?
在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。

#### 5. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。

#### 6. 什么是哈希冲突?如何解决的?
以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。
插入元素时,如果两条Key落在同一个桶(比如哈希值117取模16后都属于第一个哈希桶),我们称之为哈希冲突。

JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。
在JDK8里,新增默认为8的阈值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。
当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。

#### 7. HashMap在高并发下引起的死循环

* HashMap进行存储时,如果size超过当前最大容量*负载因子时候会发生resize。
* 而这段代码中又调用了transfer()方法,而这个方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生get()死循环
* 链表头插法的会颠倒原来一个散列桶里面链表的顺序。在并发的时候原来的顺序被另外一个线程a颠倒了,而被挂起线程b恢复后拿扩容前的节点和顺序继续完成第一次循环后,又遵循a线程扩容后的链表顺序重新排列链表中的顺序,最终形成了环。
* 假如有两个线程P1、P2,以及链表 a=》b=》null
1. P1先执行,执行完"Entry<K,V> next = e.next;"代码后发生阻塞,或者其他情况不再执行下去,此时e=a,next=b
2. 而P2已经执行完整段代码,于是当前的新链表newTable[i]为b=》a=》null
3. P1又继续执行"Entry<K,V> next = e.next;"之后的代码,则执行完"e=next;"后,newTable[i]为a《=》b,则造成回路,while(e!=null)一直死循环

## rxjava的背压了解吗?

被观察者发送消息太快以至于它的操作符或者订阅者不能及时处理相关的消息,从而操作消息的阻塞的现象。

### 阻塞是怎么形成的

#### 在RxJava1.0中,Observable是支持背压的,翻下源码,可以看到在Rxjava1.0中的Buffer的大小为16.Observable.java 3551行
~~~java
public final <B> Observable<List<T>> buffer(Observable<B> boundary) {
return buffer(boundary, 16);
}

需要在发送和接收在不同线程就可触发。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
for (int i = 0; ; i++) { //无限循环发事件
emitter.onNext(i);
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {

Log.d(TAG, "" + integer);
}
});

RXJava2.0中Observable不再支持背压,多出了Flowable来支持背压操作

如果用Observable发送造成上面的阻塞,其结果是内存会不断增大,导致程序异常。

RxJava2缓存池的最大大小是128,如果缓存池里有超过128个事件就会抛出异常

如何解决阻塞

让上游发送事件的速度慢点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 //控制发送速度,减少内存消耗
Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
for (int i = 0; ; i++) { //无限循环发事件
emitter.onNext(i);
Thread.sleep(1000);
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {

Log.d(TAG, "" + integer);
}
});

下游少接收点事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定时取样
Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
for (int i = 0; ; i++) { //无限循环发事件
emitter.onNext(i);

}
}
}).sample(1, TimeUnit.SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {

Log.d(TAG, "" + integer);
}
});

过滤操作符,过滤掉一些上游事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  //过滤器 过滤操作
Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
for (int i = 0; ; i++) { //无限循环发事件
emitter.onNext(i);
}
}
}).filter(new Predicate<Integer>() {
@Override
public boolean test(Integer integer) throws Exception {
return integer % 100 == 0;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer integer) throws Exception {

Log.d(TAG, "" + integer);
}
});

采用提供的背压策略

  • MISSING:
    如果流的速度无法保持同步,可能会抛出MissingBackpressureException或IllegalStateException。

  • BUFFER
    上游不断的发出onNext请求,直到下游处理完,也就是和Observable一样了,缓存池无限大,最后直到程序崩溃

  • ERROR
    会在下游跟不上速度时抛出MissingBackpressureException。

  • DROP
    会在下游跟不上速度时把onNext的值丢弃。

  • LATEST
    会一直保留最新的onNext的值,直到被下游消费掉。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Flowable.create(new FlowableOnSubscribe<Integer>() {
@Override
public void subscribe(FlowableEmitter<Integer> e) throws Exception {
Log.d(TAG, "emit 1");
emitter.onNext(1);
Log.d(TAG, "emit 2");
emitter.onNext(2);
Log.d(TAG, "emit 3");
emitter.onNext(3);
Log.d(TAG, "emit complete");
emitter.onComplete();
}
}, BackpressureStrategy.ERROR)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<Integer>() {
@Override
public void onSubscribe(Subscription s) {

}

@Override
public void onNext(Integer s) {
Log.d(TAG, "onNext: " + integer);
}

@Override
public void onError(Throwable t) {
Log.d(TAG, "onError"+t);

}

@Override
public void onComplete() {
Log.d(TAG, "onComplete");

}
});

下游方法中出现了一个方法

1
2
3
4
@Override
public void onSubscribe(Subscription s) {
s.request(2);
}
  • Subscription.java
    1
    2
    3
    4
    public interface Subscription {
    public void request(long n);
    public void cancel();
    }

这里需要重点说明一下,在Flowable中背压采取拉取响应的方式来进行水流控制,也就是说Subscription是控制上下游水流的主要方式,一般的,我们需要调用Subscription.request()来传入一个下游目前能处理的事件的数量

Kotlin的协程,怎么做到和 rxjava 的 zip 操作一样等待所有结果后再处理

rxjava zip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Observable.zip(editUnitInfoModel.getBaiduRegeo(location),
editUnitInfoModel.getGaodeRegeo(location),
BiFunction<BaiduRegeoBean, GaodeRegeoBean, LocationDetailsBean> {
baidu, gaode ->
val locationDetailsBean = LocationDetailsBean()
if (baidu.status == 0) {
val baiduAddressComponent = baidu.result.addressComponent
locationDetailsBean.adcode = baiduAddressComponent.adcode
locationDetailsBean.city = baiduAddressComponent.city
locationDetailsBean.country = baiduAddressComponent.country
locationDetailsBean.district = baiduAddressComponent.district
locationDetailsBean.province = baiduAddressComponent.province
locationDetailsBean.street = baiduAddressComponent.street
locationDetailsBean.streetNumber = baiduAddressComponent.streetNumber
locationDetailsBean.town = baiduAddressComponent.town
}
if (gaode.status == "1") {
locationDetailsBean.townCode = gaode.regeocode.addressComponent.towncode
}
LogUtils.i("合并后的定位信息为:${locationDetailsBean}")
return@BiFunction locationDetailsBean
})

kotlin协程

  • 假如这两个接口之间没有联系,我们想让他们并发执行的话,我们可以使用async和await配合使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    //模拟网络请求
    suspend fun one(): Int {
    delay(1500)
    return 1
    }

    suspend fun two(): Int {
    delay(1500)
    return 2
    }
    //执行网络
    fun main() {
    GlobalScope.launch {
    /*measureTimeMillis返回给定的block代码的执行时间*/
    val time = measureTimeMillis {
    val sum = withContext(Dispatchers.IO) {
    val one = async { one() }
    val two = async { two() }
    one.await() + two.await()
    }
    println("两个方法返回值的和:${sum}")
    }
    println("执行耗时:${time}")
    }
    println("----------------")
    /*应为上面的协程代码并不会阻塞掉线程,所以我们这里让线程睡4秒,保证线程的存活,在实际的Android开发中无需这么做*/
    Thread.sleep(4000)
    }
  • Kotlin协程处理多个耗时操作按顺序执行

假如你有这样的需求,接口B的参数是接口A返回的结果,那这样的话,使用协程依然能够很简洁的实现。
我们把两个方法稍微改动一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//模拟网络请求,方法two接收一个参数,该参数是由方法one返回的结果决定的
suspend fun one(): Int {
delay(1000)
return 1
}
suspend fun two(int: Int): Int {
delay(2000)
return 2 + int
}
//执行网络请求
fun main() {

GlobalScope.launch {

/*measureTimeMillis返回给定的block代码的执行时间*/
val time = measureTimeMillis {
val sum = withContext(Dispatchers.IO) {
val one = one()
val two = two(one)
one + two
}
println("两个方法返回值的和:${sum}")
}
println("执行耗时:${time}")
}
println("----------------")
/*因为上面的协程代码并不会阻塞掉线程,所以我们这里让线程睡4秒,保证线程的存活,在实际的Android开发中无需这么做*/
Thread.sleep(4000)
}

参考来源

推荐阅读