System.out.println(“生产了一个新产品,现库存为:” + list.size());
list.notifyAll();
}
}
public void consume() {
synchronized (list) {
while (list.size() == 0) {
System.out.println(“库存为0:消费暂停”);
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println(“消费了一个产品,现库存为:” + list.size());
list.notifyAll();
}
}
}
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class StorageWithAwaitAndSignal {
private final int MAX_SIZE = 10;
private ReentrantLock mLock = new ReentrantLock();
private Condition mEmpty = mLock.newCondition();
private Condition mFull = mLock.newCondition();
private LinkedList mList = new LinkedList();
public void produce() {
mLock.lock();
while (mList.size() == MAX_SIZE) {
System.out.println(“缓冲区满,暂停生产”);
try {
mFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mList.add(new Object());
System.out.println(“生产了一个新产品,现容量为:” + mList.size());
mEmpty.signalAll();
mLock.unlock();
}
public void consume() {
mLock.lock();
while (mList.size() == 0) {
System.out.println(“缓冲区为空,暂停消费”);
try {
mEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mList.remove();
System.out.println(“消费了一个产品,现容量为:” + mList.size());
mFull.signalAll();
mLock.unlock();
}
}
import java.util.concurrent.LinkedBlockingQueue;
public class StorageWithBlockingQueue {
private final int MAX_SIZE = 10;
private LinkedBlockingQueue list = new LinkedBlockingQueue(MAX_SIZE);
public void produce() {
if (list.size() == MAX_SIZE) {
System.out.println(“缓冲区已满,暂停生产”);
}
try {
list.put(new Object());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“生产了一个产品,现容量为:” + list.size());
}
public void consume() {
if (list.size() == 0) {
System.out.println(“缓冲区为空,暂停消费”);
}
try {
list.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“消费了一个产品,现容量为:” + list.size());
}
}
final 可以修饰类、变量和方法。修饰类代表这个类不可被继承。修饰变量代表此变量不可被改变。修饰方法表示此方法不可被重写 (override)。
finally 是保证重点代码一定会执行的一种机制。通常是使用 try-finally 或者 try-catch-finally 来进行文件流的关闭等操作。
finalize 是 Object 类中的一个方法,它的设计目的是保证对象在垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9已经被标记为 deprecated。
Java 中常见的单例模式实现有这么几种:饿汉式、双重判断的懒汉式、静态内部类实现的单例、枚举实现的单例。 这里着重讲一下双重判断的懒汉式和静态内部类实现的单例。
双重判断的懒汉式:
public class SingleTon {
//需要注意的是volatile
private static volatile SingleTon mInstance;
private SingleTon() {
}
public static SingleTon getInstance() {
if (mInstance == null) {
synchronized (SingleTon.class) {
if (mInstance == null) {
mInstance=new SingleTon();
}
}
}
return mInstance;
}
}
双重判断的懒汉式单例既满足了延迟初始化,又满足了线程安全。通过 synchronized 包裹代码来实现线程安全,通过双重判断来提高程序执行的效率。这里需要注意的是单例对象实例需要有 volatile 修饰,如果没有 volatile 修饰,在多线程情况下可能会出现问题。原因是这样的,mInstance=new SingleTon()
这一句代码并不是一个原子操作,它包含三个操作:
我们知道 JVM 会发生指令重排,正常的执行顺序是1-2-3
,但发生指令重排后可能会导致1-3-2
。我们考虑这样一种情况,当线程 A 执行到1-3-2
的3步骤暂停了,这时候线程 B 调用了 getInstance,走到了最外层的if判断上,由于最外层的 if 判断并没有 synchronized 包裹,所以可以执行到这一句,这时候由于线程 A 已经执行了步骤3,此时 mInstance 已经不为 null 了,所以线程B直接返回了 mInstance。但其实我们知道,完整的初始化必须走完这三个步骤,由于线程 A 只走了两个步骤,所以一定会报错的。
解决的办法就是使用 volatile 修饰 mInstance,我们知道 volatile 有两个作用:保证可见性和禁止指令重排,在这里关键在于禁止指令重排,禁止指令重排后保证了不会发生上述问题。
静态内部类实现的单例:
class SingletonWithInnerClass {
private SingletonWithInnerClass() {
}
private static class SingletonHolder{
private static SingletonWithInnerClass INSTANCE=new SingletonWithInnerClass();
}
public SingletonWithInnerClass getInstance() {
return SingletonHolder.INSTANCE;
}
}
由于外部类的加载并不会导致内部类立即加载,只有当调用 getInstance 的时候才会加载内部类,所以实现了延迟初始化。由于类只会被加载一次,并且类加载也是线程安全的,所以满足我们所有的需求。静态内部类实现的单例也是最为推荐的一种方式。
Java中引用类型分为四类:强引用、软引用、弱引用、虚引用。
强引用: 强引用指的是通过 new 对象创建的引用,垃圾回收器即使是内存不足也不会回收强引用指向的对象。
软引用: 软引用是通过 SoftRefrence 实现的,它的生命周期比强引用短,在内存不足,抛出 OOM 之前,垃圾回收器会回收软引用引用的对象。软引用常见的使用场景是存储一些内存敏感的缓存,当内存不足时会被回收。
弱引用: 弱引用是通过 WeakRefrence 实现的,它的生命周期比软引用还短,GC 只要扫描到弱引用的对象就会回收。弱引用常见的使用场景也是存储一些内存敏感的缓存。
虚引用: 虚引用是通过 FanttomRefrence 实现的,它的生命周期最短,随时可能被回收。如果一个对象只被虚引用引用,我们无法通过虚引用来访问这个对象的任何属性和方法。它的作用仅仅是保证对象在 finalize 后,做某些事情。虚引用常见的使用场景是跟踪对象被垃圾回收的活动,当一个虚引用关联的对象被垃圾回收器回收之前会收到一条系统通知。
Exception 和 Error 都继承于 Throwable,在 Java 中,只有 Throwable 类型的对象才能被 throw 或者 catch,它是异常处理机制的基本组成类型。
Exception 和 Error 体现了 Java 对不同异常情况的分类。Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。
Error 是指在正常情况下,不大可能出现的情况,绝大部分 Error 都会使程序处于非正常、不可恢复的状态。既然是非正常,所以不便于也不需要捕获,常见的 OutOfMemoryError 就是 Error 的子类。
Exception 又分为 checked Exception 和 unchecked Exception。
一般提到 volatile,就不得不提到内存模型相关的概念。我们都知道,在程序运行中,每条指令都是由 CPU 执行的,而指令的执行过程中,势必涉及到数据的读取和写入。程序运行中的数据都存放在主存中,这样会有一个问题,由于 CPU 的执行速度是要远高于主存的读写速度,所以直接从主存中读写数据会降低 CPU 的效率。为了解决这个问题,就有了高速缓存的概念,在每个 CPU 中都有高速缓存,它会事先从主存中读取数据,在 CPU 运算之后在合适的时候刷新到主存中。
这样的运行模式在单线程中是没有任何问题的,但在多线程中,会导致缓存一致性的问题。举个简单的例子:i=i+1
,在两个线程中执行这句代码,假设i的初始值为0。我们期望两个线程运行后得到2,那么有这样的一种情况,两个线程都从主存中读取i到各自的高速缓存中,这时候两个线程中的i都为0。在线程1执行完毕得到i=1
,将之刷新到主存后,线程2开始执行,由于线程2中的i是高速缓存中的0,所以在执行完线程2之后刷新到主存的i仍旧是1。
所以这就导致了对共享变量的缓存一致性的问题,那么为了解决这个问题,提出了缓存一致性协议:当 CPU 在写数据时,如果发现操作的是共享变量,它会通知其他 CPU 将它们内部的这个共享变量置为无效状态,当其他 CPU 读取缓存中的共享变量时,发现这个变量是无效的,它会从新从主存中读取最新的值。
在Java的多线程开发中,有三个重要概念:原子性、可见性、有序性。
把一个变量声明为volatile,其实就是保证了可见性和有序性。 可见性我上面已经说过了,在多线程开发中是很有必要的。这个有序性还是得说一下,为了执行的效率,有时候会发生指令重排,这在单线程中指令重排之后的输出与我们的代码逻辑输出还是一致的。但在多线程中就可能发生问题,volatile在一定程度上可以避免指令重排。
volatile的原理是在生成的汇编代码中多了一个lock前缀指令,这个前缀指令相当于一个内存屏障,这个内存屏障有3个作用:
http 是超文本传输协议,而 https 可以简单理解为安全的 http 协议。https 通过在 http 协议下添加了一层 ssl 协议对数据进行加密从而保证了安全。https 的作用主要有两点:建立安全的信息传输通道,保证数据传输安全;确认网站的真实性。
提到 https 的话首先要说到加密算法,加密算法分为两类:对称加密和非对称加密。
对称加密: 加密和解密用的都是相同的秘钥,优点是速度快,缺点是安全性低。常见的对称加密算法有 DES、AES 等等。
非对称加密: 非对称加密有一个秘钥对,分为公钥和私钥。一般来说,私钥自己持有,公钥可以公开给对方,优点是安全性比对称加密高,缺点是数据传输效率比对称加密低。采用公钥加密的信息只有对应的私钥可以解密。常见的非对称加密包括RSA等。
在正式的使用场景中一般都是对称加密和非对称加密结合使用,使用非对称加密完成秘钥的传递,然后使用对称秘钥进行数据加密和解密。二者结合既保证了安全性,又提高了数据传输效率。
AIDL 、广播、文件、socket、管道
Android 中常用的性能优化工具包括这些:Android Studio 自带的 Android Profiler、LeakCanary、BlockCanary
Android 自带的 Android Profiler 其实就很好用,Android Profiler 可以检测三个方面的性能问题:CPU、MEMORY、NETWORK。
LeakCanary 是一个第三方的检测内存泄漏的库,我们的项目集成之后 LeakCanary 会自动检测应用运行期间的内存泄漏,并将之输出给我们。
BlockCanary 也是一个第三方检测UI卡顿的库,项目集成后Block也会自动检测应用运行期间的UI卡顿,并将之输出给我们。
二者都是从BaseDexClassLoader继承下来的,都可以加载jar包,app等等中的class字节码,唯一的区别是DexClassLoader可以指定optimizedDirectory,而PathClassLoader不能指定,默认是null。 这个optimizedDirectory的作用是存放dex的优化产物odex文件的目录,如果传null则odex文件会放在系统内部的一个默认位置。 odex文件是dex文件针对不同手机的优化产物,虽然dex文件在所有Android机子上都可以运行,但odex针对不同的手机做了相应的优化,效率更高,速度更快。 在8.0以后的系统中,Android实际上废弃了optimizedDirectory这个目录作用,所以8.0以后,PathClassLoader和DexClassLoader二者作用完全一致。
Android中动画大致分为3类:帧动画、补间动画(Tween Animation)、属性动画(Property Animation)。
View.animate()
即可得到 ViewPropertyAnimator,之后进行相应的动画操作即可。后者适合用于为我们的自定义控件添加动画,当然首先我们应该在自定义 View 中添加相应的 getXXX()
和 setXXX()
相应属性的 getter 和 setter 方法,这里需要注意的是在 setter 方法内改变了自定义 View 中的属性后要调用 invalidate()
来刷新View的绘制。之后调用 ObjectAnimator.of
属性类型()返回一个 ObjectAnimator,调用 start()
方法启动动画即可。补间动画与属性动画的区别:
说到 Handler,就不得不提与之密切相关的这几个类:Message、MessageQueue,Looper。
Message。 Message 中有两个成员变量值得关注:target 和 callback。
target 其实就是发送消息的 Handler 对象
callback 是当调用 handler.post(runnable)
时传入的 Runnable 类型的任务。post 事件的本质也是创建了一个 Message,将我们传入的这个 runnable 赋值给创建的Message的 callback 这个成员变量。
MessageQueue。 消息队列很明显是存放消息的队列,值得关注的是 MessageQueue 中的 next()
方法,它会返回下一个待处理的消息。
Looper。 Looper 消息轮询器其实是连接 Handler 和消息队列的核心。首先我们都知道,如果想要在一个线程中创建一个 Handler,首先要通过 Looper.prepare()
创建 Looper,之后还得调用 Looper.loop()
开启轮询。我们着重看一下这两个方法。
prepare()
。 这个方法做了两件事:首先通过ThreadLocal.get()
获取当前线程中的Looper,如果不为空,则会抛出一个RunTimeException,意思是一个线程不能创建2个Looper。如果为null则执行下一步。第二步是创建了一个Looper,并通过 ThreadLocal.set(looper)。
将我们创建的Looper与当前线程绑定。这里需要提一下的是消息队列的创建其实就发生在Looper的构造方法中。
loop()
。 这个方法开启了整个事件机制的轮询。它的本质是开启了一个死循环,不断的通过 MessageQueue的next()
方法获取消息。拿到消息后会调用 msg.target.dispatchMessage()
来做处理。其实我们在说到 Message 的时候提到过,msg.target
其实就是发送这个消息的 handler。这句代码的本质就是调用 handler的dispatchMessage()。
Handler。 上面做了这么多铺垫,终于到了最重要的部分。Handler 的分析着重在两个部分:发送消息和处理消息。
*发送消息。其实发送消息除了 sendMessage 之外还有 sendMessageDelayed 和 post 以及 postDelayed 等等不同的方式。但它们的本质都是调用了 sendMessageAtTime。在 sendMessageAtTime 这个方法中调用了 enqueueMessage。在 enqueueMessage 这个方法中做了两件事:通过 msg.target = this
实现了消息与当前 handler 的绑定。然后通过 queue.enqueueMessage
实现了消息入队。
dispatchMessage()
这个方法。这个方法里面的逻辑很简单,先判断 msg.callback
是否为 null,如果不为空则执行这个 runnable。如果为空则会执行我们的handleMessage
方法。Android 中的性能优化在我看来分为以下几个方面:内存优化、布局优化、网络优化、安装包优化。
内存优化: 下一个问题就是。
布局优化: 布局优化的本质就是减少 View 的层级。常见的布局优化方案如下
在 LinearLayout 和 RelativeLayout 都可以完成布局的情况下优先选择 RelativeLayout,可以减少 View 的层级
将常用的布局组件抽取出来使用 \< include \>
标签
通过 \< ViewStub \>
标签来加载不常用的布局
使用 \< Merge \>
标签来减少布局的嵌套层次
网络优化: 常见的网络优化方案如下
尽量减少网络请求,能够合并的就尽量合并
避免 DNS 解析,根据域名查询可能会耗费上百毫秒的时间,也可能存在DNS劫持的风险。可以根据业务需求采用增加动态更新 IP 的方式,或者在 IP 方式访问失败时切换到域名访问方式。
大量数据的加载采用分页的方式
网络数据传输采用 GZIP 压缩
加入网络数据的缓存,避免频繁请求网络
上传图片时,在必要的时候压缩图片
安装包优化: 安装包优化的核心就是减少 apk 的体积,常见的方案如下
使用混淆,可以在一定程度上减少 apk 体积,但实际效果微乎其微
减少应用中不必要的资源文件,比如图片,在不影响 APP 效果的情况下尽量压缩图片,有一定的效果
在使用了 SO 库的时候优先保留 v7 版本的 SO 库,删掉其他版本的SO库。原因是在 2018 年,v7 版本的 SO 库可以满足市面上绝大多数的要求,可能八九年前的手机满足不了,但我们也没必要去适配老掉牙的手机。实际开发中减少 apk 体积的效果是十分显著的,如果你使用了很多 SO 库,比方说一个版本的SO库一共 10M,那么只保留 v7 版本,删掉 armeabi 和 v8 版本的 SO 库,一共可以减少 20M 的体积。
Android的内存优化在我看来分为两点:避免内存泄漏、扩大内存,其实就是开源节流。
其实内存泄漏的本质就是较长生命周期的对象引用了较短生命周期的对象。
handler.removeCallbacksAndMessages
来取消延时事件。disposable.dispose()
来取消操作。为什么要扩大我们的内存呢?有时候我们实际开发中不可避免的要使用很多第三方商业的 SDK,这些 SDK 其实有好有坏,大厂的 SDK 可能内存泄漏会少一些,但一些小厂的 SDK 质量也就不太靠谱一些。那应对这种我们无法改变的情况,最好的办法就是扩大内存。
扩大内存通常有两种方法:一个是在清单文件中的 Application 下添加largeHeap="true"
这个属性,另一个就是同一个应用开启多个进程来扩大一个应用的总内存空间。第二种方法其实就很常见了,比方说我使用过个推的 S DK,个推的 Service 其实就是处在另外一个单独的进程中。
Android 中的内存优化总的来说就是开源和节流,开源就是扩大内存,节流就是避免内存泄漏。
在Linux中,为了避免一个进程对其他进程的干扰,进程之间是相互独立的。在一个进程中其实还分为用户空间和内核空间。这里的隔离分为两个部分,进程间的隔离和进程内的隔离。
既然进程间存在隔离,那其实也是存在着交互。进程间通信就是 IPC,用户空间和内核空间的通信就是系统调用。
Linux 为了保证独立性和安全性,进程之间不能直接相互访问,Android 是基于 Linux 的,所以也是需要解决进程间通信的问题。
其实 Linux 进程间通信有很多方式,比如管道、socket 等等。为什么 Android 进程间通信采用了Binder而不是 Linux
已有的方式,主要是有这么两点考虑:性能和安全
性能。 在移动设备上对性能要求是比较严苛的。Linux传统的进程间通信比如管道、socket等等进程间通信是需要复制两次数据,而Binder则只需要一次。所以Binder在性能上是优于传统进程通信的。
安全。 传统的 Linux 进程通信是不包含通信双方的身份验证的,这样会导致一些安全性问题。而Binder机制自带身份验证,从而有效的提高了安全性。
Binder 是基于 CS 架构的,有四个主要组成部分。
Binder 机制主要的流程是这样的:
LruCache 的核心原理就是对 LinkedHashMap 的有效利用,它的内部存在一个 LinkedHashMap 成员变量。值得我们关注的有四个方法:构造方法、get、put、trimToSize。
构造方法: 在 LruCache 的构造方法中做了两件事,设置了 maxSize、创建了一个 LinkedHashMap。这里值得注意的是 LruCache 将 LinkedHashMap的accessOrder 设置为了 true,accessOrder 就是遍历这个LinkedHashMap 的输出顺序。true 代表按照访问顺序输出,false代表按添加顺序输出,因为通常都是按照添加顺序输出,所以 accessOrder 这个属性默认是 false,但我们的 LruCache 需要按访问顺序输出,所以显式的将 accessOrder 设置为 true。
get方法: 本质上是调用 LinkedHashMap 的 get 方法,由于我们将 accessOrder 设置为了 true,所以每调用一次get方法,就会将我们访问的当前元素放置到这个LinkedHashMap的尾部。
put方法: 本质上也是调用了 LinkedHashMap 的 put 方法,由于 LinkedHashMap 的特性,每调用一次 put 方法,也会将新加入的元素放置到 LinkedHashMap 的尾部。添加之后会调用 trimToSize 方法来保证添加后的内存不超过 maxSize。
trimToSize方法: trimToSize 方法的内部其实是开启了一个 while(true)的死循环,不断的从 LinkedHashMap 的首部删除元素,直到删除之后的内存小于 maxSize 之后使用 break 跳出循环。
其实到这里我们可以总结一下,为什么这个算法叫 最近最少使用 算法呢?原理很简单,我们的每次 put 或者get都可以看做一次访问,由于 LinkedHashMap 的特性,会将每次访问到的元素放置到尾部。当我们的内存达到阈值后,会触发 trimToSize 方法来删除 LinkedHashMap 首部的元素,直到当前内存小于 maxSize。为什么删除首部的元素,原因很明显:我们最近经常访问的元素都会放置到尾部,那首部的元素肯定就是 最近最少使用 的元素了,因此当内存不足时应当优先删除这些元素。
设计一个图片加载框架,肯定要用到图片加载的三级缓存的思想。三级缓存分为内存缓存、本地缓存和网络缓存。
内存缓存:将Bitmap缓存到内存中,运行速度快,但是内存容量小。 本地缓存:将图片缓存到文件中,速度较慢,但容量较大。 网络缓存:从网络获取图片,速度受网络影响。
如果我们设计一个图片加载框架,流程一定是这样的:
上面是一些基本的概念,如果是具体的代码实现的话,大概需要这么几个方面的文件:
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
DiskLruCache。在 MemeryAndDiskCache 这个类中我们定义两个方法,一个是 getBitmap,另一个是 putBitmap,对应着图片的获取和缓存,内部的逻辑也很简单。getBitmap中按内存、本地的优先级去取 BItmap,putBitmap 中先缓存内存,之后缓存到本地。
displayImage(url,imageView)
,另一个是从网络获取图片downloadImage(url,imageView)
。在展示图片方法中首先要通过 ImageView.setTag(url)
,将 url 和 imageView 进行绑定,这是为了避免在列表中加载网络图片时会由于ImageView的复用导致的图片错位的 bug。之后会从 MemeryAndDiskCache 中获取缓存,如果存在,直接加载;如果不存在,则调用从网络获取图片这个方法。从网络获取图片方法很多,这里我一般都会使用 OkHttp+Retrofit
。当从网络中获取到图片之后,首先判断一下imageView.getTag()
与图片的 url 是否一致,如果一致则加载图片,如果不一致则不加载图片,通过这样的方式避免了列表中异步加载图片的错位。同时在获取到图片之后会通过 MemeryAndDiskCache 来缓存图片。在我们的手指触摸到屏幕的时候,事件其实是通过 Activity -> ViewGroup -> View
这样的流程到达最后响应我们触摸事件的 View。
说到事件分发,必不可少的是这几个方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent。
接下来就按照 Activity -> ViewGroup -> View
的流程来大致说一下事件分发机制。
我们的手指触摸到屏幕的时候,会触发一个 Action_Down 类型的事件,当前页面的 Activity 会首先做出响应,也就是说会走到 Activity 的 dispatchTouchEvent()
方法内。在这个方法内部简单来说是这么一个逻辑:
getWindow.superDispatchTouchEvent()。
onTouchEvent()。
这个逻辑很好理解,getWindow().superDispatchTouchEvent()
如果返回 true 代表当前事件已经被处理,无需调用自己的 onTouchEvent;否则代表事件并没有被处理,需要 Activity 自己处理,也就是调用自己的 onTouchEvent。
getWindow()
方法返回了一个 Window 类型的对象,这个我们都知道,在 Android 中,PhoneWindow 是Window 的唯一实现类。所以这句本质上是调用了``PhoneWindow中的superDispatchTouchEvent()。`
而在 PhoneWindow 的这个方法中实际调用了mDecor.superDispatchTouchEvent(event)
。这个 mDecor 就是 DecorView,它是 FrameLayout 的一个子类,在 DecorView 中的 superDispatchTouchEvent()
中调用的是 super.dispatchTouchEvent()
。到这里就很明显了,DecorView 是一个 FrameLayout 的子类,FrameLayout 是一个 ViewGroup 的子类,本质上调用的还是 ViewGroup的dispatchTouchEvent()
。
分析到这里,我们的事件已经从 Activity 传递到了 ViewGroup,接下来我们来分析下 ViewGroup 中的这几个事件处理方法。
在 ViewGroup 中的 dispatchTouchEvent()
中的逻辑大致如下:
onInterceptTouchEvent()
判断当前 ViewGroup 是否拦截事件,默认的 ViewGroup 都是不拦截的;onTouchEvent()
;child.dispatchTouchEvent()
的返回值判断。如果返回 true,则 return true;否则 return 自己的 onTouchEvent()
,在这里实现了未处理事件的向上传递。通常情况下 ViewGroup 的 onInterceptTouchEvent()
都返回 false,也就是不拦截。这里需要注意的是事件序列,比如 Down 事件、Move 事件…Up事件,从 Down 到 Up 是一个完整的事件序列,对应着手指从按下到抬起这一系列的事件,如果 ViewGroup 拦截了 Down 事件,那么后续事件都会交给这个 ViewGroup的onTouchEvent。如果 ViewGroup 拦截的不是 Down 事件,那么会给之前处理这个 Down 事件的 View 发送一个 Action_Cancel 类型的事件,通知子 View 这个后续的事件序列已经被 ViewGroup 接管了,子 View 恢复之前的状态即可。
这里举一个常见的例子:在一个 Recyclerview 钟有很多的 Button,我们首先按下了一个 button,然后滑动一段距离再松开,这时候 Recyclerview 会跟着滑动,并不会触发这个 button 的点击事件。这个例子中,当我们按下 button 时,这个 button 接收到了 Action_Down 事件,正常情况下后续的事件序列应该由这个 button处理。但我们滑动了一段距离,这时 Recyclerview 察觉到这是一个滑动操作,拦截了这个事件序列,走了自身的 onTouchEvent()
方法,反映在屏幕上就是列表的滑动。而这时 button 仍然处于按下的状态,所以在拦截的时候需要发送一个 Action_Cancel 来通知 button 恢复之前状态。
事件分发最终会走到 View 的 dispatchTouchEvent()
中。在 View 的 dispatchTouchEvent()
中没有 onInterceptTouchEvent()
,这也很容易理解,View 不是 ViewGroup,不会包含其他子 View,所以也不存在拦截不拦截这一说。忽略一些细节,View 的 dispatchTouchEvent()
中直接 return 了自己的 onTouchEvent()
。如果 onTouchEvent()
返回 true 代表事件被处理,否则未处理的事件会向上传递,直到有 View 处理了事件或者一直没有处理,最终到达了 Activity 的 onTouchEvent()
终止。
这里经常有人问 onTouch 和 onTouchEvent 的区别。首先,这两个方法都在 View 的 dispatchTouchEvent()
中,是这么一个逻辑:
onTouchEvent()
方法。onTouchEvent()
方法中。所以 onTouch 的顺序是在 onTouchEvent 之前的。视图绘制的起点在 ViewRootImpl 类的 performTraversals()
方法,在这个方法内其实是按照顺序依次调用了 mView.measure()、mView.layout()、mView.draw()
View的绘制流程分为3步:测量、布局、绘制,分别对应3个方法 measure、layout、draw。
测量阶段。 measure 方法会被父 View 调用,在measure 方法中做一些优化和准备工作后会调用 onMeasure 方法进行实际的自我测量。onMeasure方法在View和ViewGroup做的事情是不一样的:
View。 View 中的 onMeasure 方法会计算自己的尺寸并通过 setMeasureDimension 保存。
ViewGroup。 ViewGroup 中的 onMeasure 方法会调用所有子 iew的measure 方法进行自我测量并保存。然后通过子View的尺寸和位置计算出自己的尺寸并保存。
布局阶段。 layout 方法会被父View调用,layout 方法会保存父 View 传进来的尺寸和位置,并调用 onLayout 进行实际的内部布局。onLayout 在 View 和 ViewGroup 中做的事情也是不一样的:
View。 因为 View 是没有子 View 的,所以View的onLayout里面什么都不做。
ViewGroup。 ViewGroup 中的 onLayout 方法会调用所有子 View 的 layout 方法,把尺寸和位置传给他们,让他们完成自我的内部布局。
绘制阶段。 draw 方法会做一些调度工作,然后会调用 onDraw 方法进行 View 的自我绘制。draw 方法的调度流程大致是这样的:
**绘制背景。**对应 drawBackground(Canvas)
方法。
**绘制主体。**对应 onDraw(Canvas)
方法。
绘制子View。 对应 dispatchDraw(Canvas)
方法。
绘制滑动相关和前景。 对应 onDrawForeground(Canvas)
。
在 Android 中,Android 与js 的交互分为两个方面:Android 调用 js 里的方法、js 调用 Android 中的方法。
Android调js。 Android 调 js 有两种方法:
WebView.loadUrl(“javascript:js中的方法名”)。 这种方法的优点是很简洁,缺点是没有返回值,如果需要拿到js方法的返回值则需要js调用Android中的方法来拿到这个返回值。
WebView.evaluateJavaScript(“javascript:js中的方法名”,ValueCallback)。 这种方法比 loadUrl 好的是可以通过 ValueCallback 这个回调拿到 js方法的返回值。缺点是这个方法 Android4.4 才有,兼容性较差。不过放在 2018 年来说,市面上绝大多数 App 都要求最低版本是 4.4 了,所以我认为这个兼容性问题不大。
js 调 Android。 js 调 Android有三种方法:
WebView.addJavascriptInterface()。
这是官方解决 js 调用 Android 方法的方案,需要注意的是要在供 js 调用的 Android 方法上加上 @JavascriptInterface 注解,以避免安全漏洞。这种方案的缺点是 Android4.2 以前会有安全漏洞,不过在 4.2 以后已经修复了。同样,在 2018 年来说,兼容性问题不大。
重写 WebViewClient的shouldOverrideUrlLoading()
方法来拦截url, 拿到 url 后进行解析,如果符合双方的规定,即可调用 Android 方法。优点是避免了 Android4.2 以前的安全漏洞,缺点也很明显,无法直接拿到调用 Android 方法的返回值,只能通过 Android 调用 js 方法来获取返回值。
重写 WebChromClient 的 onJsPrompt()
方法,同前一个方式一样,拿到 url 之后先进行解析,如果符合双方规定,即可调用Android方法。最后如果需要返回值,通过 result.confirm("Android方法返回值")
即可将 Android 的返回值返回给 js。方法的优点是没有漏洞,也没有兼容性限制,同时还可以方便的获取 Android 方法的返回值。其实这里需要注意的是在 WebChromeClient 中除 了 onJsPrompt 之外还有 onJsAlert 和 onJsConfirm 方法。那么为什么不选择另两个方法呢?原因在于 onJsAlert 是没有返回值的,而 onJsConfirm 只有 true 和 false 两个返回值,同时在前端开发中 prompt 方法基本不会被调用,所以才会采用 onJsPrompt。
SparseArray,通常来讲是 Android 中用来替代 HashMap 的一个数据结构。 准确来讲,是用来替换key为 Integer 类型,value为Object 类型的HashMap。需要注意的是 SparseArray 仅仅实现了 Cloneable 接口,所以不能用Map来声明。 从内部结构来讲,SparseArray 内部由两个数组组成,一个是 int[]
类型的 mKeys,用来存放所有的键;另一个是 Object[]
类型的 mValues,用来存放所有的值。 最常见的是拿 SparseArray 跟HashMap 来做对比,由于 SparseArray 内部组成是两个数组,所以占用内存比 HashMap 要小。我们都知道,增删改查等操作都首先需要找到相应的键值对,而 SparseArray 内部是通过二分查找来寻址的,效率很明显要低于 HashMap 的常数级别的时间复杂度。提到二分查找,这里还需要提一下的是二分查找的前提是数组已经是排好序的,没错,SparseArray 中就是按照key进行升序排列的。 综合起来来说,SparseArray 所占空间优于 HashMap,而效率低于 HashMap,是典型的时间换空间,适合较小容量的存储。 从源码角度来说,我认为需要注意的是 SparseArray的remove()、put()
和 gc()
方法。
remove()
。 SparseArray 的 remove()
方法并不是直接删除之后再压缩数组,而是将要删除的 value 设置为 DELETE 这个 SparseArray 的静态属性,这个 DELETE 其实就是一个 Object 对象,同时会将 SparseArray 中的 mGarbage 这个属性设置为 true,这个属性是便于在合适的时候调用自身的 gc()
方法压缩数组来避免浪费空间。这样可以提高效率,如果将来要添加的key等于删除的key,那么会将要添加的 value 覆盖 DELETE。gc()。
SparseArray 中的 gc()
方法跟 JVM 的 GC 其实完全没有任何关系。``gc()` 方法的内部实际上就是一个for循环,将 value 不为 DELETE 的键值对往前移动覆盖value 为DELETE的键值对来实现数组的压缩,同时将 mGarbage 置为 false,避免内存的浪费。put()。
put 方法是这么一个逻辑,如果通过二分查找 在 mKeys 数组中找到了 key,那么直接覆盖 value 即可。如果没有找到,会拿到与数组中与要添加的 key 最接近的 key 索引,如果这个索引对应的 value 为 DELETE,则直接把新的 value 覆盖 DELET 即可,在这里可以避免数组元素的移动,从而提高了效率。如果 value 不为 DELETE,会判断 mGarbage,如果为 true,则会调用 gc()
方法压缩数组,之后会找到合适的索引,将索引之后的键值对后移,插入新的键值对,这个过程中可能会触发数组的扩容。我们知道内存中的 Bitmap 大小的计算公式是:长所占像素 * 宽所占像素 * 每个像素所占内存。想避免 OOM 有两种方法:等比例缩小长宽、减少每个像素所占的内存。
decodeFile()、decodeStream()、decodeByteArray()、decodeResource()
。这些方法中都有一个 Options 类型的参数,这个 Options 是 BitmapFactory 的内部类,存储着 BItmap 的一些信息。Options 中有一个属性:inSampleSize。我们通过修改 inSampleSize 可以缩小图片的长宽,从而减少 BItma p 所占内存。需要注意的是这个 inSampleSize 大小需要是 2 的幂次方,如果小于 1,代码会强制让inSampleSize为1。ARGB_8888
,代表每个像素所占尺寸。我们可以通过将之修改为 RGB_565
或者 ARGB_4444
来减少一半内存。加载高清大图,比如清明上河图,首先屏幕是显示不下的,而且考虑到内存情况,也不可能一次性全部加载到内存。这时候就需要局部加载了,Android中有一个负责局部加载的类:BitmapRegionDecoder。使用方法很简单,通过BitmapRegionDecoder.newInstance()创建对象,之后调用decodeRegion(Rect rect, BitmapFactory.Options options)即可。第一个参数rect是要显示的区域,第二个参数是BitmapFactory中的内部类Options。
由于源码分析篇幅太大,所以这里之贴出我的源码分析的链接(掘金)。
OkHttp源码分析
Retrofit源码分析1 Retrofit源码分析2 Retrofit源码分析3