join 方法通常是保证线程间顺序调度的一个方法,它是 Thread 类中的方法。比方说在线程 A 中执行线程 B.join()
,这时线程 A 会进入等待状态,直到线程 B 执行完毕之后才会唤醒,继续执行A线程中的后续方法。
join 方法可以传时间参数,也可以不传参数,不传参数实际上调用的是 join(0)
。它的原理其实是使用了 wait 方法,join 的原理如下:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException(“timeout value is negative”);
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
生产者消费者模式要保证的是当缓冲区满的时候生产者不再生产对象,当缓冲区空时,消费者不再消费对象。实现机制就是当缓冲区满时让生产者处于等待状态,当缓冲区为空时让消费者处于等待状态。当生产者生产了一个对象后会唤醒消费者,当消费者消费一个对象后会唤醒生产者。
三种种实现方式:wait 和 notify、await 和 signal、BlockingQueue。
//wait和notify
import java.util.LinkedList;
public class StorageWithWaitAndNotify {
private final int MAX_SIZE = 10;
private LinkedList list = new LinkedList();
public void produce() {
synchronized (list) {
while (list.size() == MAX_SIZE) {
System.out.println(“仓库已满:生产暂停”);
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
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 架构的,有四个主要组成部分。
间。这里的隔离分为两个部分,进程间的隔离和进程内的隔离。
既然进程间存在隔离,那其实也是存在着交互。进程间通信就是 IPC,用户空间和内核空间的通信就是系统调用。
Linux 为了保证独立性和安全性,进程之间不能直接相互访问,Android 是基于 Linux 的,所以也是需要解决进程间通信的问题。
其实 Linux 进程间通信有很多方式,比如管道、socket 等等。为什么 Android 进程间通信采用了Binder而不是 Linux
已有的方式,主要是有这么两点考虑:性能和安全
性能。 在移动设备上对性能要求是比较严苛的。Linux传统的进程间通信比如管道、socket等等进程间通信是需要复制两次数据,而Binder则只需要一次。所以Binder在性能上是优于传统进程通信的。
安全。 传统的 Linux 进程通信是不包含通信双方的身份验证的,这样会导致一些安全性问题。而Binder机制自带身份验证,从而有效的提高了安全性。
Binder 是基于 CS 架构的,有四个主要组成部分。