/# 参考链接
- 应用存储空间
- shared preferences
- 文件存储
- 数据库储存
- ContentProvider
应用存储空间
Android使用的文件系统类似于其他平台上基于磁盘的文件系统,主要有以下几种保存应用数据的方式:
- 应用专属存储空间: 存储仅供应用使用的文件,可以存储到内部存储卷中的专属目录或者外部存储空间的其他专属目录。使用内部存储空间中的目录保存其他应用不应访问的敏感信息。
- 共享存储: 存储应用打算与其他应用共享的文件,包括媒体、文档和其他文件。
- SharedPreference: 以键值对的形式存储私有原始数据。
- 数据库: 使用持久性库将结构化数据存储在专用数据库中。 | | 内容类型 | 访问方法 | 所需权限 | 其他应用可访问? | 卸载时是否删除文件?| |-------|----------|--------|----------| -------------- | -------------------| |应用专属文件|仅供应用使用的文件 | 内部存储空间:getFilesDir()或getCacheDir() 外部存储空间:getExternalFileDir()或者getExternalCacheFileDir() | 内部存储空间不需要权限,如果应用在Android4.4及其以上运行,外部存储空间不需要权限 | 内部存储空间不能访问,外部存储空间可以访问 | 是 | | 媒体 | 可共享的媒体文件(图片、音频、视频) | MediaStore API | 在Android10(API 29)或者更高的版本访问其他应用的文件需要READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE权限(开启分区后,访问自己应用不需要权限),在Android9(API 28)或者一下访问所有文件均需要相关权限 | 是,但是其他应用需要READ_EXTERNAL_STIRAGE权限 | 否 | | 应用偏好设置 | 键值对 | SharedPreference | 无 | 否 | 是 | | 数据库 | 结构化数据 | ROOM持久库 | 无 | 否 | 是 |
- Android存储路径
分区存储 为了更好的管理文件并减少混乱,以Android10(API 29)及其之后版本为目标平台的应用在默认情况下被赋予了对外部存储空间的分区访问权限(分区存储),此类应用只能访问外部存储空间上的应用专属目录,以及本应用所创建的特定类型的媒体文件.
SharedPreferences
- 常见问题:
- SharedPreferences是如何初始化的,会阻塞线程吗,每次获取SP对象真的会很慢吗?
- commit和apply方法有什么区别,commit操作一定是在主线程吗?
- SP在使用时有什么需要注意的,如何优化呢?
- 基本使用:
SharedPreferences sp = context.getSharedPreferences("fileName",Context.MODE_PRIVATE);
SharedPreferences.Editor editor =- sp.edit();
editor.putString("key","value");
editor.commit(); // editor.apply();
SharedPreferences是一个接口,只能通过context.getSharedPreferences("fileName",MODE_PRIVATE)创建,fileName为要存储的xml文件名,Context.MODE_PRIVATE表示该sp数据只能被该应用读写,MODE_WORLD_READABLE已经被废弃,因为sp在多进程下表现不稳定。
- 适用范围: 只能保存应用中少量的数据,数据格式简单。
- 核心原理: 保存基于XML文件存储的key-value键值对数据,文件路径:/data/data/<package_name>/shared_prefs目录下。 SharedPreferences本身只能获取数据,存储修改是由editor实现,它们两个都是接口,真正实现是在SharedPreferencesImpl和EditorImpl。
- 源码总结
- ContextImpl中包含一个ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache静态变量,key:包名,value:ArrayMap<File, SharedPreferencesImpl>(File:sp存储的xml文件,SharedPreferencesImpl:sp实例),为什么会以包名为key呢(因为一个进程中只会包含一个ContextImpl对象,所以同一个进程内的所有SharedPreferences实例都会保存在这个静态列表中,而一个进程中又可以有多个Android应用,所以用包名来区分<每个Android应用都会有一个唯一的Linux用户ID,但是如果将两个应用共享同一Linux用户ID,它们能够互相访问彼此的文件(data目录等),为了节省系统资源,可以安排具有相同ID的应用在同一个Linux进程中运行,共享同一个VM,而这时两个应用还要必须使用相同的证书签署>如何理解Android的多进程)。
- ContextImpl类中并没有定义将SharedPreferences对象移除的方法,所以一旦加入到内存中,就会存在直至进程销毁,sp对象只有在第一个初始化时,读取数据磁盘到内存,之后都是在内存中获取。
- SharedPreferencesImpl实例化的时候会启动子线程来读取磁盘上文件数据,所以获取SP对象并不会阻塞主线程,但是如果在读取磁盘完成前调用get("XXX")获取使用editor存数据会阻塞主线程,因为往SP文件中读取和写入都必须等待SP文件从磁盘加载完成。
- 在调用EditorImpl.put(XXX)时,内部通过HashMap存储数据,提交的时候分为commit和apply,它们都会先将数据写入内存中,然后再写入磁盘,apply操作是异步的,commit操作可能为同步也可能为异步,取决于commit时当前是否还有写磁盘的任务,有的话则将写任务添加到QueenWork队列中。对于apply和commit的同步,是通过CountDownLatch来实现的,它是一个同步工具类,它允许一个线程或者多个线程一直等待,知道其他线程执行完之后再执行。
- SP的读写是线程安全的,因为对内部mMap的读写操作用的是同一把锁。
- 注意事项
- 不要存大的key/value,避免ANR
- 不要高频的使用apply和commit,尽可能的批量提交
- 不使用MODE_MULTI_PROCESS
- 高频的写操作key与高频的读操作key可以适当的分文件存储,减少同步锁竞争
- 不要连续多次edit,每次edit就会打开一次文件,应获取一次edit,再执行多次put,减少内存消耗
- ANR容易发生的地方
- sp.getXXX,首先会调用awaitLoadedLocked等待首次sp文件创建和读取操作完成
- apply,异步操作可能会在 Service Activity 等生命周期期间 mcr.writtenToDiskLatch.await() 等待过久
- ContextImpl.getSharedPreferences,主线程直接调用的话,如果 sp 文件很大处理时间也就会变久
线程相关
参考链接:Java 线程生命周期 线程的创建方式有两种,继承Thread类和实现Runnable接口,执行任务后无法获取执行结果,在JDK1.5之后加入了Callable和Future具有返回结果的接口。
线程池相关
- 什么时候使用线程池
- 单个任务执行时间较短
- 需要处理的任务数量很大
- 线程池的好处:
- 降低资源消耗,通过重复利用已创建的线程,降低创建和销毁线程所造成的消耗
- 提高响应速度,当任务到达的时候不需要等待线程的创建就能立即执行
- 提高线程的可管理性,使用线程统一分配、调优和监控
- 线程池的状态:
- RUNNING:运行状态,能够接受新任务,并且能够处理阻塞队列中的任务
- SHUTDOWN:关闭状态,不再接受新任务,但是能够继续将阻塞队列中的任务取出执行。调用shutdown方法使线程池进入该状态。
- STOP:停止状态,不能接受新任务,也不处理阻塞队列中的任务,会中断正在处理任务的线程。调用shutdownNow方法使线程池进入该状态。
- TIDYING:如果所有的任务都停止了,workerCount(有效线程数)为0,线程池会调用terminated方法进入TERMINATED状态。
- TERMINATED:在terminated方法后进入该状态。
- ThreadPoolExecutor构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize:核心线程数量
- maximumPoolSize:最大线程数量
- keepAliveTime:线程池维护线程所允许的空闲时间,当线程池中线程数量大于corePoolSize时,如果此时任务队列中没有任务可执行,核心线程外的线程不会立即销毁,而是会等待,直到等待时间超过keepAliveTime,默认情况下核心线程是不会被销毁的,但是如果将allowCoreThreadTimeOut设置为true时,核心线程也会通过该时间来判断是否销毁。
- unit:线程存活时间单位
- workQueue:任务阻塞队列
- SynchronousQueue:不会保存提交的任务,而是直接创建一个线程来执行新来的任务
- LinkedBlockingQueue:基于链表,默认队列大小Integer.MAX_VALUE
- ArrayBlockingQueue:基于数组,创建时必须指定大小
- threadFactory:用于创建线程,默认使用Executors.defaultThreadFactory()
- handler:线程池饱和策略
- AbortPolicy:直接抛出异常,这是默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务
- 自定义,实现RejectedExecutionHandler接口
线程池创建成功后是没有线程的,需要提交任务后才创建线程,另外可以通过prestartCoreThread():初始化一个核心线程,prestartAllCoreThreads():初始化所有核心线程
- execute()方法执行过程:
- 如果workerCount < corePoolSize,创建并启动一个核心线程来执行新提交的任务,如果addworker()启动线程失败(方法中会对线程池状态进行判断),则判断线程池是否处于运行状态并且将任务入队,如果为真:再次判断线程池如果不是运行状态,那么将该任务移除队列(前边已经加入队列了)并且将抛出该任务执行饱和策略,如果是运行状态,判断当前有效线程是否为0 ,为0:开启额外线程(这里开启线程addworker(null,false),不将任务传入进去是因为任务已经入队了,线程里直接取队列的任务),不为0:不做任何处理,因为核心线程执行完之后也会去取队列中的任务。如果为假: 将开启额外线程处理该任务,如果处理失败,抛出该任务执行饱和策略。
- 简单来说:执行execute()方法时如果一直处于RUNNING时:
- 如果workerCount < corePoolSize,创建一个核心线程执行任务
- 如果workerCount >= corePoolSize且队列未满,那么将任务加入队列
- 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且任务队列已满,则创建一个线程执行任务
- 如果workerCount >= maximumPoolSize,执行饱和策略处理该任务
- addWorker(Runnable firstTask, boolean core)方法
addWorker方法的主要工作是在线程池中创建一个新的线程并执行,firstTask参数用于指定新增的线程执行的第一个任务(如果为null,则启动一个线程),core参数为true表示新增线程前需要判断当前线程数是否小于corePoolSize,参数为false表示新增线程前需要判断当前线程数是否小于maximumPoolSize
- Worker类
线程池中的每个线程都被封装成一个Worker对象,ThreadPool中维护的其实就是一组Worker对象 Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。
- Worker类中的runWorker()方法
- while循环不断的通过getTask方法获取任务
- 如果线程正在停止,那么要保证当前线程是中断状态,否则要保证线程不是中断状态
- 如果task为null则跳出循环执行processWorkerExit()方法
- runWorker方法执行完毕,销毁线程
- getTask()方法
内部通过条件判断取出任务,并且有超时和有效线程数的控制
- processWorkerExit方法
做worker线程退出后线程池的变量维护,比如完成的任务数和根据线程池状态判断是否结束线程池
- 思考:在runWorker方法中,执行任务时对Worker对象w进行了lock操作,为什么要在执行任务的时候对每个工作线程都加锁呢?
- 在getTask方法中,如果这时线程池的状态是SHUTDOWN并且workQueue为空,那么就应该返回null来结束这个工作线程,而使线程池进入SHUTDOWN状态需要调用shutdown方法;
- shutdown方法会调用interruptIdleWorkers来中断空闲的线程,interruptIdleWorkers持有mainLock,会遍历workers来逐个判断工作线程是否空闲。但getTask方法中没有mainLock;
- 在getTask中,如果判断当前线程池状态是RUNNING,并且阻塞队列为空,那么会调用workQueue.take()进行阻塞;
- 如果在判断当前线程池状态是RUNNING后,这时调用了shutdown方法把状态改为了SHUTDOWN,这时如果不进行中断,那么当前的工作线程在调用了workQueue.take()后会一直阻塞而不会被销毁,因为在SHUTDOWN状态下不允许再有新的任务添加到workQueue中,这样一来线程池永远都关闭不了了;
- 由上可知,shutdown方法与getTask方法(从队列中获取任务时)存在竞态条件;
- 解决这一问题就需要用到线程的中断,也就是为什么要用interruptIdleWorkers方法。在调用workQueue.take()时,如果发现当前线程在执行之前或者执行期间是中断状态,则会抛出InterruptedException,解除阻塞的状态;
- 但是要中断工作线程,还要判断工作线程是否是空闲的,如果工作线程正在处理任务,就不应该发生中断;
- 所以Worker继承自AQS,在工作线程处理任务时会进行lock,interruptIdleWorkers在进行中断时会使用tryLock来判断该工作线程是否正在处理任务,如果tryLock返回true,说明该工作线程当前未执行任务,这时才可以被中断。
java中的锁
参考链接: 不可不说的Java“锁”事 锁的类型和状态
- 线程要不要锁住同步资源?
- 悲观锁:要
- 乐观锁:不要
- 锁住同步资源失败,线程要不要阻塞?
- 阻塞
- 自旋锁、适应性自旋锁:不阻塞
- 多个线程竞争同步资源的流程细节区别?
- 无锁:不锁住资源,多个线程只有一个线程能成功修改资源,其他线程重试
- 偏向锁:同一个线程获取同步资源时,自动获取锁
- 轻量级锁:多个线程竞争同步资源时,没有获取到资源的线程自旋等待锁释放
- 重量级锁:多个线程竞争同步资源时,没有获取到资源的线程阻塞等待唤醒
- 多个线程竞争锁时要不要排队?
- 公平锁:要排队
- 非公平锁:先尝试插队,失败后再排队
- 一个线程中的多个流程能不能获取同一把锁?
- 可重入锁:能
- 非可重入锁:不能
- 多个线程能不能共享一把锁?
- 共享锁:能
- 排他锁:不能
1. 乐观锁 VS 悲观锁
乐观锁:适合读操作多的场景,不加锁的特点是使其读操作性能大幅度提升
悲观锁:适合写操作多的场景,保证写数据正确
乐观锁和悲观锁是一种广义上的概念。对于同一个数据的并发操作,悲观锁 会认为自己在使用数据时,会有其他线程来修改数据,所以在获取数据的时候会加锁,synchronized关键字和Lock的实现类都是悲观锁。乐观锁 在使用数据时不会添加锁,只是在更新数据的时候去判断之前有没有线程对该数据进行了修改,如果没有被修改,当前线程将自己修改的数据成功写入,如果被修改,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是使用无锁编程来实现,最常用的是CAS算法。
2. 自旋锁 VS 适应性自旋锁
前提:阻塞或唤醒线程需要操作系统切换CPU状态来完成,这种切换需要消耗处理器的时间,如果同步代码块中的内容过于简单,状态转换所消耗的时间就有可能比用户代码执行的时间还要长。如果物理机器有多个处理器,可以让两个或以上的线程同时并行执行,我们就可以让后面请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。 而为了让当前线程“等待”,我们就需要让当前线程进行自旋,如果在自旋完成后能够拿到锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。
自旋锁本身是有缺点的,他不能代替阻塞,虽然避免了线程之间切换的消耗,但是会占用处理器时间,如果锁被占用的时间很长,那么自旋的线程就白白的浪费处理器资源,所以,自旋等待的时间必须一定要有一定的限度,如果自旋超过了限定次数还没获取到锁,则应当挂起线程。由此便有了适应性自旋锁,自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
这四种锁是指锁的状态,专门针对于synchronized
- 额外知识:
- Java对象头:锁都是存在Java对象头里边的,对象头主要包含了两部分数据:Mark Word(标记字段)和 Klass Pointer(类型指针)
- Mark Word:默认存储对象的HashCode,分代年龄和锁的标志位
- Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- Monitor:
- Monitor可以理解为一个同步工具或者同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,成为内部锁或者Monitor锁。
- Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
- 无锁: 无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
- 偏向锁: 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
- 轻量级锁: 是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
- 重量级锁: 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
4. 可重入锁 VS 非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
JVM相关
参考链接:一篇文章学完 JVM 重点知识
Java内存区域与HotSpot虚拟机
- 运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的区域。这些区域有各自的用途以及创建和销毁时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖于线程的启动和结束而建立和销毁。
- 程序计数器 由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的位置,每条线程都需要一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
如果线程执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器则为空。此内存区域是唯一一个没有规定任何 OOM 的区域。
- 虚拟机栈 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接地址、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型,对象引用类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在 JVM 规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,则会抛出 OOM。
- 本地方法栈 本地方法栈和虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OOM。
- Java堆 Java 堆是虚拟机管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器基本都是采用分代手机算法,所以 Java 堆还可以细分为新生代和老年代。在细致一点有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(TLAB)。不过不论如何划分,都与存放内容无关,无论哪个区域,存放的都是对象实例,进一步划分的目的是为了更好的回收内存和更快的分配内存。
如果堆中没有内存完成实例分配,并且堆也无法再扩展时,就会抛出 OOM。
- 方法区 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java 虚拟机对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可以扩展外,还可以选择不实现垃圾回收。相对而言,垃圾回收在这个区域是比较少出现的。
运行时常量池:
运行时常量池也是方法区的一部分,常量池用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有在编译期才能生成,也就是并非预置入 Class 文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量池放入池中,这种特性被开发人员利用比较多的便是 String 类的 intern 方法。
既然运行时常量池也是方法区的一部分,自然会受到方法区内存的限制,当常量池无法申请到内存时则会抛出 OOM。
- 对象的创建 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务就等同于把一块确定大小的内存从 Java 堆中划分出来。假设 Java 堆内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那么所分配内存就仅仅是把指针向空闲内存区域移动,这种分配方法称为指针碰撞。如果 Java 堆并不是规整的,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找出一块足够大的空间划分给对象实例,并更新表上的记录,这种分配方式称为空闲列表。选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否具有压缩整理功能决定。
对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改了一个指针所指向的位置,在并发情况下也并不是线程安全的。可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用原来的指针来分配内存。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理 — 实际上虚拟机采用 CAS 配上失败重试的方法保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程都在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定。
Java内存模型
- 概述 Java虚拟机规范试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各个平台上都能达到访问一致的效果。
- 主内存和工作内存 Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节,此处的变量与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的,不会被共享,自然就不存在竞争问题。
Java 内存模型规定了所有的变量都是存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保留了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
- 原子性 原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。字节码指令 monitorenter 和 monitorexit 来隐式的使用这两种操作,这两个字节码指令就对应于 Java 中的 synchronized 关键字。
- 可见性 当一个变量修改了共享变量的值,其他线程能够立即得知这个修改,Java内存模型是通过在变量修改后将新值同步到主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。
- 缓存一致性 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
- 有序性 如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指 “线程内表现为串行的语义(As - if - Serial)”,后半句是指 “指令重排序” 现象和 “工作内存和主内存同步延迟” 现象。
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 关键字本身就包含了禁止指令重排序的语义,而 synchronized 则是由 “一个变量在同一个时刻只允许一条线程对其进行 lock 操作” 这条规则获得的,这条规则决定了持有同一个锁的同步块只能串行的进入。
JVM垃圾收集器和内存分配策略
- 判断对象的存活
-
- 引用计数法 给对象添加一个引用计数器,每当有个地方引用对象,计数器+1,当引用失效时,计数器-1,任何时刻当计数器为0时,该对象就是没有被使用的,缺点是无法判断两个对象相互引用的问题。
-
- 可达性分析 通过一系列“GC Root”的对象节点,从这些节点开始向下搜索,当一个对象没有与任何一个Root节点相连时,即不可达,证明该对象是没有被使用的,Root节点有:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
-
- 再谈引用
- 强引用:obj = new Object(),只要强引用还存在,永远不会被回收
- 软引用:用来描述有用但非必须的对象,在内存溢出之前,会将软引用的对象回收,如果内存还不够,则会抛出内存溢出,使用SoftReference类来实现
- 弱引用:对象只会生存到下次垃圾回收之前,使用WeakReference类来实现
- 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在 JDK1.2 之后,提供了 PhantomReference 类来实现虚引用。
- 垃圾收集算法
-
标记-清理法:首先标记出需要回收的对象,然后统一做回收处理。缺点:标记和回收效率都不高,标记清除后会存在大量地址不连续的内存碎片
-
复制算法:将内存分成大小相等的两块,每次只使用一块,回收时将存活的对象复制到另一块去,然后清空当前内存块,这种方式的代价是将内存缩小了一半。
现在虚拟机都用复制算法回收新生代,新生代的对象几乎都是朝生晚死的,所以不需要一比一的分配空间,而是将内存分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden和一块Survivor,当回收时将存活对象复制到另一块Survivor区,再清空Eden和使用过的Survivor区。
-
标记整理算法:和标记清除算法的标记过程一致,但是后续步骤是将可回收的对象向一端移动,然后清理掉边界以外的内存。
-
- 安全点:主动式中断的思想是当 GC 需要中断线程时,不直接对线程操作,仅仅是设置一个标志,各个线程执行时主动去轮训这个标志,发现中断标志时就自己中断挂机。轮训标志的地方和安全点是重合的。
- 内存分配策略 在堆上分配内存,对象主要分配在新生代的Eden区上,如果线程分配了缓冲区,将按线程优先在TLAB上分配。
新生代对象在经历多次新生代GC后还存活,会进入老年代,次数默认是15,另内存较大的对象直接分配在老年代。
volatile
volatile值保证了变量的可见性和有序性,并不保证原子性,原子性是通过monitorenter和monitorexit指令来实现的,例如synchronized,volatile 保证了有序性,禁止指令重排。 - 常见问题:为什么单例双重检查实现中,已经有了synchronized却还需要volatile? - synchronized只是保证了程序的有序性,没有禁止指令重排,有序性扩展:如果在本线程内观察,所有操作都是天然有序的,如果在另一个线程中观察另一个线程,所有操作都是无序的。as-if-serial:不管指令怎么重排序,单线程程序的执行结果都不会被改变。
volatile使用内存屏障来保证可见性和有序性的,内存屏障是一个CPU指令,该条指令的前后禁止指令重排序,并且会强制刷新cpu的缓存数据,保证数据的最新。
双亲委派机制
如果一个类加载器收到一个类的加载请求,会先向委托父类进行加载,一直到顶层加载器,如果顶层加载器无法加载,子类再会一层一层的往下判断是否能够加载。这样做的好处是避免用户写的类来替换java原生提供的类。
Handler
参考链接:Handler 消息机制 Android应用是通过消息来驱动运行的,在Android中一切皆为消息,包括触摸消息、视图绘制、显示和刷新等。
- Handler如果避免内存泄漏?
- 静态内部类 + 弱引用 + 及时移除未处理的消息
- Handler消息机制关联的类:Looper,MessageQueue,Message
- handler构造函数中做了哪些事情?
- 调用Looper.myLooper()方法初始化Looper对象,myLooper()中调用了threadLocal.get()方法来获取当前线程的Looper对象。
- 判断looper对象是否为空,为空则抛出错误,提示需调用Looper.prepare()方法,prepare()内部new了一个Looper对象并设置给threadLocal。
- 关联looper中的mQueue队列(handler提供消息入队和清空消息处理)
- 关联外部传入的callback Runnable对象
ThreadLocal是用于线程隔离的,它可以在不同的线程中互不干扰的存储数据,当某些数据是以线程为作用域并且不同线程具有不同的数据副本时可以采用ThreadLocal。
- 为什么主线程中是可以直接使用Handler? ActivityThread.main()方法中已经调用了Looper.prepareMainLooper()初始化。
- handler发送消息方式
- 通过handler.post(runnable),postDelayed(runnable,delayMillis)
- 通过handler.sendMessage(message),sendEmptyMessage(int what),sendMessageDelayed(Message msg, long delayMillis) 两种方式最后都会调用sendMessageAtTime(Message msg, long uptimeMillis)方法
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this; //tartget为handler对象,后边用于消息事件分发处理
if (mAsynchronous) {
msg.setAsynchronous(true);
}
//转到 MessageQueue 的 enqueueMessage 方法
return queue.enqueueMessage(msg, uptimeMillis);
}
- Android应用是基于Message对象的,会存在大量的Message对象吗? Message源码分析
/*package*/ Message next; //Message是以链表结构存储
private static Message sPool; //Message对象缓存池的头节点
private static int sPoolSize = 0; //当前缓存大小
private static final int MAX_POOL_SIZE = 50; //最大缓存数
/**
* 获取一个缓存的Message,当头节点不为空时,取出,为空则new一个
* */
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
/**
* 回收Message对象,在looper的死循环中,一个Message对象事件分发处理完之后,会调用recycle()来回收
**/
public void recycle() {
if (isInUse()) {
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
recycleUnchecked();
}
/**
* 具体的回收操作,清空所有标志位以及关联的对象(next在looper取出Message时就已经被置空),
* 判断当前缓存数是否大于默认的缓存数量,小于将其插入到链表头部,
* 如果大于,当前Message已经不关联任何对象了,所以等着被回收就行了
**/
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
- Looper源码分析:
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);
boolean slowDeliveryDetected = false;
//===== 死循环一直读取queue的消息
for (;;) {
Message msg = queue.next(); //==== 可能会阻塞,因为next中也有一个死循环操作
if (msg == null) {
// ====== 消息为空时,跳出循环
return;
}
//省略代码.....
try {
msg.target.dispatchMessage(msg); //==== handler分发Message事件并处理
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
//省略代码....
msg.recycleUnchecked(); //====== msg处理完之后,回收msg对象
}
}
- Handler分发消息具体是怎么分发的?
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
//通过 handler.postXxx 形式传入的 Runnable
handleCallback(msg);
} else {
if (mCallback != null) {
//以 Handler(Handler.Callback) 写法
if (mCallback.handleMessage(msg)) {
return;
}
}
//以 Handler(){} 内存泄露写法
handleMessage(msg);
}
}
- MessageQueue中又做了哪些事情了呢?
//存消息,从 Handler.sendMessageAtTime 会跳到这
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
//当头节点为空或当前 Message 需要立即执行或当前 Message 的相对执行时间比头节点早
//则把当前 Message 插入头节点
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
//根据 when 插入链表的合适位置
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
}
}
return true;
}
//取消息
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//调用系统底层休眠nextPollTimeoutMillis时间
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//取出头节点 Message
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
//消息屏障处理
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// 如果当前时间小于msg的开始处理时间,将设置系统休眠时间,等待唤醒执行
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// 没有更多msg,设置为-1
nextPollTimeoutMillis = -1;
}
}
//省略代码...
}
nativePollOnce方法通过JNI调用进入到native层中去,最终nativePollOnce方法阻塞在epoll_wait方法中,epoll_wait方法会让当前线程释放CPU资源进入休眠状态,等到下一个消息到达(mWakeEventFd会往管道写入字符)或监听的其他事件发生时就会唤醒线程,然后处理消息,所以就算loop方法是死循环,当线程空闲时,它会进入休眠状态,不会消耗大量的CPU资源。(参考链接Android消息机制(native层))
- handler常见问题
- Looper.loop 死循环不会造成应用卡死吗?
- loop()方法中去调用queue.next(),只有当app退出时,next()才会返回null,如果取出的messge执行时间未到,会调用nativePollOnce(delay)使线程休眠一段时间,如果message队列为null时,则会调用nativePollOnce(-1)使线程休眠,直至新的消息来唤醒。所以死循环不会卡死应用,而且线程休眠和ANR是两个概念,ANR是消息超过一定时间后该事件还未得到处理 。
- Looper.loop 死循环不会造成应用卡死吗?
注解
参考链接:注解
- 定义 注解是代码里边的特殊标记,在编译、类加载、运行时被读取,并执行相应的处理。开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。
Annotation不能运行,它只有成员变量,没有方法,是程序元素一部分。
- 元注解
- @Retention
- @Target
- @Inherited
- @Documented
- @Repeatable
- @Retention:定义该注解的生命周期
- RetentionPoicy.SOURCE 注解只保留在源文件中,当Java文件被编译成class文件时,注解被遗弃,常用于做一些代码检查性工作,例如@Override。
- RetentionPoicy.CLASS 注解被保留在class文件,但JVM加载class文件时被遗弃,这是默认的生命周期,常用于在编译时进行一些预处理,比如生成一些辅助代码,ButterKnife。
- RetentionPoicy.RUNTIME 注解不仅被保留在class文件,JVM加载class文件后仍然存在,常用于在运行时动态获取注解信息,大多会与反射一起使用。
- @Target:定义注解所修饰的对象结构
- ElementType.CONSTRUCTOR 用于构造器
- ElementType.FIELD 用于描述属性
- ElementType.LOCAL_VARIABLE 用于描述局部变量
- ElementType.METHOD 用于描述方法
- ElementType.PACKAGE 用于描述包
- ElementType.TYPE 用于描述类、接口或枚举
- @Inherited:是否允许子类继承父类的注解,默认是 false。
- @Documented:是否会保存到 Javadoc 文档中。
- @Repeatable:JDK1.8新加的,表明当自定义注解的属性值有多个时,自定义注解可以多次使用。
- 自定义注解 自定义注解根据有没有成员变量分为标记型注解和元数据注解,解析注解根据Retention来划分,编译时注解采用APT解析,运行时注解通过反射获取。
APT
- 实现原理:
Java源代码编译过程可以分为三个步骤:
- 将源代码解析成抽象语法数
- 调用已注册的注解处理器
- 生成字节码 如果在第二步调用注解处理器生成了新的Java文件,那么编译器将重复第一二步骤,解析并处理新源文件。
- APT注解处理器 是一种注解处理工具,用来在编译期扫描和处理注解,通过注解来生成Java文件,即以注解为桥梁,通过预先规定好的代码规则来自动生成Java文件。
- 使用APT的步骤
-
- 定义注解
-
- 注册注解处理器
-
- 注解处理器处理注解
-
- 生成Java文件
-
- 引入使用
-
- 定义注解:后序
- 注册注解处理器 :AutoServcie
以前要注册注解处理器要在 module 的 META_INFO 目录新建 services 目录,并创建一个名为 Java.annotation.processing.Processor 的文件,然后在文件中写入要注册的注解处理器的全名。后来 Google 推出了 AutoService 注解库来实现注册注解处理器的注册,通过在注解处理器上加上@AutoService(Processor.class) 注解,即可在编译时生成 META_INFO 信息。
- 处理注解 创建一个Peocessor 继承AbstractProcessor,实现下面四个方法
/**
* 初始化方法
* 可以初始化一些给注解处理器使用的工具类
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementsUtils = processingEnvironment.getElementUtils();
}
/**
* 指定目标注解对象
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> hashSet = new HashSet<>();
hashSet.add(BindView.class.getCanonicalName());
return hashSet;
}
/**
* 指定使用的 Java 版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 处理注解
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){
//略,调用APT中相关的API处理注解
// Elements:用于处理程序元素的工具类
// Types:用于处理类型数据的工具类
// Filter:用于给注解处理器创建文件
// Messager:用于给注解处理器报告错误、警告、提示等信息
//在该方法中生成Java类
//roundEnvironment为Java编译时生成的抽象语法树,
//roundEnvironment.getElementsAnnotatedWith(注解.class)获取所有注解对象:Element程序元素
//获取后通过JavaPoet生成Java文件
}
-
Element的子类
- ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序,包括注释类型元素。
对应注解是 ElementType.METHOD 和 ElementType.CONSTRUCTOR。
- PackageElement
表示一个包程序元素,提供对有关包及其成员的信息访问。
对应注解是 ElementType.PACKAGE。
- TypeElement
表示一个类或接口程序元素,提供对有关类型及其成员的信息访问。
对应注解是 ElementType.TYPE。
注意:枚举类型是一种类,而注解类型是一种接口。
- TypeParameterElement
表示类、接口、方法元素的类型参数。
对应注解是 ElementType.PARAMETER。
- VariableElement
表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
对应注解是 ElementType.FIELD 和 ElementType.LOCAL_VARIABLE。
-
JavaPoet常用类:
- MethodSpec:代表一个方法的声明
- TypeSpec:代表一个类、接口、枚举声明
- FieldSpec:代表一个成员变量、字段声明
- JavaFile:包含一个顶级Java文件
http相关(http和https,tcp三次握手四次挥手,tcp、udp区别,https加密)
- http:超文本传输协议 是一种建立在TCP上的无状态连接,属于应用层协议,其传输的内容都是明文的,不安全。
- 通信格式
//====请求格式
//起始行
GET /tools/mockapi/448/weBankstep HTTP/1.1
//首部
Host: www.wanandroid.com
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: https://www.wanandroid.com/tools/mockapi
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: xxx
//主体
Get 请求通常不会发送主体,所以为空
//=========响应格式
//起始行
HTTP/1.1 200 OK
//首部
Server: Apache-Coyote/1.1
Cache-Control: private
Expires: Thu, 01 Jan 1970 08:00:00 CST
Content-Type: application/json;charset=utf-8
Transfer-Encoding: chunked
Date: Fri, 15 Mar 2019 13:30:00 GMT
//主体
Response: {
"idInfoExisted": true,
"openAccountStep": "NONE"
}
- 加密方式:参考链接对称加密、非对称加密、RSA(总结)
- 对称加密:指的是加密、解密都是用的同一串密钥。常见算法:DES,AES。
- 优缺点:加解密效率高,加密速度快,缺陷在于对密钥的管理难度较大。
- 非对称加密:指的是加密、解密使用不同的密钥,一把作为公钥、一把作为私钥,公钥加密的信息只有私钥可以解密,反之亦然。常见的算法:RSA。
- 优缺点:安全性更高,公钥是公开的,不需要将私钥发送给别人,缺点就是加密解密时间长速度慢。
- 混合使用
- 将对称加密的密钥用非对称加密的公钥进行加密,另一方用私钥进行解密,最后用对称加密的密钥加密数据。
- 对称加密:指的是加密、解密都是用的同一串密钥。常见算法:DES,AES。
- Https:在http上新增了SSL/TLS,提供三个基本服务
- 加密
- 身份验证(数字证书)
- 消息完整性校验
- https握手流程 参考链接:深入理解HTTPS协议
- TCP三次握手、四次挥手 参考链接:面试官,不要再问我三次握手和四次挥手
- TCP与UDP的区别 参考链接:TCP协议与UDP协议的区别
- 基于连接与无连接
- 对系统资源的要求(TCP较多)
- UDP程序结构简单
- TCP保证数据正确性,UDP可能会丢包
- TCP保证数据顺序
Retrofoit的实现原理 参考链接:深入理解Retrofit动态代理
- 代理模式: 在原有的逻辑调用中,再额外的使用一个代理类的方法,使这个逻辑的变化过程尽量全部卸载代理类里中,这样就可以不改变原有类的逻辑增加新的逻辑。
- 静态代理: 定义一个接口interface A,实现类 class A implements A,同时再定义一个proxy类 class ProxyA implements A。这种静态代理的方案在复杂度上升时就会生成无数个Proxy类,维护非常困难。
- 动态代理:
不用直接写代理class类,也就是说不用写.Java文件,直接获取到动态代理class类对象(直接在JVM中生成这个代理类的class对象),然后用这个class的类对象创建实例。这样写的好处在于,即使A接口发生重大变化,我们都不需要更改代理类,只需要修改实现类就行了。
- 动态代理原理:自定义一个实现了InvocationHandler类接口的类,在代码运行时,通过Proxy.newProxyInstance动态的生成我们需要的class对象,整个class对象是在代码运行期生成在JVM中的。
- Retrofit原理
Retrofit本身是不产生请求,而是使用okhttp产生请求,Retrofit通过动态代理来完成了okhttp请求的构建Call操作,通过注解来解析请求Call操作需要的数据,例如header,请求方式,请求地址和参数等。
- okhttp参考链接
- ARouter实现原理 阿里开源路由框架ARouter的源码分析
- LRU缓存
- Android面试题
- Android面试题集锦--上
MVP与MVVM的区别?
- MVP:是广义上的架构模式,适用于面向实体或虚拟接口的开发。主要是在MVC的背景下,通过依赖倒置来解决逻辑复用难、实现更替难的问题。
- MVVM:是侠义上的架构模式,专注于页面开发。主要是在多人协作的软件工程背景下,通过只操作ViewModel中映射的视图数据来刷新视图状态,以此来解决视图调用的一致性问题。 MVP的特质是依赖倒置,MVVM的特质是数据驱动。
HashMap相关
参考链接:[Hash表](www.cnblogs.com/dolphin0520…
- Hash表 Hash表也成为散列表,是一种特殊的数据结构,能够快速的定位到想要查找到的记录而不是与表中存在的记录关键字进行比较来查找。
- Hash碰撞:计算的hash值相同,需要放入同一个bucket中,解决hash碰撞通常有两种方法。
- 链表法:将相同hash值的对象组织成一个链表放在hash值对应的槽位。
- 开放地址法:通过一个探测算法,当某个槽位被占用时,会继续查找下一个可以使用的槽位。
- HashMap实现,内部为数组+单链表实现,Java8开始引入红黑树来提升查找效率,当链表长度大于8时,会转换为红黑树来存储,小于6时转换回链表。Java 8系列之重新认识HashMap
Activity相关
生命周期
- onCreate :activity第一次被创建,可以做一切初始化操作
- onStart:针对用户可见,但不可交互操作,因为onresume时才把view 添加到界面上来
- onResume:处于前台,接受用户交互操作
- onPause:用户可见,不可操作,启动或者恢复另一个Activity时调用
- onStop:用户不可见
- onDestory:activity被销毁前调用,之后activity变为被销毁状态
- onRestart:activity调用onStop后,再切换到前台会走onRestart->onStart->onResume
A 跳转到 B 生命周期 再从B退回到A
打开(standard):A.onPause->B.onCreate->B.onStart->B.onResume->A.onStop
打开(B为singleTop且B已经存在栈顶):A.onPause->B.onNewIntent->B.onResume->A...
打开(B为SingleInstance、singlgeTask并且有实例):A.onPause->B.onNewIntent->B.onRestart...
退回:B.onPause->A.onRestart->A.onStart->A.onResume->B.onStop->B.onDestory
启动模式
- 标准模式(standard):每次启动都会新建
- 栈顶复用(singleTop):若栈顶为该activity就直接回调onNewIntent复用,否则新建
- 栈内复用(singleTask):若栈内有activity,将该实例的上边activity出栈,使其位于栈顶,回调onNewIntent,否则新建
- 单例模式(singleInstance):若单独的栈内有实例,则回调onNewIntent复用,否则创建一个新的栈并创建实例放入栈中
重要知识点
1. 锁屏只会调用onPause,开屏后调用onResume
2. 新的activity为透明主题,旧的activity不会走onStop
3. 横竖屏切换未配置android:configChanges="orientation|screenSize"时,会重走创建的生命周期,如果配置了,只会调用onConfigurationChanged方法
4. 生命周期的回调都是AMS通过Binder通知应用进程调用的,而Dialog、Toast、PopupWindow是通过WindowManager.addView()显示的,所以不会影响生命周期,不过启动一个theme为Dialog的Activity是会影响的
5. onActivytResult调用时机:B.onPause->A.onActivityResult->B.onRestart...
onSaveInstanceState作用
异常情况下系统配置发生改变时导致activity被杀死重建,资源内存不足导致低优先级的activity被杀死
- 系统会调用该方法来保存当前activity的状态,并且在onStop之前,与onPause没有既定关系
- 重建后,系统会调用onRestoreInstanceState方法,并且把保存的bundle对象同时传入onRestoreInstanceState和onCreate中
theme和style
- style样式是一个属性集合,用于指定单个View的外观,如字体颜色、大小、背景色等属性
- theme主题是应用于整个应用、Activity或视图层次结构,不仅仅是单个view
- 优先级
- 文本span的样式
- 代码方式设置样式
- 将单独的属性直接应用到view
- 将样式应用到view
- 默认样式
- 将主题背景应用到view集合、activity或整个应用
- 特定的样式,如TextView的TextAppearance
隐式启动
- schema完整路径:schema://host:port/path/query
- 样例:juejin.cn/editor/draf…
- schema:https
- host:juejin.cn
- path:/editor/drafts/6890495879074185229
- authotity:juejin.cn
- query:无
<!--schema-->
<activity android:name=".module.study.components.activity.schema.ZZZActivity"
android:screenOrientation="portrait">
<intent-filter>
<!--action.VIEW和category.DEFAULT必须设置-->
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<!--如果需要浏览器支持打开,则category.BROWSABLE-->
<category android:name="android.intent.category.BROWSABLE"/>
<!--schema的协议类型:随便设置,只要按照统一规则,前后端一致就行-->
<data
android:scheme="xl"
android:host="goods"
android:port="8888"
android:path="/goodsDetail"
/>
</intent-filter>
</activity>
Intent相关
- 传递基本数据和序列化data,不能传递较大的对象
Fragment
生命周期
onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume->onPause->onStop->onDestoryView->onDestory->onDetach
Fragment管理
- 使用Fragment,需要用到 FragmentManager,想用获取它,可以通过在 Activity 中调用 getSupportFragmentManager(),Fragment中使用getChildFragmentManager
- FragmentManager管理前需要获取transaction对象,完成add、remove、replace操作,replace是先remove掉相同id的fragment,再add,最后需要调用commit才能完成,如果有必要可以使用setTranssition设置过度动画
- commit是不会立即提交事务,而是在UI线程可以处理该操作时再安排其他线程处理,如果需要UI线程处理,使用commitNow,只能在activity保存状态前使用commit提交事务,因为恢复后的activity,可能会失去之后提交的fragment状态,如果对于丢失的状态不在乎,则可以用commitAllowingStateLoss
- addBackStack和popBackStack是针对系统返回键处理,返回上一个栈
- 创建Fragment时使用setArgument方法传递参数,如果使用构造函数传参,那么在某些情况activity重建后参数会丢失,因为重建会默认调用fragment的无参构造函数
FragmentPageAdapter、FragmentStatePagerAdapter和FragmentStateAdapter区别及使用场景
- FragmentPagerAdapter 内部transaction使用的是attach和detach,并没有移除当前fragment,也就是说fragment仍然存在fragmentmanager中,UI被移除,实例不会被回收。离开界面时会走到onDestoryView,重新出现在界面时生命周期会走oncreateView->onResume,所以适合Fragment个数较少的情况
- FragmentStatePagerAdapter 内部transaction使用的是add和remove,在remove前会使用SavedState来保存当前fragment的状态,add的时候,如果SavedState存在再传递给fragment,生命周期会完整的从onCreate->onDestroy,FragmentManager内部不会保存实例,使用mFragments链表来缓存当前fragment和左右limited个数的实例,其余的实例会被回收,所以适用于Fragment较多的情况,减少内存开销
- FragmentStateAdapter 与FragmentStatePagerAdapter类似,transaction不会保存实例,mFragments数组缓存实例,也会使用SavedState保存状态,生命周期会重走,FragmentStateAdapter是RecyclerView.Adapter子类,结合ViewPager2使用
Fragment 懒加载
- 判断fragment的显隐
- show和hidden,根据onHiddenChanged方法回调判断
- 传统 ViewPager 根据setUserVisibleHint方法来判断
- Androidx ViewPager 使用setMaxLifecycle控制生命周期,onResume和onStop中判断
- ViewPager2 本身就支持对实际可见的fragment才调用onResume方法.
View的绘制流程
事件分发机制
事件分发的顺序为Activity到ViewGroup,再到View,如果View未处理该事件,那么将事件依次返回上一层去处理,若都没有处理,最后由Activity处理,若DOWN事件在某个节点被消费后,那么后续的MOVE、UP事件都会交给该节点处理。源代码实现方面,Activity、ViewGroup、View都有分发事件(dispatchTouchEvent)和处理事件(onTouchEvent)方法,onTouchEvent方法在dispatchTouchEvent中调用,ViewGroup有拦截事件(onTnterceptTouchEvent)方法,也在dispatchTouchEvent中调用。
- Activity Activity中的dispatchTouchEvent中调用window的superDispatchTouchEvent方法,其内部调用的是docerview的superDispatchTouchEvent方法来判断是否消费了该事件,docerview为framelayout的子类,内部就是调用ViewGroup的dispatchTouchEvent方法,这样就把事件分发到ViewGroup层,如果ViewGroup没有消费该事件,那么就调用ouTouchEvent处理该事件。
Service
Service的创建方法
ANR(cloud.tencent.com/developer/a…
ANR排查:
通过 adb pull /data/anr获取trace.txt文件,trace文件中可以查看当前进程,发生的时间,ANR类型(keyDispatchingTimedOut),cpu负载情况,ioWait等。
ANR触发场景
- 输入事件分发超时5s未响应完毕
- 前台广播在10s内,后台广播在20s内未执行完成s
- 前台服务在20s内、后台服务在200s内未执行完成
- 内容提供者,在publish过超时10s
ANR 预防
- 避免在主线程中做耗时的任务,操作数据库、读写文件等异步处理。
- 广播中如果需要做耗时的任务,使用intentService或者JobIntentService
- 程序编写避免死锁或者死循环
Service ANR原理:
- Service创建之前会延迟发送一个消息,而这个消息就是ANR的起源;
- Service创建完毕,在规定的时间之内执行完毕onCreate()方法就移除这个消息,就不会产生ANR了;
- 在规定的时间之内没有完成onCreate()的调用,消息被执行,ANR发生。
卡顿优化
卡顿检测
looper函数中,有一个mlogging对象,这个对象在message处理的前后会被调用,如果处理message发生了耗时操作,那么便可以通过mlogging进行监控。
卡顿分析处理
- 动画记得及时关闭。
- 避免耗时方法,提升算法效率。
- 布局进行优化,减少绘制。
- 内存优化,减少gc,避免频繁的内存抖动。
- 适当的进行数据预处理,缓存。
布局优化
inflate是通过xmlparse解析xml所以是io类型操作,属于耗时的, 创建view时,通过发射,也会降低速度。
- 实用layoutinspector来检查布局件套。
- 降低使用constantlayout降低UI层级,背景颜色尽可能的不重复设置,避免过度绘制。
- 减少view个数,如textview同时显示图标和文字,或者显示多个文字。
- 简单的布局直接使用代码创建。
- 使用viewstub懒加载状态布局,如错误界面、提示界面。
- 使用merge标签减少层级。
内存优化
内存泄漏
当前不再使用的对象,被GC Root引用,导致不能正常回收。
- 资源性对象不使用时,应该调用close函数关闭。
- 注册性对象未及时注销,如广播和Rxbus观察对象。
- 静态变量持有大数据对象。
- 非静态内部类持有外部类的引用。
- 使用若引用。
减少不必要的内存开销
- 减少自动装箱,在装箱的过程中会产生更多的内存和性能开销,如int4个字节,而Integer16个字节。
- 使用恰当的数据类型,如使用int不是long。
- 内存复用,如通用的资源、字符串、颜色值、简单的界面复用。使用holder机制实现复用,使用对象池复用。
- 使用stringbuilder,在频繁调用的方法里,尽量避免new 对象,可以用对象池来创建。
- 图片加载优化,图片格式转化以及进行缩放。
profiler工具
在memory中,手动点击GC,然后查看内存状态,如果始终居高不下,则内存引用有问题,使用dump功能查看内存快照,可以查看对象的个数、内存大小,以及被引用的地方。
RecyclerView的缓存
获取holder
一切都是onLayoutChildren开始的,先dettach所有的view,在attach view
- 如果是预布局阶段,尝试从mChangedScrap中获取。
- 通过position尝试从mAttachedScrap、mHiddenViews(ChildHelper类中)、mCachedViews中获取。
- 如果设置了stableId,通过id在mAttachedScrap、mCachedViews获取。
- 如果自己设置的mViewCacheExtension不为空的话,尝试获取。
- 尝试从RecyclerViewPool中获取,只检验viewType是否一致。
- 创建新的holder。 scrap是在布局期间使用的,布局完成后便被清空,mAttachedScrap中的holder不会重走bind方法,mChangedScrap会重新bind数据,notifyItemChanged方法会把holder放在mChangedScrap中,scrap是存放的dettach的视图。 mCachedViews存放的是remove的视图,依然保存着之前的信息,比如位置、绑定的数据等,默认容量是2。mRecyclerPool根据itemtype来存放数据,无绑定信息。
onlayout方法中,如果scrap中还有holder就会放入cacheviews或者recyclerpool中。
滑动过程
滑出是调用的remove,所以存放在cacheViews或者recyclerpool中,如果cacheviews满了,就会把最老的缓存holder放入recyclerpool中,再插入。 滑入是先在cacheViews中获取,再recyclerpool或者新建。
notifyDataSetChanged方法会都缓存到recyclerpool中,都要重新绑定数据,如果超过缓存数量,就重新创建,notifyItemChanged会将视图缓存到scrap中,只有mChangedScrap中的视图要重新绑定。
View的绘制流程
window是一个窗口概念,对应唯一实现类是PhoneWindow,Window和view是通过ViewRoot来建立联系的,ViewRootImpl是ViewRoot唯一实现类。
Activity->phoneWindow->decorView->(ActionBar + content)
在create中即使布局已经被加入到decorView中了,但是没被加入到window上,不能响应事件,在ActivityThread的handleResumeActivity方法中调用onResume方法,之后再windowmanager.addview将decorView添加到窗口上,addview会创建一个ViewRootImpl对象,关联window和decorView,最后调用performTraversals开始绘制。
多次onMeasure->onlayout-onDraw
onDraw步骤:背景->保存图层->内容->子视图->还原图层->滚动条
invalidate和postInvalidate二者都会出发刷新View,并且当这个View的可见性为VISIBLE的时候,View的onDraw()方法将会被调用,invalidate()方法在 UI 线程中调用,重绘当前 UI。postInvalidate() 方法在非 UI 线程中调用,通过Handler通知 UI 线程重绘。
requestLayout()也可以达到重绘view的目的,但是与前两者不同,它会先调用onLayout()重新排版,再调用ondraw()方法。当view确定自身已经不再适合现有的区域时,该view本身调用这个方法要求parent view(父类的视图)重新调用他的onMeasure、onLayout来重新设置自己位置。
todo
- blog.51cto.com/13626762/25… 指令重排序是对于物理机内存模型的
- BroadCast
- Service
- ContentProvider
- 动画相关
- 事件分发
- 绘制流程
- Okhttp
- Retrofit
- Arouter
- Glide
- MVP
- MVVM
- Recyclerview
- invalidate与requestlayout区别:invalidate只会触发draw,用户主动调用requestLayout只会触发mearsure和layout过程
- 数据结构相关(arraylist,hashmap)
- 设计模式相关(六大设计原则,常用设计模式)
- 算法相关(递归,排序,树便利)
- java泛型
- Object的wait()
- 三级缓存、RLU缓存