JAVA部分
JVM虚拟机
JVM基本构成
从上图可知,JVM主要包括四个部分:
1.类加载器(ClassLoader):在JVM启动时或者在类运行将需要的class加载到JVM中
2.执行引擎:负责执行class文件中包含的字节码指令;
3.本地方法接口:主要是调用C或C++实现的本地方法及回调结果
4.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域:
方法区(MethodArea):用于存储类结构信息的地方,包括常量池、静态常量、构造函数等。虽然JVM规范把方法区描述为堆的一个辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。 java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域。从存储的内容我们可以很容易知道,方法和堆是被所有java线程共享的。 java栈(Stack):java栈总是和线程关联在一起,每当创一个线程时,JVM就会为这个线程创建一个对应的java栈在这个java栈中,其中又会包含多个栈帧,每运行一个方法就建一个栈帧,用于存储局部变量表、操作栈、方法返回等。每一个方法从调用直至执行完成的过程,就对应一栈帧在java栈中入栈到出栈的过程。所以java栈是现成有的。 程序计数器(PCRegister):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证程切换回来后,还能恢复到原先状态,就需要一个独立计数器,记录之前中断的地方,可见程序计数器也是线程私有的。 本地方法栈(Native MethodStack):和java栈的作用差不多,只不过是为JVM使用到native方法服务的。
JVM、DVM(dalvik)和ART之间的区别
JVM和Dalvik的区别
1、执行的字节码不一样
jvm:java->class->jar
dvm:java->class->dex
2、 基于的架构不一样
Java JIT(just in time)即时编译器是sun公司采用了hotspot虚拟机取代其开发的classic vm之后引入的一项技术,目的在于提高java程序的性能,改变人们“java比C/C++慢很多”这一尴尬印象。说起来是编译器,但此编译器与通常说的javac那个编译器不同,它其实是将字节码编译为硬件可执行的机器码的。
Dalvik 基于寄存器,而 JVM 基于栈。基于寄存器的虚拟机对于更大的程序来说,在它们编译的时候,花费的时间更短。 JVM字节码中,局部变量会被放入局部变量表中,继而被压入堆栈供操作码进行运算,当然JVM也可以只使用堆栈而不显式地将局部变量存入变量表中。Dalvik字节码中,局部变量会被赋给65536个可用的寄存器中的任何一个,Dalvik指令直接操作这些寄存器,而不是访问堆栈中的元素。
3、Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
4、Dalvik虚拟机在android2.2之后使用JIT (Just-In-Time)技术,与传统JVM的JIT并不完全相同,
5、Dalvik虚拟机有自己的 bytecode,并非使用 Java bytecode。
Dalvik与ART的区别
1、ART与Dalvik最大的不同在于,在启用ART模式后,系统在安装应用的时候会进行一次预编译,在安装应用程序时会先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。
2、ART占用空间比Dalvik大(字节码变为机器码之后,可能会增加10%-20%),这就是“时间换空间大法”。
3、预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗
JVM的内存模型(JMM)
Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
总之,JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性。
GC垃圾回收
-
强引用:代码中普遍存在的,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
-
软引用:SoftReference,用来描述还有用但是非必须的对象,当内存不足的时候会回收这类对象。
-
弱引用:WeakReference,用来描述非必须对象,弱引用的对象只能生存到下一次GC发生时,当GC发生时,无论内存是否足够,都会回收该对象。
-
虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用取得一个对象的引用,它存在的唯一目的是在这个对象被回收时可以收到一个系统通知
判断对象存活一般有两种方式:引用计数算法和可达性分析算法
GC Roots对象通常包括:
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法中类的静态属性引用的对象
- 方法区中常量引用的对象
- Native方法引用的对象
常见的回收算法:
- 标记-清除(Mark-sweep)
- 标记-整理(Mark-Compact)
- 复制(Copying)
- 分代收集算法
-
新生代: 对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。 Java堆内存一般可以分为新生代、老年代和永久代三个模块: 1.所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。 2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。 3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。 4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
-
老年代: 1.在老年代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。 2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC,即Full GC。Full GC发生频率比较低,老年代对象存活时间比较长。
-
永久代: 主要存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类
设计模式
设计模式不要死记硬背,常问的设计模式可以结合在安卓中的使用场景去记忆:
-
Bulider(建造者)模式
定义:将一个复杂对象的构建与它的表示分离,避免过多的setter方法 使用场景:AlertDialog、Notification初始化 -
单例模式
定义:保证一个类仅有一个实例 使用场景:Android中的系统服务都是通过容器的单例模式获取 -
抽象工厂模式
定义:为创建一组相关或者是相互依赖的对象提供一个接口 使用场景:封装的BaseActivity基类 -
责任链模式
定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止 使用场景:ViewGroup事件传递、okhttp的拦截器 -
享元模式
定义:运用共享技术有效地支持大量细粒度的对象(其实就是复用对象) 使用场景:Message.obtainMessage通过重用Message对象来避免大量的Message对象被频繁的创建和销毁 -
观察者模式
定义:一个对象发生改变时,所有信赖于它的对象自动做相应改变 使用场景:ContentObserver数据发生变化时自动通知其他观测方 -
代理模式(静态代理、动态代理)
定义:为其他对象提供一个代理以控制对这个对象的访问 使用场景:Retrofit创建service时就是动态代理;AIDL中的比如获取到的AMS、WMS等通过静态代理获取到binder对象 -
适配器模式
定义:将一个类的接口转换成客户希望的另外一个接口 使用场景:RecyclerView中的Adapter -
组合模式
定义:将对象组合成树形结构以表示“部分-整体”的层次结构 使用场景:View和ViewGroup的组合 -
装饰模式
定义:动态地给一个对象添加一些额外的职责。就扩展功能而言, 它比生成子类方式更为灵活 使用场景:ContextWrapper才是继承自Context。ContextWrapper就是装饰者 -
外观模式
定义:为子系统中的一组接口提供一个一致的界面,使得子系统更易于使用 使用场景:Context/ContextImpl
HashMap相关
HashMap 1.7的原理:
HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。
负载因子:
- 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
- 因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。
其实真正存放数据的是 Entry<K,V>[] table,Entry 是 HashMap 中的一个静态内部类,它有key、value、next、hash(key的hashcode)成员变量。
put 方法:
- 判断当前数组是否需要初始化。
- 如果 key 为空,则 put 一个空值进去。
- 根据 key 计算出 hashcode。
- 根据计算出的 hashcode 定位出所在桶。
- 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
- 如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置。(当调用 addEntry 写入 Entry 时需要判断是否需要扩容。如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。)
get 方法:
- 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
- 判断该位置是否为链表。
- 不是链表就根据 key、key 的 hashcode 是否相等来返回值。
- 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
- 啥都没取到就直接返回 null 。
HashMap 1.8的原理:
当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N),因此 1.8 中重点优化了这个查询效率。
TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
HashEntry 修改为 Node。
put 方法:
- 判断当前桶是否为空,空的就需要初始化(在resize方法 中会判断是否进行初始化)。
- 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
- 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
- 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
- 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
- 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
- 如果在遍历过程中找到 key 相同时直接退出遍历。
- 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
- 最后判断是否需要进行扩容。
get 方法:
- 首先将 key hash 之后取得所定位的桶。
- 如果桶为空则直接返回 null 。
- 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
- 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
- 红黑树就按照树的查找方式返回值。
- 不然就按照链表的方式遍历匹配返回值。
修改为红黑树之后查询效率直接提高到了 O(logn)。但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环:
- 在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环:在 1.7 中 hash 冲突采用的头插法形成的链表,在并发条件下会形成循环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。
ConcurrentHashMap 1.7原理:
ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
put 方法:
首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。
-
虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
-
首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁:
尝试自旋获取锁。 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
-
将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
-
遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
-
为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
-
最后会使用unlock()解除当前 Segment 的锁。
get 方法:
- 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
- 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
- ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
ConcurrentHashMap 1.8原理:
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:那就是查询遍历链表效率太低。和 1.8 HashMap 结构类似:其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
CAS:
如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作。
问题:
- 目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大的执行开销。
put 方法:
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 如果当前 key 定位出的 Node为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
- 如果都不满足,则利用 synchronized 锁写入数据。
- 最后,如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
get 方法:
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 就不满足那就按照链表的方式遍历获取值。
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
ConcurrentHashMap加锁机制是什么,详细说一下?
Java7 ConcurrentHashMap
ConcurrentHashMap作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比HashTable的表锁在性能上的提升非常之大。HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
concurrencyLevel:并行级别、并发数、Segment 数。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。其中的每个 Segment 很像 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
初始化槽: ensureSegment
ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化。对于并发操作使用 CAS 进行控制。
Java8 ConcurrentHashMap
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。结构上和 Java8 的 HashMap(数组+链表+红黑树) 基本上一样,不过它要保证线程安全性,所以在源码上确实要复杂一些。1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
HashMap何时扩容:
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即大于当前数组的长度乘以加载因子的值的时候,就要自动扩容。
扩容的算法是什么:
扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。
Hashmap如何解决散列碰撞
Java中HashMap是利用“拉链法”处理HashCode的碰撞问题。在调用HashMap的put方法或get方法时,都会首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法。hashMap基于hasing原理,我们通过put和get方法存取对象。当我们将键值对传递给put方法时,他调用键对象的hashCode()方法来计算hashCode,然后找到bucket(哈希桶)位置来存储对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当碰撞发生了,对象将会存储在链表的下一个节点中。hashMap在每个链表节点存储键值对对象。当两个不同的键却有相同的hashCode时,他们会存储在同一个bucket位置的链表中。键对象的equals()来找到键值对。
Hashmap底层为什么是线程不安全的?
- 并发场景下使用时容易出现死循环,在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环;
- 在 1.7 中 hash 冲突采用的头插法形成的链表,在并发条件下会形成循环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。
CopyOnWriteArrayList的了解。
CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
优点和缺点:
优点:
1.据一致性完整,为什么?因为加锁了,并发数据不会乱。
2.解决了像ArrayList、Vector这种集合多线程遍历迭代问题,记住,Vector虽然线程安全,只不过是加了synchronized关键字,迭代问题完全没有解决!
缺点:
1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。
2.数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
使用场景:
1、读多写少(白名单,黑名单,商品类目的访问和更新场景),为什么?因为写的时候会复制新集合。
2、集合不大,为什么?因为写的时候会复制新集合。
3、实时性要求不高,为什么,因为有可能会读取到旧的集合数据。
JAVA多线程
Java中的线程池共有几种?
Java有四种线程池:
- newCachedThreadPool
不固定线程数量,且支持最大为Integer.MAX_VALUE的线程数量:
public static ExecutorService newCachedThreadPool() {
// 这个线程池corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE
// 意思也就是说来一个任务就创建一个woker,回收时间是60s
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可缓存线程池:
1、线程数无限制。
2、有空闲线程则复用空闲线程,若无空闲线程则新建线程。
3、一定程序减少频繁创建/销毁线程,减少系统开销。
2. newFixedThreadPool
一个固定线程数量的线程池:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
// corePoolSize跟maximumPoolSize值一样,同时传入一个无界阻塞队列
// 该线程池的线程会维持在指定线程数,不会进行回收
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
定长线程池:
1、可控制线程最大并发数(同时执行的线程数)。
2、超出的线程会在队列中等待。
3. newSingleThreadExecutor
可以理解为线程数量为1的FixedThreadPool:
public static ExecutorService newSingleThreadExecutor() {
// 线程池中只有一个线程进行任务执行,其他的都放入阻塞队列
// 外面包装的FinalizableDelegatedExecutorService类实现了finalize方法,在JVM垃圾回收的时候会关闭线程池
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
单线程化的线程池:
1、有且仅有一个工作线程执行任务。
2、所有任务按照指定顺序执行,即遵循队列的入队出队规则。
4. newScheduledThreadPool。
支持定时以指定周期循环执行任务:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
注意:前三种线程池是ThreadPoolExecutor不同配置的实例,最后一种是ScheduledThreadPoolExecutor的实例。
线程池原理
线程池的机制是这样的:
- 如果正在运行的线程数 < coreSize,马上创建核心线程执行该task,不排队等待;
- 如果正在运行的线程数 >= coreSize,把该task放入阻塞队列;
- 如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该task;
- 如果队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。
线程池的线程复用:
这里就需要深入到源码addWorker():它是创建新线程的关键,也是线程复用的关键入口。最终会执行到runWoker,它取任务有两个方式:
firstTask:这是指定的第一个runnable可执行任务,它会在Woker这个工作线程中运行执行任务run。并且置空表示这个任务已经被执行。 getTask():这首先是一个死循环过程,工作线程循环直到能够取出Runnable对象或超时返回,这里的取的目标就是任务队列workQueue,对应刚才入队的操作,有入有出。
其实就是任务在并不只执行创建时指定的firstTask第一任务,还会从任务队列的中通过getTask()方法自己主动去取任务执行,而且是有/无时间限定的阻塞等待,保证线程的存活。
信号量
semaphore 可用于进程间同步也可用于同一个进程间的线程同步。 可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
线程池都有哪几种工作队列?
1、ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了这个队列。
3、SynchronousQueue 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue 一个具有优先级的无限阻塞队列。
怎么理解无界队列和有界队列?
有界队列
1.初始的poolSize < corePoolSize,提交的runnable任务,会直接做为new一个Thread的参数,立马执行 。
2.当提交的任务数超过了corePoolSize,会将当前的runable提交到一个block queue中。
3.有界队列满了之后,如果poolSize < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。
4.如果3中也无法处理了,就会走到第四步执行reject操作。
无界队列
与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。
多线程中的安全队列一般通过什么实现?
Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue. 对于BlockingQueue,想要实现阻塞功能,需要调用put(e) take() 方法。而ConcurrentLinkedQueue是基于链接节点的、无界的、线程安全的非阻塞队列。
synchronized的原理?
synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现,而 synchronized 同步方法使用了ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁(可能会先进行自旋锁升级,如果失败再尝试重量级锁升级)。
我注意到有的观点认为 Java 不会进行锁降级。实际上据我所知,锁降级确实是会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。
Synchronized优化后的锁机制简单介绍一下,包括自旋锁、偏向锁、轻量级锁、重量级锁?
自旋锁:
线程自旋说白了就是让cpu在做无用功,比如:可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。
偏向锁
偏向锁就是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。
轻量级锁:
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争用的时候,偏向锁就会升级为轻量级锁;
重量级锁
重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。
谈谈对Synchronized关键字涉及到的类锁,方法锁,重入锁的理解?
synchronized修饰静态方法获取的是类锁(类的字节码文件对象)。
synchronized修饰普通方法或代码块获取的是对象锁。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。
它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!因为锁的持有者是“线程”,而不是“调用”。这就是内置锁的可重入性。
wait、sleep的区别和notify运行过程。
wait、sleep的区别
最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。wait 通常被用于线程间交互,sleep 通常被用于暂停执行。
- 首先,要记住这个差别,“sleep是Thread类的方法,wait是Object类中定义的方法”。尽管这两个方法都会影响线程的执行行为,但是本质上是有区别的。
- Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁。如果能够帮助你记忆的话,可以简单认为和锁相关的方法都定义在Object类中,因此调用Thread.sleep是不会影响锁的相关行为。
- Thread.sleep和Object.wait都会暂停当前的线程,对于CPU资源来说,不管是哪种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其它线程。区别是,调用wait后,需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间。
- 线程的状态参考 Thread.State的定义。新创建的但是没有执行(还没有调用start())的线程处于“就绪”,或者说Thread.State.NEW状态。
- Thread.State.BLOCKED(阻塞)表示线程正在获取锁时,因为锁不能获取到而被迫暂停执行下面的指令,一直等到这个锁被别的线程释放。BLOCKED状态下线程,OS调度机制需要决定下一个能够获取锁的线程是哪个,这种情况下,就是产生锁的争用,无论如何这都是很耗时的操作。
notify运行过程
当线程A(消费者)调用wait()方法后,线程A让出锁,自己进入等待状态,同时加入锁对象的等待队列。 线程B(生产者)获取锁后,调用notify方法通知锁对象的等待队列,使得线程A从等待队列进入阻塞队列。 线程A进入阻塞队列后,直至线程B释放锁后,线程A竞争得到锁继续从wait()方法后执行。
synchronized关键字和Lock的区别你知道吗?为什么Lock的性能好一些?
| 类别 | synchronized | Lock(底层实现主要是Volatile + CAS) |
|---|---|---|
| 存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
| 锁的释放 | 1、已获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁。 | 在finally中必须释放锁,不然容易造成线程死锁。 |
| 锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待。 | 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待 |
| 锁状态 | 无法判断 | 可以判断 |
| 锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
| 性能 | 少量同步 | 大量同步 |
Lock(ReentrantLock)的底层实现主要是Volatile + CAS(乐观锁),而Synchronized是一种悲观锁,比较耗性能。但是在JDK1.6以后对Synchronized的锁机制进行了优化,加入了偏向锁、轻量级锁、自旋锁、重量级锁,在并发量不大的情况下,性能可能优于Lock机制。所以建议一般请求并发量不大的情况下使用synchronized关键字。
volatile原理。
在《Java并发编程:核心理论》一文中,我们已经提到可见性、有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果对Synchonized原理有了解的话,应该知道Synchronized是一个较重量级的操作,对系统的性能有比较大的影响,所以如果有其他解决方案,我们通常都避免使用Synchronized来解决问题。
而volatile关键字就是Java中提供的另一种解决可见性有序性问题的方案。对于原子性,需要强调一点,也是大家容易误解的一点:对volatile变量的单次读/写操作可保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。
volatile也是互斥同步的一种实现,不过它非常的轻量级。
volatile 的意义?
- 防止CPU指令重排序
volatile有两条关键的语义:
保证被volatile修饰的变量对所有线程都是可见的
禁止进行指令重排序
synchronized 和 volatile 关键字的作用和区别。
Volatile
1)保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值,这新值对其他线程来是立即可见的。
2)禁止进行指令重排序。
作用
volatile 本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其它线程被阻塞住。
区别
1.volatile 仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
2.volatile 仅能实现变量的修改可见性,并不能保证原子性;synchronized 则可以保证变量的修改可见性和原子性。
3.volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
4.volatile 标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
ReentrantLock的内部实现
ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心,CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点,Node有两种模式:共享模式和独占模式。ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。AQS的子类一般只需要重写tryAcquire(int arg)和tryRelease(int arg)两个方法即可。
ReentrantLock的处理逻辑:
其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。Sync作为ReentrantLock中公用的同步组件,继承了AQS(要利用AQS复杂的顶层逻辑嘛,线程排队,阻塞,唤醒等等);NonFairSync和FairSync则都继承Sync,调用Sync的公用逻辑,然后再在各自内部完成自己特定的逻辑(公平或非公平)。
接着说下这两者的lock()方法实现原理:
NonFairSync(非公平可重入锁)
1.先获取state值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则代表获取到排他锁了;
2.若state大于0,肯定有线程已经抢占到资源了,此时再去判断是否就是自己抢占的,是的话,state累加,返回true,重入成功,state的值即是线程重入的次数;
3.其他情况,则获取锁失败。
FairSync(公平可重入锁)
可以看到,公平锁的大致逻辑与非公平锁是一致的,不同的地方在于有了!hasQueuedPredecessors()这个判断逻辑,即便state为0,也不能贸然直接去获取,要先去看有没有还在排队的线程,若没有,才能尝试去获取,做后面的处理。反之,返回false,获取失败。
最后,说下ReentrantLock的tryRelease()方法实现原理:
若state值为0,表示当前线程已完全释放干净,返回true,上层的AQS会意识到资源已空出。若不为0,则表示线程还占有资源,只不过将此次重入的资源的释放了而已,返回false。
ReentrantLock是一种可重入的,可实现公平性的互斥锁,它的设计基于AQS框架,可重入和公平性的实现逻辑都不难理解,每重入一次,state就加1,当然在释放的时候,也得一层一层释放。至于公平性,在尝试获取锁的时候多了一个判断:是否有比自己申请早的线程在同步队列中等待,若有,去等待;若没有,才允许去抢占。
ReentrantLock 、synchronized 和 volatile 比较?
synchronized是互斥同步的一种实现。
synchronized:当某个线程访问被synchronized标记的方法或代码块时,这个线程便获得了该对象的锁,其他线暂时无法访问这个方法,只有等待这个方法执行完毕或代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法代码块。
可以看到被synchronized同步的代码块,会在前后分别加上monitorenter和monitorexit,这两个字节码都需要指定加锁和解锁的对象。
关于加锁和解锁的对象:
synchronized代码块 :同步代码块,作用范围是整个代码块,作用对象是调用这个代码块的对象。
synchronized方法 :同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。
synchronized静态方法 :同步静态方法,作用范围是整个静态方法,作用对象是调用这个类的所有对象。
synchronized(this):作用范围是该对象中所有被synchronized标记的变量、方法或代码块,作用对象是对象本身。
synchronized(ClassName.class) :作用范围是静态的方法或者静态变量,作用对象是Class对象。
synchronized(this)添加的是对象锁,synchronized(ClassName.class)添加的是类锁,它们的区别如下:
- 对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
- 类锁:对象锁是用来控制实例方法之间的同步,类锁是来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有个互斥锁,而类的静态方法是需要Class对象。所以所谓类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。类锁和对象锁不是同一个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:一个线程访问静态sychronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,为他们需要的锁是不同的。
造成死锁的四个条件:
-
互斥条件:一个资源每次只能被一个线程使用。
-
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
-
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
CAS介绍?
Unsafe
Unsafe是CAS的核心类。因为Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
CAS
CAS,Compare and Swap即比较并交换,设计并发算法时常用到的一种技术,java.util.concurrent包全完建立在CAS之上,没有CAS也就没有此包,可见CAS的重要性。当前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。并且CAS也是通过Unsafe实现的,由于CAS都是硬件级别的操作,因此效率会比普通加锁高一些。
CAS的缺点
CAS看起来很美,但这种操作显然无法涵盖并发下的所有场景,并且CAS从语义上来说也不是完美的,存在这样一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个漏洞称为CAS操作的"ABA"问题。java.util.concurrent包为了解决这个问题,提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。不过目前来说这个类比较"鸡肋",大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,使用传统的互斥同步可能回避原子类更加高效。
进程和线程的区别?
简而言之,一个程序至少有一个进程,一个进程至少有一个线程。
-
线程的划分尺度小于进程,使得多线程程序的并发性高。
-
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
-
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
-
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
-
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
-
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
什么导致线程阻塞?
线程的阻塞
为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持.
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。
sleep() 方法:sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。
上述的核心区别导致了一系列的细节上的区别。
首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用 任意对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。
wait() 和 notify() 方法的上述特性决定了它们经常和synchronized 方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。(此外,线程间通信的方式还有多个线程通过synchronized关键字这种方式来实现线程间的通信、while轮询、使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信的管道通信)。
关于 wait() 和 notify() 方法最后再说明两点:
第一:调用 notify() 方法导致解除阻塞的线程是从调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。
以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。
乐观锁与悲观锁。
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
使用场景
乐观锁适用于写比较少的情况下(多读场景),而一般多写的场景下用悲观锁就比较合适。
乐观锁常见的两种实现方式
1、版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
2、CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 一般情况下是一个自旋操作,即不断的重试。
乐观锁的缺点
1、ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
JDK 1.5 以后的 AtomicStampedReference 类一定程度上解决了这个问题,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2、自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
3、CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
怎么安全停止一个线程任务?原理是什么?线程池里有类似机制吗?
终止线程
1、使用violate boolean变量退出标志,使线程正常退出,也就是当run方法完成后线程终止。(推荐)
2、使用interrupt()方法中断线程,但是线程不一定会终止。
3、使用stop方法强行终止线程。不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。
终止线程池
ExecutorService线程池就提供了shutdown和shutdownNow这样的生命周期方法来关闭线程池自身以及它拥有的所有线程。
1、shutdown关闭线程池
线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
2、shutdownNow关闭线程池并中断任务
终止等待执行的线程,并返回它们的列表。试图停止所有正在执行的线程,试图终止的方法是调用Thread.interrupt(),但是大家知道,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
Java的线程模型
多对一模型、一对一模型、多对多模型
多对一模型
多对一线程模型,又叫作用户级线程模型,即多个用户线程对应到同一个内核线程上,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。
优点:
- 用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换,使线程的创建、调度、同步等非常快;
缺点:
-
由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么该其他用户线程也无法执行;
-
内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度、优先级等;
一对一模型
一对一模型,又叫作内核级线程模型,即一个用户线程对应一个内核线程,内核负责每个线程的调度,可以调度到其他处理器上面。
优点:
- 实现简单
缺点:
-
对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换;
-
内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响;
Java使用的就是一对一线程模型,所以在Java中启一个线程要谨慎。
多对多模型
多对多模型,又叫作两级线程模型,它是博采众长之后的产物,充分吸收前两种线程模型的优点且尽量规避它们的缺点。
在此模型下,用户线程与内核线程是多对多(m : n,通常m>=n)的映射模型。
首先,区别于多对一模型,多对多模型中的一个进程可以与多个内核线程关联,于是进程内的多个用户线程可以绑定不同的内核线程,这点和一对一模型相似;
其次,又区别于一对一模型,它的进程里的所有用户线程并不与内核线程一一绑定,而是可以动态绑定内核线程, 当某个内核线程因为其绑定的用户线程的阻塞操作被内核调度让出CPU时,其关联的进程中其余用户线程可以重新与其他内核线程绑定运行。
所以,多对多模型既不是多对一模型那种完全靠自己调度的也不是一对一模型完全靠操作系统调度的,而是中间态(自身调度与系统调度协同工作),因为这种模型的高度复杂性,操作系统内核开发者一般不会使用,所以更多时候是作为第三方库的形式出现。
优点:
-
兼具多对一模型的轻量;
-
由于对应了多个内核线程,则一个用户线程阻塞时,其他用户线程仍然可以执行;
-
由于对应了多个内核线程,则可以实现较完整的调度、优先级等;
缺点:
- 实现复杂
Go语言中的goroutine调度器就是采用的这种实现方案,在Go语言中一个进程可以启动成千上万个goroutine,这也是其出道以来就自带“高并发”光环的重要原因。
ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。 ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
ThreadLocal与Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本
,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
双线程通过线程同步的方式打印12121212...
Android部分
什么是ANR 如何避免它?
-
主线程被IO操作(从4.0之后网络IO不允许在主线程中)阻塞。
-
主线程中存在耗时的计算
-
主线程中错误的操作,比如Thread.wait或者Thread.sleep等 Android系统会监控程序的响应状况,一旦出现下面两种情况,则弹出ANR对话框
-
应用在5秒内未响应用户的输入事件(如按键或者触摸)
-
BroadcastReceiver未在10秒内完成相关的处理
-
Service在特定的时间内无法处理完成 20秒
Activity和Fragment生命周期有哪些?
android中进程的优先级?
1. 前台进程:
即与用户正在交互的Activity或者Activity用到的Service等,如果系统内存不足时前台进程是最晚被杀死的
2. 可见进程:
可以是处于暂停状态(onPause)的Activity或者绑定在其上的Service,即被用户可见,但由于失了焦点而不能与用户交互
3. 服务进程:
其中运行着使用startService方法启动的Service,虽然不被用户可见,但是却是用户关心的,例如用户正在非音乐界面听的音乐或者正在非下载页面下载的文件等;当系统要空间运行,前两者进程才会被终止
4. 后台进程:
其中运行着执行onStop方法而停止的程序,但是却不是用户当前关心的,例如后台挂着的QQ,这时的进程系统一旦没了有内存就首先被杀死
5. 空进程:
不包含任何应用程序的进程,这样的进程系统是一般不会让他存在的
Context相关
-
1、Activity和Service以及Application的Context是不一样的,Activity继承自ContextThemeWraper.其他的继承自ContextWrapper。
-
2、每一个Activity和Service以及Application的Context是一个新的ContextImpl对象。
-
3、getApplication()用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那也许在绝大多数情况下我们都是在Activity或者Servic中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法,getApplicationContext()比getApplication()方法的作用域会更广一些,任何一个Context的实例,只要调用getApplicationContext()方法都可以拿到我们的Application对象。
-
4、创建对话框时不可以用Application的context,只能用Activity的context。
-
5、Context的数量等于Activity的个数 + Service的个数 +1,这个1为Application。
BroadcastReceiver广播的基本原理
1.广播接收者BroadcastReceiver通过Binder机制向AMS(Activity Manager Service)进行注册;
2.广播发送者通过binder机制向AMS发送广播;
3.AMS查找符合相应条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到BroadcastReceiver(一般情况下是Activity)相应的消息循环队列中;
4.消息循环执行拿到此广播,回调BroadcastReceiver中的onReceive()方法。
由此看来,广播发送者和广播接收者分别属于观察者模式中的消息发布和订阅两端,AMS属于中间的处理中心。广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有无接收者接收,也不确定接收者到底是何时才能接收到
Handler机制
Android消息循环流程图如下所示:
主要涉及的角色如下所示:
- message:消息。
- MessageQueue:消息队列,负责消息的存储与管理,负责管理由 Handler 发送过来的 Message。读取会自动删除消息,单链表维护,插入和删除上有优势。在其next()方法中会无限循环,不断判断是否有消息,有就返回这条消息并移除。
- Looper:消息循环器,负责关联线程以及消息的分发,在该线程下从 MessageQueue获取 Message,分发给Handler,Looper创建的时候会创建一个 MessageQueue,调用loop()方法的时候消息循环开始,其中会不断调用messageQueue的next()方法,当有消息就处理,否则阻塞在messageQueue的next()方法中。当Looper的quit()被调用的时候会调用messageQueue的quit(),此时next()会返回null,然后loop()方法也就跟着退出。
- Handler:消息处理器,负责发送并处理消息,面向开发者,提供 API,并隐藏背后实现的细节。
整个消息的循环流程还是比较清晰的,具体说来:
- 1、Handler通过sendMessage()发送消息Message到消息队列MessageQueue。
- 2、Looper通过loop()不断提取触发条件的Message,并将Message交给对应的target handler来处理。
- 3、target handler调用自身的handleMessage()方法来处理Message。
事实上,在整个消息循环的流程中,并不只有Java层参与,很多重要的工作都是在C++层来完成的。我们来看下这些类的调用关系。
注:虚线表示关联关系,实线表示调用关系。
在这些类中MessageQueue是Java层与C++层维系的桥梁,MessageQueue与Looper相关功能都通过MessageQueue的Native方法来完成,而其他虚线连接的类只有关联关系,并没有直接调用的关系,它们发生关联的桥梁是MessageQueue。
总结
-
Handler 发送的消息由 MessageQueue 存储管理,并由 Looper 负责回调消息到 handleMessage()。
-
线程的转换由 Looper 完成,handleMessage() 所在线程由 Looper.loop() 调用者所在线程决定。
Handler 引起的内存泄露原因以及最佳解决方案
Handler 允许我们发送延时消息,如果在延时期间用户关闭了 Activity,那么该 Activity 会泄露。 这个泄露是因为 Message 会持有 Handler,而又因为 Java 的特性,内部类会持有外部类,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。
解决:将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,并在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)及时移除所有消息。
为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ?
通常我们认为 ActivityThread 就是主线程。事实上它并不是一个线程,而是主线程操作的管理者。在 ActivityThread.main() 方法中调用了 Looper.prepareMainLooper() 方法创建了 主线程的 Looper ,并且调用了 loop() 方法,所以我们就可以直接使用 Handler 了。
因此我们可以利用 Callback 这个拦截机制来拦截 Handler 的消息。如大部分插件化框架中Hook ActivityThread.mH 的处理。
主线程的 Looper 不允许退出
主线程不允许退出,退出就意味 APP 要挂。
Handler 里藏着的 Callback 能干什么?
Handler.Callback 有优先处理消息的权利 ,当一条消息被 Callback 处理并拦截(返回 true),那么 Handler 的 handleMessage(msg) 方法就不会被调用了;如果 Callback 处理了消息,但是并没有拦截,那么就意味着一个消息可以同时被 Callback 以及 Handler 处理。
创建 Message 实例的最佳方式
为了节省开销,Android 给 Message 设计了回收机制,所以我们在使用的时候尽量复用 Message ,减少内存消耗:
- 通过 Message 的静态方法 Message.obtain();
- 通过 Handler 的公有方法 handler.obtainMessage()。
子线程里弹 Toast 的正确姿势
本质上是因为 Toast 的实现依赖于 Handler,按子线程使用 Handler 的要求修改即可,同理的还有 Dialog。
妙用 Looper 机制
- 将 Runnable post 到主线程执行;
- 利用 Looper 判断当前线程是否是主线程。
主线程的死循环一直运行是不是特别消耗CPU资源呢?
并不是,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质是同步I/O,即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
handler postDelay这个延迟是怎么实现的?
handler.postDelay并不是先等待一定的时间再放入到MessageQueue中,而是直接进入MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。
一个线程可以有几个Handler,几个Looper,几个MessageQueue对象
一个线程可以有多个Handler,只有一个Looper对象,只有一个MessageQueue对象。Looper.prepare()函数中知
道,。在Looper的prepare方法中创建了Looper对象,并放入到ThreadLocal中,并通过ThreadLocal来获取looper
的对象, ThreadLocal的内部维护了一个ThreadLocalMap类,ThreadLocalMap是以当前thread做为key的,因此可以得
知,一个线程最多只能有一个Looper对象, 在Looper的构造方法中创建了MessageQueue对象,并赋值给mQueue
字段。因为Looper对象只有一个,那么Messagequeue对象肯定只有一个。
内存泄露,怎样查找,怎么产生的内存泄露
Java Heap 泄漏监控
Java Heap 泄漏监控:它利用 Copy-on-write 机制 fork 子进程 dump Java Heap,解决了 dump 过程中 app 长时间冻结的问题。 不同与LeakCanary和Matrix Resource Canary,koom-java-leak模块实现的内存泄漏监控并不是通过弱引用或者ReferenceQueue来实现的,而是通过检测内存大小的变化来实现的,如果多次检测内存仍然处于逐步增长状态或者超过预定阈值,则会触发内存分析,进行内存泄漏检测
Native Heap 泄漏监控
不同于Java,在Native层主要使用C或者C++来进行编码,语言本身并没有垃圾回收机制,对于对象的回收依赖于开发者手动释放空间,这也就意味着在native层进行泄漏检测相对而言更加困难。不过在Android N(7.0)以后系统新增了libmemunreachable模块,该模块是一个零开销的本地内存泄漏检测器,其使用不精确的标记-清除垃圾回收器遍历所有Native内存,并将不可访问的内存块报告为泄漏内存区域。基于libmemunreachable我们可以设计一套机制用于监控Native层内存泄漏问题,主要原理如下:
- hook malloc/free 等内存分配器方法,用于记录 Native 内存分配元数据「大小、堆栈、地址等」
- 周期性的使用 mark-and-sweep 分析整个进程 Native Heap,获取不可达的内存块信息「地址、大小」
- 利用不可达的内存块的地址、大小等从我们记录的元数据中获取其分配堆栈,产出泄漏数据「不可达内存块地址、大小、分配堆栈等」
Native层内存泄漏对象 = 不可达的内存块信息
线程泄漏监控
- 用xhook hook pthread_create,pthread_detach,pthread_join,pthread_exit这四个线程操作的核心方法 ,用于记录线程的生命周期和创建堆栈,名称等信息
- 当发现一个joinable的线程在没有detach或者join的情况下,执行了pthread_exit,则记录下泄露线程信息
- 当线程泄露时间到达配置设置的延迟期限的时候,上报线程泄露信息,在线程detach和join时,会判断线程状态,将其设置为detach=true的状态,也就意味着针对一个线程而言,如果其没有执行detach或者join直接执行exit则会判定为线程泄漏。
- pthread有两种状态joinable状态(属性)和unjoinable状态,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符。只有当你调用了pthread_join之后这些资源才会被释放。若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit时自动会被释放。
- unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach自己, 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。或者将线程置为 joinable,然后适时调用pthread_join.
- 其实简单的说就是在线程函数头加上 pthread_detach(pthread_self())的话,线程状态改变,在函数尾部直接 pthread_exit线程就会自动退出。省去了给线程擦屁股的麻烦。
- pthread_exit实际就类似于进程的exit,线程会直接退出, 而其资源不会释放.
类的初始化顺序依次是?
(静态变量、静态代码块)>(变量、代码块)>构造方法
Recyclerview缓存机制原理
RecyclerView的缓存分为四级
-
Scrap
-
Cache
-
ViewCacheExtension
-
RecycledViewPool
Scrap对应ListView 的Active View,就是屏幕内的缓存数据,就是相当于换了个名字,可以直接拿来复用。
Cache 刚刚移出屏幕的缓存数据,默认大小是2个,当其容量被充满同时又有新的数据添加的时候,会根据FIFO原则
ViewCacheExtension是google留给开发者自己来自定义缓存的,现阶段Recycler并没有将任何的view缓存到ViewCacheExtension中。所以在ViewCacheExtension中并没有缓存任何数据
RecycledViewPool Cache中移除出队的ViewHolder移出会缓存到RecycledViewPool中,RecycledViewPool默认的缓存数量是5个。RecycledViewPool是根据itemType获取的
Bitmap 内存分配策略
Bitmap 在内存中的组成部分,在任何系统版本中都会存在以下 3 个部分:
- 1、Java Bitmap 对象: 位于 Java 堆,即我们熟悉的
android.graphics.Bitmap.java; - 2、Native Bitmap 对象: 位于 Native 堆,以
Bitmap.cpp为代表,除此之外还包括与 Skia 引擎相关的 SkBitmap、SkBitmapInfo 等一系列对象; - 3、图片像素数据: 图片解码后得到的像素数据。
其中,Java Bitmap 对象和 Native Bitmap 对象是分别存储在 Java 堆和 Native 堆的,毋庸置疑。唯一有操作性的是 3、图片像素数据,不同系统版本采用了不同的分配策略,分为 3 个历史时期:
- 时期 1 - Android 3.0 以前: 像素数据存放在 Native 堆;
- 时期 2 - Android 8.0 以前: 从 Android 3.0 到 Android 7.1,像素数据存放在 Java 堆;
- 时期 3 - Android 8.0 以后: 从 Android 8.0 开始,像素数据重新存放在 Native 堆。另外还新增了 Hardware Bitmap 硬件位图,可以减少图片内存分配并提高绘制效率。
不同版本的 Bitmap 内存回收兜底策略
Java Bitmap 对象提供了 recycle() 方法主动释放内存资源。然而, 由于 Native 内存不属于 Java 虚拟机垃圾收集管理的区域,如果不手动调用 recycle() 方法释放资源,即使 Java Bitmap 对象被垃圾回收,位于 Native 层的 Native Bitmap 对象和图片像素数据也不会被回收的。 为了避免 Native 层内存泄漏,Bitmap 内部增加了兜底策略,分为 2 个历史时期:
- 1、Finalizer 机制: 在最初的版本,Bitmap 依赖于 Java Finalizer 机制辅助 Native 内存。Java Finalizer 机制提供了一个在对象被回收之前释放资源的时机,不过 Finalizer 机制是不稳定甚至危险的,所以后续保证 Google 修改了辅助方案;
- 2、引用机制: Android 7.0 开始,开始使用
NativeAllocationRegistry工具类辅助回收内存。NativeAllocationRegistry 本质上是虚引用的工具类,利用了引用类型感知 Java 对象垃圾回收时机的特性。引用机制相对于 Finalizer 机制更稳定。
| 分配策略 | 回收兜底策略 | |
|---|---|---|
| Android 7.0 以前 | Java 堆 | Finalizer 机制 |
| Android 7.0 / Android 7.1 | Java 堆 | 引用机制 |
| Android 8.0 以后 | Native 堆 / 硬件 | 引用机制 |
创建内存分配
recycle流程
是否有必要主动调用 recycle()?
需要。 Finalizer 机制和引用机制的定位是清晰明确的,它们都是 Bitmap 用来辅助回收内存的兜底策略。虽然从 Finalizer 机制升级到引用机制后稳定性略有提升,或者将来从引用机制升级到某个更优秀的机制,不管怎么升级,兜底策略永远是兜底策略,它永远不会也不能替换主要策略: 在不需要使用资源时立即释放资源。Glide 内部的 Bitmap 缓存池在清除缓存时,会主动调用 recycle() 吗?看源码:
LruBitmapPool.java
// 已简化
private synchronized void trimToSize(long size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
currentSize -= strategy.getSize(removed);
// 主动调用 recycle()
removed.recycle();
}
}
Bitmap像素点如何计算
ARGB_8888:(1像素占 4 byte)
ARGB_4444:(1像素占 2 byte)
RGB_565:(1像素占 2 byte)
ALPHA_8:(1像素占 1 byte)
一张bitmap的大小 = 有多少个像素点 * 每个像素点内存中占用的大小 = 长 * 宽 * 对应的像素点占用的比特位
Bitmap中有专门获取占用内存大小的方法
getAllocationByteCount()//API 19
getByteCount()//API 12
在低版本中用一行的字节x高度 bitmap.getRowBytes() * bitmap.getHeight();
Bitmap 使用时候注意什么?
1、要选择合适的图片规格(bitmap类型):
ALPHA_8 每个像素占用1byte内存
ARGB_4444 每个像素占用2byte内存
ARGB_8888 每个像素占用4byte内存(默认)
RGB_565 每个像素占用2byte内存
2、降低采样率。BitmapFactory.Options 参数inSampleSize的使用,先把options.inJustDecodeBounds设为true,只是去读取图片的大小,在拿到图片的大小之后和要显示的大小做比较通过calculateInSampleSize()函数计算inSampleSize的具体值,得到值之后。options.inJustDecodeBounds设为false读图片资源。
3、复用内存。即,通过软引用(内存不够的时候才会回收掉),复用内存块,不需要再重新给这个bitmap申请一块新的内存,避免了一次内存的分配和回收,从而改善了运行效率。
4、使用recycle()方法及时回收内存。
5、压缩图片。
Oom 是否可以try catch ?
只有在一种情况下,这样做是可行的:
在try语句中声明了很大的对象,导致OOM,并且可以确认OOM是由try语句中的对象声明导致的,那么在catch语句中,可以释放掉这些对象,解决OOM的问题,继续执行剩余语句。
但是这通常不是合适的做法。
Java中管理内存除了显式地catch OOM之外还有更多有效的方法:比如SoftReference, WeakReference, 硬盘缓存等。 在JVM用光内存之前,会多次触发GC,这些GC会降低程序运行的效率。 如果OOM的原因不是try语句中的对象(比如内存泄漏),那么在catch语句中会继续抛出OOM
安卓的动画显示原理
Property Animation
属性动画的优点
- 属性动画顾名思义就是改变了View的属性,而不仅仅是绘制的位置。
- 属性动画可以操作的属性相比于补间动画大大增加,除了常用的平移、旋转、缩放、透明度还有颜色等,基本上能通过View.setXX来设置的属性,属性动画都可以操作,这大大增加了我们在使用动画时的灵活性。
- 属性动画分为ObjectAnimator和ValueAnimator,其中ObjectAnimator是继承于ValueAnimator。
PropertyAnimation流程图
在start()->startAnimation()方法中,有个addAnimationCallback()方法
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}
public AnimationHandler getAnimationHandler() {
return mAnimationHandler != null ? mAnimationHandler : AnimationHandler.getInstance();
}
流程执行到了AnimationHandler
public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
public static AnimationHandler getInstance() {
if (sAnimatorHandler.get() == null) {
sAnimatorHandler.set(new AnimationHandler());
}
return sAnimatorHandler.get();
}
这里ThreadLocal的作用的存储的对象在当前Thread中,内部的实现是通过Thread的字段ThreadLocalMap来存储对应的值,保证存储的内容只供当前Thread使用。
AnimationHandler实例创建后,执行addAnimationFrameCallback(),这里方法内部最终其实是调用了Choreographer.postFrameCallback(callback)通过Choreographer注册每一帧的绘制回调
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
当屏幕刷新信号到的时候,Choreographer的doFram()会将带执行队列里的工作取出来执行,此时就回调到了将mFrameCallback的doFrame()方法,再这个方法里面完成AnimationFrameCallback的doAnimationFrame()以及commitAnimationFrame();具体的的实现逻辑在ValueAnimaotor的doAnimationFrame和commitAnimationFrame完成。
ValueAnimator和ObjectAnimator区别:
原理上:
ValueAnimator类是先改变值,然后手动赋值给对象的属性从而实现动画;是间接对对象属性进行操作;
ObjectAnimator类是先改变值,然后自动赋值给对象的属性从而实现动画;是直接对对象属性进行操作。
两者类之间的区别: 其实二者都是属于属性动画,本质上是一样的,都是先改 变值,然后赋值给对象属性,从而实现动画操作。 但二者区别就在于,ValueAnimator类 是 手动 赋值给对象的属性,从而实现动画,是间接对对象属性进行操作。 ValueAnimator 类本质上是一种 改变 值 的操作机制。而ObjectAnimator类,是 自动 赋值给对象的属性,从而实现动画操作。是直接对对象属 性进行操作。可以理解为ObjectAnimator类更加智能,自动化程度更高。
属性动画的内存泄露
-
上面讲述到
ValueAnimator.AnimationHandler.doAnimationFrame的时候说过,这个方法会循环执行。 -
因为
ValueAnimator.AnimationHandler.doAnimationFrame每次执行完动画(如果动画没有结束),都在再一次请求Vsync同步信号回调给自己。 -
Choreographer的回调都配post进入了当前线程的looper队列中。 -
mRepeatCount无穷大,会导致该循环会一直执行下去,即使关闭当前的页面也不会停止。
Drawable Animation
DrawableAnimation流程图
也就是所谓的帧动画,Frame动画。指通过指定每一帧的图片和播放时间,有序的进行播放而形成动画效果。
内存方面
帧动画相比较属性动画而言可能会出现OOM,因为在家的每一帧的图片会占用很大的内存空间。帧动画不会出现内存泄露的问题:
public abstract class Drawable {
/***部分代码省略***/
//持有当前View的弱引用,当View回收之后,没办法继续下一帧的展示
private WeakReference<Callback> mCallback = null;
public Callback getCallback() {
if (mCallback != null) {
return mCallback.get();
}
return null;
}
}
Tween Animation
TweenAnimation流程图
视图动画,也就是所谓补间动画,只是在视图层实现了动画效果,并没有真正改变View的属性。在每一次VSYN到来时,调用ViewRootImpl performTraversals, 在View的draw方法里面 根据当前时间计算动画进度 计算出一个需要变换的Transformation矩阵 然后最终设置到canvas上去 调用canvas concat做矩阵变换.
RootViewImpl 的初始化
View 的三大流程都是通过 RootViewImpl 来完成的,在 ActivityThread 中,当 Activity 对象被创建完毕后,在 onResume 后,就会通过 WindowManager 将 DecorView 添加到窗口上,在这个过程中会创建 ViewRootImpl:
ActivityThread.handleResumeActivity
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
// .....
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
//设置窗口类型为应用类型
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//添加 decor 到 window 中
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
//....
}
WindowManagerImpl.addView
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
WindowManagerGlobal.addView
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//检查参数是否合法
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
//.....
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
//创建 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//将 Window 所对应的 View,ViewRootImp,params 顺序添加到列表中,这一步是为了方便更新和删除 View
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
//把 Window 对应的 View 设置给创建的 ViewRootImpl
//通过 ViewRootImpl 来更新界面并添加到 WIndow中。
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
Android 渲染机制
Android 系统采用一种称为 Surface 的UI 架构为应用程序提供用户界面。
在Android 应用程序中,每一个Activity 组件都关联有一个或者若干个窗口,每一个窗口都对应有一个Surface 。有了这个Surface之后,应用程序就可以在上面渲染窗口的UI。
最终这些已经绘制好了的Surface 都会被统一提交给Surface 管理服务 SurfaceFlinger 进行合成,最后显示在屏幕上面。
无论是应用程序,还是SurfaceFlinger ,都可以利用GPU 等硬件来进行UI渲染,以获得更流畅的UI.
总结: Android 应用程序调用SurfaceFlinger 服务把经过测量、布局和绘制后的Surface 渲染到显示屏幕上。
重要成员:
-
ViewRootImpl: 用来控制窗口的渲染,以及用来与WindowManagerService 、SurfaceFlinger 通信。
-
WindowManager : WindowManager 会控制窗口对象,它们是用于容纳视图对象的容器。窗口对象始终由Surface 对象提供支持。WindowManager 会监督生命周期、输入和聚焦事件、屏幕方向、转换、动画、位置、变形、Z轴顺序以及窗口的许多其他方面。
WindowManager 会将所有窗口元数据发送到SurfaceFlinger ,以便SurfaceFlinger 可以使用这些数据在屏幕上合成Surface.
-
Surface : Andriod 应用的每个窗口对应一个画布(Canvas) ,即Surface ,可以理解为 Android 应用程序的一个窗口。Surface 是一个接口,供生产方与使用方交换缓冲区。
-
SurfaceView : SurfaceView 是一个组件,可用于在View 层次结构中嵌入其他合成层。SurfaceView 采用与其他 View 相同的布局参数,因此可以像对待其他任何 View 一样对其进行操作,但 SurfaceView 的内容是透明的。当 SurfaceView 的 View 组件即将变得可见时,框架会要求 SurfaceControl 从 SurfaceFlinger 请求新的 Surface。
-
BufferQueue:BufferQueue 类将可生成图形数据缓冲区的组件(生产方)连接到接受数据以便进行显示或进一步处理的组件(使用方)。几乎所有在系统中移动图形数据缓冲区的内容都依赖于 BufferQueue。
-
SurfaceFlinger:Android 系统服务,负责管理 Android 系统的帧缓冲区,即显示屏幕。 EGLSurface 和 OpenGL ES:OpenGL ES (GLES) 定义了用于与 EGL 结合使用的图形渲染 API。EGI 是一个规定如何通过操作系统创建和访问窗口的库(要绘制纹理多边形,请使用 GLES 调用;要将渲染放到屏幕上,请使用 EGL 调用)。
-
Vulkan:Vulkan 是一种用于高性能 3D 图形的低开销、跨平台 API。与 OpenGL ES 一样,Vulkan 提供用于在应用中创建高质量实时图形的工具。
Canvas.save()跟Canvas.restore()的调用时机
save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。
restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
save和restore要配对使用(restore可以比save少,但不能多),如果restore调用次数比save多,会引发Error。save和restore操作执行的时机不同,就能造成绘制的图形不同。
编译期注解跟运行时注解
运行期注解(RunTime)利用反射去获取信息还是比较损耗性能的,对应@Retention(RetentionPolicy.RUNTIME)。
编译期(Compile time)注解,以及处理编译期注解的手段APT和Javapoet,对应@Retention(RetentionPolicy.CLASS)。 其中apt+javaPoet目前也是应用比较广泛,在一些大的开源库,如EventBus3.0+,页面路由 ARout、Dagger、Retrofit等均有使用的身影,注解不仅仅是通过反射一种方式来使用,也可以使用APT在编译期处理
广播传输的数据是否有限制,是多少,为什么要限制?
Intent在传递数据时是有大小限制的,大约限制在1MB-8K,你用Intent传递数据,实际上走的是跨进程通信(IPC),跨进程通信需要把数据从内核copy到进程中,每一个进程有一个接收内核数据的缓冲区,默认是1M;如果一次传递的数据超过限制,就会出现异常。
不同厂商表现不一样有可能是厂商修改了此限制的大小,也可能同样的对象在不同的机器上大小不一样。
传递大数据,不应该用Intent;考虑使用ContentProvider或者直接匿名共享内存。简单情况下可以考虑分段传输。
Fragment状态保存
Fragment状态保存入口:
1、Activity的状态保存, 在Activity的onSaveInstanceState()里, 调用了FragmentManger的saveAllState()方法, 其中会对mActive中各个Fragment的实例状态和View状态分别进行保存.
2、FragmentManager还提供了public方法: saveFragmentInstanceState(), 可以对单个Fragment进行状态保存, 这是提供给我们用的。
3、FragmentManager的moveToState()方法中, 当状态回退到ACTIVITY_CREATED, 会调用saveFragmentViewState()方法, 保存View的状态.
为什么bindService可以跟Activity生命周期联动?
1、bindService 方法执行时,LoadedApk 会记录 ServiceConnection 信息。
2、Activity 执行 finish 方法时,会通过 LoadedApk 检查 Activity 是否存在未注销/解绑的 BroadcastReceiver 和 ServiceConnection,如果有,那么会通知 AMS 注销/解绑对应的 BroadcastReceiver 和 Service,并打印异常信息,告诉用户应该主动执行注销/解绑的操作。
广播注册一般有几种,各有什么优缺点?
第一种是常驻型(静态注册):当应用程序关闭后如果有信息广播来,程序也会被系统调用,自己运行。
第二种不常驻(动态注册):广播会跟随程序的生命周期。
动态注册
优点: 在android的广播机制中,动态注册优先级高于静态注册优先级,因此在必要情况下,是需要动态注册广播接收者的。
缺点: 当用来注册的 Activity 关掉后,广播也就失效了。
静态注册
优点: 无需担忧广播接收器是否被关闭,只要设备是开启状态,广播接收器就是打开着的。
服务启动一般有几种,服务和activty之间怎么通信,服务和服务之间怎么通信
方式:
1、startService:
onCreate()--->onStartCommand() ---> onDestory()
如果服务已经开启,不会重复的执行onCreate(), 而是会调用onStartCommand()。一旦服务开启跟调用者(开启者)就没有任何关系了。 开启者退出了,开启者挂了,服务还在后台长期的运行。 开启者不能调用服务里面的方法。
2、bindService:
onCreate() --->onBind()--->onunbind()--->onDestory()
bind的方式开启服务,绑定服务,调用者挂了,服务也会跟着挂掉。 绑定者可以调用服务里面的方法。
通信:
1、通过Binder对象。
2、通过broadcast(广播)。
AndroidManifest的作用与理解
AndroidManifest.xml文件,也叫清单文件,来获知应用中是否包含该组件,如果有会直接启动该组件。可以理解是一个应用的配置文件。
作用:
- 为应用的 Java 软件包命名。软件包名称充当应用的唯一标识符。
- 描述应用的各个组件,包括构成应用的 Activity、服务、广播接收器和内容提供程序。它还为实现每个组件的类命名并发布其功能,例如它们可以处理的 Intent - 消息。这些声明向 Android 系统告知有关组件以及可以启动这些组件的条件的信息。
- 确定托管应用组件的进程。
- 声明应用必须具备哪些权限才能访问 API 中受保护的部分并与其他应用交互。还声明其他应用与该应用组件交互所需具备的权限
- 列出 Instrumentation类,这些类可在应用运行时提供分析和其他信息。这些声明只会在应用处于开发阶段时出现在清单中,在应用发布之前将移除。
- 声明应用所需的最低 Android API 级别
- 列出应用必须链接到的库
LaunchMode应用场景
standard,创建一个新的Activity。
singleTop,栈顶不是该类型的Activity,创建一个新的Activity。否则,onNewIntent。
singleTask,回退栈中没有该类型的Activity,创建Activity,否则,onNewIntent+ClearTop。
注意:
设置了"singleTask"启动模式的Activity,它在启动的时候,会先在系统中查找属性值affinity等于它的属性值taskAffinity的Task存在;如果存在这样的Task,它就会在这个Task中启动,否则就会在新的任务栈中启动。因此, 如果我们想要设置了"singleTask"启动模式的Activity在新的任务中启动,就要为它设置一个独立的taskAffinity属性值。
如果设置了"singleTask"启动模式的Activity不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的Activity实例, 如果存在,就会把位于这个Activity实例上面的Activity全部结束掉,即最终这个Activity 实例会位于任务的Stack顶端中。
在一个任务栈中只有一个”singleTask”启动模式的Activity存在。他的上面可以有其他的Activity。这点与singleInstance是有区别的。
singleInstance,回退栈中,只有这一个Activity,没有其他Activity。
singleTop适合接收通知启动的内容显示页面。 例如,某个新闻客户端的新闻内容页面,如果收到10个新闻推送,每次都打开一个新闻内容页面是很烦人的。
singleTask适合作为程序入口点。 例如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
singleInstance应用场景: 闹铃的响铃界面。
MVP,MVVM,MVC解释和实践
MVC:
- 视图层(View) 对应于xml布局文件和java代码动态view部分
- 控制层(Controller) MVC中Android的控制层是由Activity来承担的,Activity本来主要是作为初始化页面,展示数据的操作,但是因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。
- 模型层(Model) 针对业务模型,建立数据结构和相关的类,它主要负责网络请求,数据库处理,I/O的操作。
总结
具有一定的分层,model彻底解耦,controller和view并没有解耦 层与层之间的交互尽量使用回调或者去使用消息机制去完成,尽量避免直接持有 controller和view在android中无法做到彻底分离,但在代码逻辑层面一定要分清 业务逻辑被放置在model层,能够更好的复用和修改增加业务。
MVP
通过引入接口BaseView,让相应的视图组件如Activity,Fragment去实现BaseView,实现了视图层的独立,通过中间层Preseter实现了Model和View的完全解耦。MVP彻底解决了MVC中View和Controller傻傻分不清楚的问题,但是随着业务逻辑的增加,一个页面可能会非常复杂,UI的改变是非常多,会有非常多的case,这样就会造成View的接口会很庞大。
MVVM
MVP中我们说过随着业务逻辑的增加,UI的改变多的情况下,会有非常多的跟UI相关的case,这样就会造成View的接口会很庞大。而MVVM就解决了这个问题,通过双向绑定的机制,实现数据和UI内容,只要想改其中一方,另一方都能够及时更新的一种设计理念,这样就省去了很多在View层中写很多case的情况,只需要改变数据就行。
MVVM与DataBinding的关系?
MVVM是一种思想,DataBinding是谷歌推出的方便实现MVVM的工具。
看起来MVVM很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图属性的修改导致。如果项目中打算用MVVM的话可以考虑使用官方的架构组件ViewModel、LiveData、DataBinding去实现MVVM。
三者如何选择?
-
如果项目简单,没什么复杂性,未来改动也不大的话,那就不要用设计模式或者架构方法,只需要将每个模块封装好,方便调用即可,不要为了使用设计模式或架构方法而使用。
-
对于偏向展示型的app,绝大多数业务逻辑都在后端,app主要功能就是展示数据,交互等,建议使用mvvm。
-
对于工具类或者需要写很多业务逻辑app,使用mvp或者mvvm都可。
SharedPrefrences的apply和commit有什么区别?
这两个方法的区别在于:
-
apply没有返回值而commit返回boolean表明修改是否提交成功。
-
apply是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘, 而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内容,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。
-
apply方法不会提示任何失败的提示。 由于在一个进程中,sharedPreference是单实例,一般不会出现并发冲突,如果对提交的结果不关心的话,建议使用apply,当然需要确保提交成功且有后续操作的话,还是需要用commit的。
BroadcastReceiver,LocalBroadcastReceiver 区别?
1、应用场景
1、BroadcastReceiver用于应用之间的传递消息;
2、而LocalBroadcastManager用于应用内部传递消息,比broadcastReceiver更加高效。
2、安全
1、BroadcastReceiver使用的Content API,所以本质上它是跨应用的,所以在使用它时必须要考虑到不要被别的应用滥用;
2、LocalBroadcastManager不需要考虑安全问题,因为它只在应用内部有效。
3、原理方面
(1) 与BroadcastReceiver是以 Binder 通讯方式为底层实现的机制不同,LocalBroadcastManager 的核心实现实际还是 Handler,只是利用到了 IntentFilter 的 match 功能,至于 BroadcastReceiver 换成其他接口也无所谓,顺便利用了现成的类和概念而已。
(2) LocalBroadcastManager因为是 Handler 实现的应用内的通信,自然安全性更好,效率更高。
如何保证Service不被杀死?
Android 进程不死从3个层面入手:
A.提供进程优先级,降低进程被杀死的概率
方法一:监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素的 Activity,在用户解锁时将 Activity 销毁掉。
方法二:启动前台service。
方法三:提升service优先级:
在AndroidManifest.xml文件中对于intent-filter可以通过android:priority = "1000"这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时适用于广播。
B. 在进程被杀死后,进行拉活
方法一:注册高频率广播接收器,唤起进程。如网络变化,解锁屏幕,开机等
方法二:双进程相互唤起。
方法三:依靠系统唤起。
方法四:onDestroy方法里重启service:service + broadcast 方式,就是当service走ondestory的时候,发送一个自定义的广播,当收到广播的时候,重新启动service;
C. 依靠第三方
ContentProvider、ContentResolver、ContentObserver 之间的关系?
ContentProvider:管理数据,提供数据的增删改查操作,数据源可以是数据库、文件、XML、网络等,ContentProvider为这些数据的访问提供了统一的接口,可以用来做进程间数据共享。
ContentResolver:ContentResolver可以为不同URI操作不同的ContentProvider中的数据,外部进程可以通过ContentResolver与ContentProvider进行交互。
ContentObserver:观察ContentProvider中的数据变化,并将变化通知给外界。
HandlerThread
1、HandlerThread原理
当系统有多个耗时任务需要执行时,每个任务都会开启个新线程去执行耗时任务,这样会导致系统多次创建和销毁线程,从而影响性能。为了解决这一问题,Google提出了HandlerThread,HandlerThread本质上是一个线程类,它继承了Thread。HandlerThread有自己的内部Looper对象,可以进行loopr循环。通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage()方法中执行异步任务。创建HandlerThread后必须先调用HandlerThread.start()方法,Thread会先调用run方法,创建Looper对象。当有耗时任务进入队列时,则不需要开启新线程,在原有的线程中执行耗时任务即可,否则线程阻塞。它在Android中的一个具体的使用场景是IntentService。由于HanlderThread的run()方法是一个无限循环,因此当明确不需要再使用HandlerThread时,可以通过它的quit或者quitSafely方法来终止线程的执行。
2、HanlderThread的优缺点
-
HandlerThread优点是异步不会堵塞,减少对性能的消耗。
-
HandlerThread缺点是不能同时继续进行多任务处理,要等待进行处理,处理效率较低。
-
HandlerThread与线程池不同,HandlerThread是一个串队列,背后只有一个线程
IntentService
IntentService是一种特殊的Service,它继承了Service并且它是一个抽象类,因此必须创建它的子类才能使用IntentService。
原理
在实现上,IntentService封装了HandlerThread和Handler。当IntentService被第一次启动时,它的onCreate()方法会被调用,onCreat()方法会创建一个HandlerThread,然后使用它的Looper来构造一个Handler对象mServiceHandler,这样通过mServiceHandler发送的消息最终都会在HandlerThread中执行。
生成一个默认的且与主线程互相独立的工作者线程来执行所有传送至onStartCommand()方法的Intetnt。
生成一个工作队列来传送Intent对象给onHandleIntent()方法,同一时刻只传送一个Intent对象,这样一来,你就不必担心多线程的问题。在所有的请求(Intent)都被执行完以后会自动停止服务,所以,你不需要自己去调用stopSelf()方法来停止。
该服务提供了一个onBind()方法的默认实现,它返回null。
提供了一个onStartCommand()方法的默认实现,它将Intent先传送至工作队列,然后从工作队列中每次取出一个传送至onHandleIntent()方法,在该方法中对Intent做相应的处理。
为什么在mServiceHandler的handleMessage()回调方法中执行完onHandlerIntent()方法后要使用带参数的stopSelf()方法?
因为stopSel()方法会立即停止服务,而stopSelf(int startId)会等待所有的消息都处理完毕后才终止服务,一般来说,stopSelf(int startId)在尝试停止服务之前会判断最近启动服务的次数是否和startId相等,如果相等就立刻停止服务,不相等则不停止服务。
显示Intent与隐式Intent的区别
对明确指出了目标组件名称的Intent,我们称之为“显式Intent”。
对于没有明确指出目标组件名称的Intent,则称之为“隐式 Intent”。
对于隐式意图,在定义Activity时,指定一个intent-filter,当一个隐式意图对象被一个意图过滤器进行匹配时,将有三个方面会被参考到:
动作(Action)
类别(Category ['kætɪg(ə)rɪ] )
数据(Data )
App稳定性优化
App启动速度优化
App内存优化
如何避免内存抖动
App绘制优化
App瘦身
为什么WebView加载会慢呢?
这是因为在客户端中,加载H5页面之前,需要先初始化WebView,在WebView完全初始化完成之前,后续的界面加载过程都是被阻塞的。
优化手段围绕着以下两个点进行:
- 预加载WebView。
- 加载WebView的同时,请求H5页面数据。
因此常见的方法是:
- 全局WebView。
- 客户端代理页面请求。WebView初始化完成后向客户端请求数据。
- asset存放离线包。
除此之外还有一些其他的优化手段:
- 脚本执行慢,可以让脚本最后运行,不阻塞页面解析。
- DNS链接慢,可以让客户端复用使用的域名与链接。
- React框架代码执行慢,可以将这部分代码拆分出来,提前进行解析。
Android Framework
Android系统架构
Android 是一种基于 Linux 的开放源代码软件栈,为广泛的设备和机型而创建。下图所示为 Android 平台的五大组件:
1.应用程序
2、Java API 框架
您可通过以 Java 语言编写的 API 使用 Android OS 的整个功能集。这些 API 形成创建 Android 应用所需的构建块,它们可简化核心模块化系统组件和服务的重复使用,包括以下组件和服务:
- 丰富、可扩展的视图系统,可用以构建应用的 UI,包括列表、网格、文本框、按钮甚至可嵌入的网络浏览器
- 资源管理器,用于访问非代码资源,例如本地化的字符串、图形和布局文件
- 通知管理器,可让所有应用在状态栏中显示自定义提醒
- Activity 管理器,用于管理应用的生命周期,提供常见的导航返回栈
- 内容提供程序,可让应用访问其他应用(例如“联系人”应用)中的数据或者共享其自己的数据
开发者可以完全访问 Android 系统应用使用的框架 API。
3、系统运行库
1)原生 C/C++ 库
许多核心 Android 系统组件和服务(例如 ART 和 HAL)构建自原生代码,需要以 C 和 C++ 编写的原生库。Android 平台提供 Java 框架 API 以向应用显示其中部分原生库的功能。例如,您可以通过 Android 框架的 Java OpenGL API 访问 OpenGL ES,以支持在应用中绘制和操作 2D 和 3D 图形。如果开发的是需要 C 或 C++ 代码的应用,可以使用 Android NDK 直接从原生代码访问某些原生平台库。
2)Android Runtime
对于运行 Android 5.0(API 级别 21)或更高版本的设备,每个应用都在其自己的进程中运行,并且有其自己的 Android Runtime (ART) 实例。ART 编写为通过执行 DEX 文件在低内存设备上运行多个虚拟机,DEX 文件是一种专为 Android 设计的字节码格式,经过优化,使用的内存很少。编译工具链(例如 Jack)将 Java 源代码编译为 DEX 字节码,使其可在 Android 平台上运行。
ART 的部分主要功能包括:
- 预先 (AOT) 和即时 (JIT) 编译
- 优化的垃圾回收 (GC)
- 更好的调试支持,包括专用采样分析器、详细的诊断异常和崩溃报告,并且能够设置监视点以监控特定字段
在 Android 版本 5.0(API 级别 21)之前,Dalvik 是 Android Runtime。如果您的应用在 ART 上运行效果很好,那么它应该也可在 Dalvik 上运行,但反过来不一定。
Android 还包含一套核心运行时库,可提供 Java API 框架使用的 Java 编程语言大部分功能,包括一些 Java 8 语言功能。
4、硬件抽象层 (HAL)
硬件抽象层 (HAL) 提供标准界面,向更高级别的 Java API 框架显示设备硬件功能。HAL 包含多个库模块,其中每个模块都为特定类型的硬件组件实现一个界面,例如相机或蓝牙模块。当框架 API 要求访问设备硬件时,Android 系统将为该硬件组件加载库模块。
5、Linux 内核
Android 平台的基础是 Linux 内核。例如,Android Runtime (ART) 依靠 Linux 内核来执行底层功能,例如线程和低层内存管理。使用 Linux 内核可让 Android 利用主要安全功能,并且允许设备制造商为著名的内核开发硬件驱动程序。
Android 系统启动流程:
第一步:手机开机后,引导芯片启动,引导芯片开始从固化在
ROM里的预设代码执行,加载引导程序到到RAM,bootloader检
查RAM,初始化硬件参数等功能;
第二步:硬件等参数初始化完成后,进入到Kernel层,Kernel层
主要加载一些硬件设备驱动,初始化进程管理等操作。在Kernel
中首先启动swapper进程(pid=0),用于初始化进程管理、内管
管理、加载Driver等操作,再启动kthread进程(pid=2),这些linux
享学课堂系统的内核进程,kthread是所有内核进程的鼻祖;
第三步:Kernel层加载完毕后,硬件设备驱动与HAL层进行交互。
初始化进程管理等操作会启动INIT进程 ,这些在Native层中;
第四步:init进程(pid=1,init进程是所有进程的鼻祖,第一个启
动)启动后,会启动adbd,logd等用户守护进程,并且会启动
servicemanager(binder服务管家)等重要服务,同时孵化出
zygote进程,这里属于C++ Framework,代码为C++程序;
第五步:zygote进程是由init进程解析init.rc文件后fork生成,它
会加载虚拟机,启动System Server(zygote孵化的第一个进程);
System Server负责启动和管理整个Java Framework,包含
ActivityManager,WindowManager,PackageManager,
PowerManager等服务;
第六步:zygote同时会启动相关的APP进程,它启动的第一个APP
进程为Launcher,然后启动Email,SMS等进程,所有的APP进程
都有zygote fork生成。
Android 的启动原理
Activity跨进程启动流程:
点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;
system_server进程接收到请求后,向zygote进程发送创建进程的请求;
Zygote进程fork出新的子进程,即App进程;
App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;
system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进 程发送
scheduleLaunchActivity请求;
App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。
Activity进程内启动
请求进程A:startActivity—(hook插入点1) (AMP,ActivityManager代理对象)——>
system_server进程:AMS(ActivityManagerService)
解析Activity信息、处理启动参数、scheduleLaunchActivity/mH中EXECUTE_TRANSACTION消息处理(Android P)-->
回到请求进程A:ApplicationThread --> ActivityThread -(hook插入点2)-> Activity生命周期
Binder通信原理
角色:Server端A、Client端B、Binder驱动、内核空间、物理内存
- Binder驱动在物理内存中开辟一块固定大小(1M-8K)的物理内存w,与内核空间的虚拟地址x进行映射得到
- A的用户空间的虚拟地址ax和物理内存w进行映射
- 此时内核空间虚拟地址x和物理内存w已经进行了映射,物理内存w和Server端A的用户空间虚拟地址ax进行了映射:也就是 内核空间的虚拟地址x = 物理内存w = Server端A的用户空间虚拟地址ax
- B发送请求:将数据按照binder协议进行打包给到Binder驱动,Binder驱动调用
coay_from_user()将数据拷贝到内核空间的虚拟地址x - 因步骤3中的三块区域进行了映射
- Server端A就得到了Client端B发送的数据
- 通过内存映射关系,只发生了一次拷贝
Activity跳转时,最多携带1M-8k(1兆减去8K)的数据量;
真实数据大小为:1M内存-两页的请求头数据=1M-8K;
应用A直接将数据拷贝到应用B的物理内存空间中,数据量不能超过1M-8K;拷贝次数少了一次,少了从服务端拷贝到用户;
View的事件分发机制?滑动冲突怎么解决?
触摸事件对应的是MotionEvent类,事件的类型主要有如下三种:
- ACTION_DOWN
- ACTION_MOVE(移动的距离超过一定的阈值会被判定为ACTION_MOVE操作)
- ACTION_UP
View事件分发本质就是对MotionEvent事件分发的过程。即当一个MotionEvent发生后,系统将这个点击事件传递到一个具体的View上。
事件分发流程
事件分发过程由三个方法共同完成:
dispatchTouchEvent:方法返回值为true表示事件被当前视图消费掉;返回为super.dispatchTouchEvent表示继续分发该事件,返回为false表示交给父类的onTouchEvent处理。
onInterceptTouchEvent:方法返回值为true表示拦截这个事件并交由自身的onTouchEvent方法进行消费;返回false表示不拦截,需要继续传递给子视图。如果return super.onInterceptTouchEvent(ev), 事件拦截分两种情况:
- 1.如果该View存在子View且点击到了该子View, 则不拦截, 继续分发 给子View 处理, 此时相当于return false。
- 2.如果该View没有子View或者有子View但是没有点击中子View(此时ViewGroup 相当于普通View), 则交由该View的onTouchEvent响应,此时相当于return true。
注意:一般的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup默认不拦截, 而 ScrollView、ListView等ViewGroup则可能拦截,得看具体情况。
onTouchEvent:方法返回值为true表示当前视图可以处理对应的事件;返回值为false表示当前视图不处理这个事件,它会被传递给父视图的onTouchEvent方法进行处理。如果return super.onTouchEvent(ev),事件处理分为两种情况:
- 1.如果该View是clickable或者longclickable的,则会返回true, 表示消费 了该事件, 与返回true一样;
- 2.如果该View不是clickable或者longclickable的,则会返回false, 表示不 消费该事件,将会向上传递,与返回false一样。
在Android系统中,拥有事件传递处理能力的类有以下三种:
- Activity:拥有分发和消费两个方法。
- ViewGroup:拥有分发、拦截和消费三个方法。
- View:拥有分发、消费两个方法。
对应一个根ViewGroup来说,点击事件产生后,首先会传递给它,这是它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,这时如果它的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。在onTouchEvent中,如果设置了mOnCLickListener,则onClick会被调用。只要View的CLICKABLE和LONG_CLICKABLE有一个为true,onTouchEvent()就会返回true消耗这个事件。如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。
一些重要的结论:
1、事件传递优先级:onTouchListener.onTouch > onTouchEvent > onClickListener.onClick。
2、正常情况下,一个时间序列只能被一个View拦截且消耗。因为一旦一个元素拦截了此事件,那么同一个事件序列内的所有事件都会直接交给它处理(即不会再调用这个View的拦截方法去询问它是否要拦截了,而是把剩余的ACTION_MOVE、ACTION_DOWN等事件直接交给它来处理)。特例:通过将重写View的onTouchEvent返回false可强行将事件转交给其他View处理。
3、如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
4、ViewGroup默认不拦截任何事件(返回false)。
5、View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable默认为false。
6、View的enable属性不影响onTouchEvent的默认返回值。
7、通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
ACTION_CANCEL什么时候触发,触摸button然后滑动到外部抬起会触发点击事件吗,再滑动回去抬起会么?
- 一般ACTION_CANCEL和ACTION_UP都作为View一段事件处理的结束。如果在父View中拦截ACTION_UP或ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件。
- 如果触摸某个控件,但是又不是在这个控件的区域上抬起(移动到别的地方了),就会出现action_cancel。
滑动冲突的处理规则:
- 对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
- 对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部View拦截事件,何时由内部View拦截事件。
- 对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。
滑动冲突的实现方法:
- 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。
- 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合requestDisallowInterceptTouchEvent方法。
View的绘制流程?
DecorView被加载到Window中
- 从Activity的startActivity开始,最终调用到ActivityThread的handleLaunchActivity方法来创建Activity,首先,会调用performLaunchActivity方法,内部会执行Activity的onCreate方法,从而完成DecorView和Activity的创建。然后,会调用handleResumeActivity,里面首先会调用performResumeActivity去执行Activity的onResume()方法,执行完后会得到一个ActivityClientRecord对象,然后通过r.window.getDecorView()的方式得到DecorView,然后会通过a.getWindowManager()得到WindowManager,最终调用其addView()方法将DecorView加进去。
- WindowManager的实现类是WindowManagerImpl,它内部会将addView的逻辑委托给WindowManagerGlobal,可见这里使用了接口隔离和委托模式将实现和抽象充分解耦。在WindowManagerGlobal的addView()方法中不仅会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView通过root.setView()把DecorView加载到Window中。这里的ViewRootImpl是ViewRoot的实现类,是连接WindowManager和DecorView的纽带。View的三大流程均是通过ViewRoot来完成的。
了解绘制的整体流程
绘制会从根视图ViewRoot的performTraversals()方法开始,从上到下遍历整个视图树,每个View控件负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作。
理解MeasureSpec
MeasureSpec表示的是一个32位的整形值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来说明应该如何测量这个View。它由三种测量模式,如下:
- EXACTLY:精确测量模式,视图宽高指定为match_parent或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下View的测量值就是SpecSize的值。
- AT_MOST:最大值测量模式,当视图的宽高指定为wrap_content时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。
- UNSPECIFIED:不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法,打包方法为makeMeasureSpec,解包方法为getMode和getSize。
对于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。
View绘制流程之Measure
-
首先,在ViewGroup中的measureChildren()方法中会遍历测量ViewGroup中所有的View,当View的可见性处于GONE状态时,不对其进行测量。
-
然后,测量某个指定的View时,根据父容器的MeasureSpec和子View的LayoutParams等信息计算子View的MeasureSpec。
-
最后,将计算出的MeasureSpec传入View的measure方法,这里ViewGroup没有定义测量的具体过程,因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去实现。不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法。(setMeasureDimension方法用于设置View的测量宽高,如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得View的宽高)
Draw的基本流程
绘制基本上可以分为六个步骤:
- 首先绘制View的背景;
- 如果需要的话,保持canvas的图层,为fading做准备;
- 然后,绘制View的内容;
- 接着,绘制View的子View;
- 如果需要的话,绘制View的fading边缘并恢复图层;
- 最后,绘制View的装饰(例如滚动条等等)。
setWillNotDraw的作用
如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。
-
默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。
-
当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。
-
当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显示地关闭WILL_NOT_DRAW这个标记位。
Requestlayout,onlayout,onDraw,DrawChild区别与联系?
requestLayout()方法 :会导致调用 measure()过程 和 layout()过程,将会根据标志位判断是否需要ondraw。
onLayout()方法:如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局。
onDraw()方法:绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)。
drawChild():去重新回调每个子视图的draw()方法。
invalidate() 和 postInvalidate()的区别 ?
invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。
Android三方库原理
OkHttp
- OkHttp 提供了对最新的 HTTP 协议版本 HTTP/2 和 SPDY 的支持,这使得对同一个主机发出的所有请求都可以共享相同的套接字连接。
- 如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率。
- OkHttp 提供了对 GZIP 的默认支持来降低传输内容的大小。
- OkHttp 也提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求。
- 当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址。
核心实现原理
OkHttp内部的请求流程:使用OkHttp会在请求的时候初始化一个Call的实例,然后执行它的execute()方法或enqueue()方法,内部最后都会执行到getResponseWithInterceptorChain()方法,这个方法里面通过拦截器组成的责任链,依次经过用户自定义普通拦截器、重试拦截器、桥接拦截器、缓存拦截器、连接拦截器和用户自定义网络拦截器以及访问服务器拦截器等拦截处理过程,来获取到一个响应并交给用户。其中,除了OKHttp的内部请求流程这点之外,缓存和连接这两部分内容也是两个很重要的点,掌握了这3点就说明你理解了OkHttp。
各个拦截器的作用:
- interceptors:用户自定义拦截器
- retryAndFollowUpInterceptor:负责失败重试以及重定向
- BridgeInterceptor:请求时,对必要的Header进行一些添加,接收响应时,移除必要的Header
- CacheInterceptor:负责读取缓存直接返回(根据请求的信息和缓存的响应的信息来判断是否存在缓存可用)、更新缓存
- ConnectInterceptor:负责和服务器建立连接
ConnectionPool:
1、判断连接是否可用,不可用则从ConnectionPool获取连接,ConnectionPool无连接,创建新连接,握手,放入ConnectionPool。
2、它是一个Deque,add添加Connection,使用线程池负责定时清理缓存。
3、使用连接复用省去了进行 TCP 和 TLS 握手的一个过程。
-
networkInterceptors:用户定义网络拦截器
-
CallServerInterceptor:负责向服务器发送请求数据、从服务器读取响应数据
Retrofit
核心实现原理
Retrofit主要是在create方法中采用动态代理模式(通过访问代理对象的方式来间接访问目标对象)实现接口方法,这个过程构建了一个ServiceMethod对象,根据方法注解获取请求方式,参数类型和参数注解拼接请求的链接,当一切都准备好之后会把数据添加到Retrofit的RequestBuilder中。然后当我们主动发起网络请求的时候会调用okhttp发起网络请求,okhttp的配置包括请求方式,URL等在Retrofit的RequestBuilder的build()方法中实现,并发起真正的网络请求。
1、创建Retrofit实例:
- 使用建造者模式通过内部Builder类建立了一个Retroift实例。
- 网络请求工厂使用了工厂方法模式。
2、创建网络请求接口的实例:
- 首先,使用外观模式统一调用创建网络请求接口实例和网络请求参数配置的方法。
- 然后,使用动态代理动态地去创建网络请求接口实例。
- 接着,使用了建造者模式 & 单例模式创建了serviceMethod对象。
- 再者,使用了策略模式对serviceMethod对象进行网络请求参数配置,即通过解析网络请求接口方法的参数、返回值和注解类型,从Retrofit对象中获取对应的网络的url地址、网络请求执行器、网络请求适配器和数据转换器。
- 最后,使用了装饰者模式ExecuteCallBack为serviceMethod对象加入线程切换的操作,便于接受数据后通过Handler从子线程切换到主线程从而对返回数据结果进行处理。
3、发送网络请求:
- 在异步请求时,通过静态delegate代理对网络请求接口的方法中的每个参数使用对应的ParameterHanlder进行解析。
4、解析数据
5、切换线程:
-
使用了适配器模式通过检测不同的Platform使用不同的回调执行器,然后使用回调执行器切换线程,这里同样是使用了装饰模式。
Glide
核心实现原理
- Glide&with:
1、初始化各式各样的配置信息(包括缓存,请求线程池,大小,图片格式等等)以及glide对象。
2、将glide请求和application/SupportFragment/Fragment的生命周期绑定在一块。
- Glide&load:
设置请求url,并记录url已设置的状态。
3、Glide&into:
1、首先根据转码类transcodeClass类型返回不同的ImageViewTarget:BitmapImageViewTarget、DrawableImageViewTarget。
2、递归建立缩略图请求,没有缩略图请求,则直接进行正常请求。
3、如果没指定宽高,会根据ImageView的宽高计算出图片宽高,最终执行到onSizeReay()方法中的engine.load()方法。
4、engine是一个负责加载和管理缓存资源的类
- 常规三级缓存的流程:强引用->软引用->硬盘缓存
当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找(软引用适合当cache,当内存吃紧的时候才会被回收。而weakReference在每次system.gc()就会被回收)(当LruCache存储紧张时,会把最近最少使用的数据放到SoftReference中),如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去硬盘缓存中中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到硬盘缓存中,然后放到LruCache中。
- Glide的三层缓存机制:
Glide缓存机制大致分为三层:内存缓存、弱引用缓存、磁盘缓存。
取的顺序是:内存、弱引用、磁盘。
存的顺序是:弱引用、内存、磁盘。
三层存储的机制在Engine中实现的。先说下Engine是什么?Engine这一层负责加载时做管理内存缓存的逻辑。持有MemoryCache、Map<Key, WeakReference<EngineResource<?>>>。通过load()来加载图片,加载前后会做内存存储的逻辑。如果内存缓存中没有,那么才会使用EngineJob这一层来进行异步获取硬盘资源或网络资源。EngineJob类似一个异步线程或observable。Engine是一个全局唯一的,通过Glide.getEngine()来获取。
需要一个图片资源,如果Lrucache中有相应的资源图片,那么就返回,同时从Lrucache中清除,放到activeResources中。activeResources map是盛放正在使用的资源,以弱引用的形式存在。同时资源内部有被引用的记录。如果资源没有引用记录了,那么再放回Lrucache中,同时从activeResources中清除。如果Lrucache中没有,就从activeResources中找,找到后相应资源引用加1。如果Lrucache和activeResources中没有,那么进行资源异步请求(网络/diskLrucache),请求成功后,资源放到diskLrucache和activeResources中。
Glide源码机制的核心思想:
使用一个弱引用map activeResources来盛放项目中正在使用的资源。Lrucache中不含有正在使用的资源。资源内部有个计数器来显示自己是不是还有被引用的情况,把正在使用的资源和没有被使用的资源分开有什么好处呢??因为当Lrucache需要移除一个缓存时,会调用resource.recycle()方法。注意到该方法上面注释写着只有没有任何consumer引用该资源的时候才可以调用这个方法。那么为什么调用resource.recycle()方法需要保证该资源没有任何consumer引用呢?glide中resource定义的recycle()要做的事情是把这个不用的资源(假设是bitmap或drawable)放到bitmapPool中。bitmapPool是一个bitmap回收再利用的库,在做transform的时候会从这个bitmapPool中拿一个bitmap进行再利用。这样就避免了重新创建bitmap,减少了内存的开支。而既然bitmapPool中的bitmap会被重复利用,那么肯定要保证回收该资源的时候(即调用资源的recycle()时),要保证该资源真的没有外界引用了。这也是为什么glide花费那么多逻辑来保证Lrucache中的资源没有外界引用的原因。
加载bitmap过程(怎样保证不产生内存溢出)
由于Android对图片使用内存有限制,若是加载几兆的大图片便内存溢出。Bitmap会将图片的所有像素(即长x宽)加载到内存中,如果图片分辨率过大,会直接导致内存OOM,只有在BitmapFactory加载图片时使用BitmapFactory.Options对相关参数进行配置来减少加载的像素。
BitmapFactory.Options相关参数详解:
(1).Options.inPreferredConfig值来降低内存消耗。
比如:默认值ARGB_8888改为RGB_565,节约一半内存。
(2).设置Options.inSampleSize 缩放比例,对大图片进行压缩 。
(3).设置Options.inPurgeable和inInputShareable:让系统能及时回收内存。
A:inPurgeable:设置为True时,表示系统内存不足时可以被回收,设置为False时,表示不能被回收。
B:inInputShareable:设置是否深拷贝,与inPurgeable结合使用,inPurgeable为false时,该参数无意义。
(4).使用decodeStream代替decodeResource等其他方法
Android中软引用与弱引用的应用场景
在 Android 应用的开发中,为了防止内存溢出,在处理一些占用内存大而且生命周期较长的对象时候,可以尽量应用软引用和弱引用技术。
- 1、软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软 / 弱引用。
- 2、如果只是想避免 OOM 异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
- 3、可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
Android里的内存缓存和磁盘缓存是怎么实现的。
内存缓存基于LruCache实现,磁盘缓存基于DiskLruCache实现。这两个类都基于Lru算法和LinkedHashMap来实现。
LRU算法可以用一句话来描述,如下所示:
LRU是Least Recently Used的缩写,最近最少使用算法,从它的名字就可以看出,它的核心原则是如果一个数据在最近一段时间没有使用到,那么它在将来被访问到的可能性也很小,则这类数据项会被优先淘汰掉。
LruCache原理
之前,我们会使用内存缓存技术实现,也就是软引用或弱引用,在Android 2.3(APILevel 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。
其实LRU缓存的实现类似于一个特殊的栈,把访问过的元素放置到栈顶(若栈中存在,则更新至栈顶;若栈中不存在则直接入栈),然后如果栈中元素数量超过限定值,则删除栈底元素(即最近最少使用的元素)。
它的内部存在一个 LinkedHashMap 和 maxSize,把最近使用的对象用强引用存储在 LinkedHashMap 中,给出来 put 和 get 方法,每次 put 图片时计算缓存中所有图片的总大小,跟 maxSize 进行比较,大于 maxSize,就将最久添加的图片移除,反之小于 maxSize 就添加进来。
LruCache的原理就是利用LinkedHashMap持有对象的强引用,按照Lru算法进行对象淘汰。具体说来假设我们从表尾访问数据,在表头删除数据,当访问的数据项在链表中存在时,则将该数据项移动到表尾,否则在表尾新建一个数据项。当链表容量超过一定阈值,则移除表头的数据。
详细来说就是LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队头元素,即近期最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队尾。
LruCache put方法核心逻辑
在添加过缓存对象后,调用trimToSize()方法,来判断缓存是否已满,如果满了就要删除近期最少使用的对象。trimToSize()方法不断地删除LinkedHashMap中队头的元素,即近期最少访问的,直到缓存大小小于最大值(maxSize)。
LruCache get方法核心逻辑
当调用LruCache的get()方法获取集合中的缓存对象时,就代表访问了一次该元素,将会更新队列,保持整个队列是按照访问顺序排序的。
为什么会选择LinkedHashMap呢?
这跟LinkedHashMap的特性有关,LinkedHashMap的构造函数里有个布尔参数accessOrder,当它为true时,LinkedHashMap会以访问顺序为序排列元素,否则以插入顺序为序排序元素。
LinkedHashMap原理
LinkedHashMap 几乎和 HashMap 一样:从技术上来说,不同的是它定义了一个 Entry<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。
EventBus
EventBus的观察者模式和一般的观察者模式不同,它使用了扩展的观察者模式对事件进行订阅和分发,其实这里的扩展就是指的使用了EventBus来作为中介者,抽离了许多职责,如下是它的官方原理图:
在得知了EventBus的原理之后,我们注意到,每次我们在register之后,都必须进行一次unregister,这是为什么呢?
因为register是强引用,它会让对象无法得到内存回收,导致内存泄露。所以必须在unregister方法中释放对象所占的内存。
EventBus2.x与EventBus3.x区别
- 1、EventBus2.x使用的是运行时注解,它采用了反射的方式对整个注册的类的所有方法进行扫描来完成注册,因而会对性能有一定影响。
- 2、EventBus3.x使用的是编译时注解,Java文件会编译成.class文件,再对class文件进行打包等一系列处理。在编译成.class文件时,EventBus会使用EventBusAnnotationProcessor注解处理器读取@Subscribe()注解并解析、处理其中的信息,然后生成Java类来保存所有订阅者的订阅信息。这样就创建出了对文件或类的索引关系,并将其编入到apk中。
- 3、从EventBus3.0开始使用了对象池缓存减少了创建对象的开销。
原理分析图:
pag动画
-
创建画布PAGSurface(java)->PAGSurface(native)
-
创建播放器PAGPlayer(java)->PAGPlayer(native)->PAGStage(native)->RenderCache(native)
-
PAGStage 渲染画布,用来承载pag文件解析后的pagComposition对象
-
RenderCache是渲染缓存,用来防止多次计算的缓存系统。
-
绑定PAGSurface到播放器PAGPlayer上
-
设置播放源
void PAGPlayer::setComposition(std::shared_ptr<PAGComposition> newComposition) {
LockGuard autoLock(rootLocker);
auto pagComposition = stage->getRootComposition();
if (pagComposition == newComposition) {
return;
}
if (pagComposition) {
auto index = stage->getLayerIndexInternal(pagComposition);
if (index >= 0) {
stage->doRemoveLayer(index);
}
delete reporter;
reporter = nullptr;
}
pagComposition = newComposition;
if (pagComposition) {
stage->doAddLayer(pagComposition, 0);
reporter = FileReporter::Make(pagComposition).release();
updateScaleModeIfNeed();
}
}
最核心的代码就是stage->doAddLayer(pagComposition, 0);了。即把PagComposition设置到了stage的Layer的最底层。这个PAGComposition就是PagFile,PAGComposition本质也是PagLayer。所以设置播放源其实也就是把PagFile设置到了PagPlayer的PagStage的最底层。
- 渲染线程播放
libpag里面内置了一套用于2D图形渲染的引擎叫做TGFX,用来替换skia自研实现的。
lottie
- json文件解析:
总体原理
- Lottie 先将动画 JSON 文件转换为 LottieComposition 数据对象。
- 继承 ImageView 的 LottieAnimationView 将数据对象 LottieComposition 和渲染能力委托给 LottieDrawable 处理。
- 在 LottieDrawable 中会将数据对象 LottieComposition 组建为具有 draw 能力的 BaseLayer,并在 LottieAnimationView 需要绘制时,调用自己和各个层级 BaseLayer 的渲染,从而达到动画效果。
适配原理
- 对于普通图片,可能会需要2x,3x多份图片资源进行手机适配。
- 而Lottie本身已经自带了适配的功能:解析json文件时,读取动画的宽、高之后,会乘以手机的密度。在使用的时候判断Lottie动画适配后的宽高是否大于手机实际宽高,如果大于,Lottie会进行缩放。
绘制原理
1、 加载并解析文件为LottieComposition数据对象
2、 将LottieComposition设置给LottieDrawable
3、 LottieDrawable通过buildComposition方法构造最外层的CompositionLayer。
4、构造CompositionLayer时,遍历LottieComposition数据对象中所有的Layer数据,并将其转换为BaseLayer图层对象。
CompositionLayer与其他BaseLayer的关系类似于ViewGroup与View的关系。
5、 BaseLayer 的 draw 绘制过程中,会调用抽象方法 drawLayer,各个继承的子类会具体实现。
6、 如果Composition包含多个子图层,会遍历子图层,调用各自的draw方法进行绘制。
动画原理
-
使用LottieAnimationView.playAnimaiton方法可以执行动画。
-
在animator执行过程中会逐层回调setprogress方法。
-
最终触发lottieDrawable的invalidateSelf方法,使lottieDrawable重新绘制。
-
这样随着animator的进行,lottieDrawable重新绘制,最终形成完整的动画。
LeakCanary
核心实现原理
主要分为如下7个步骤:
- 1、RefWatcher.watch()创建了一个KeyedWeakReference用于去观察对象。
- 2、然后,在后台线程中,它会检测引用是否被清除了,并且是否没有触发GC。
- 3、如果引用仍然没有被清除,那么它将会把堆栈信息保存在文件系统中的.hprof文件里。
- 4、HeapAnalyzerService被开启在一个独立的进程中,并且HeapAnalyzer使用了HAHA开源库解析了指定时刻的堆栈快照文件heap dump。
- 5、从heap dump中,HeapAnalyzer根据一个独特的引用key找到了KeyedWeakReference,并且定位了泄露的引用。
- 6、HeapAnalyzer为了确定是否有泄露,计算了到GC Roots的最短强引用路径,然后建立了导致泄露的链式引用。
- 7、这个结果被传回到app进程中的DisplayLeakService,然后一个泄露通知便展现出来了。
简单来说就是:
在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与ReferenceQueque关联。这时再从ReferenceQueque中查看是否有该对象,如果没有,执行gc,再次查看,还是没有的话则判断发生内存泄露了。最后用HAHA这个开源库去分析dump之后的heap内存(主要就是创建一个HprofParser解析器去解析出对应的引用内存快照文件snapshot)。
流程图:
BlockCanary原理
该组件利用了主线程的消息队列处理机制,应用发生卡顿,一定是在dispatchMessage中执行了耗时操作。我们通过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,如果超出阈值,表示发生卡顿,则dump出各种信息,提供开发者分析性能瓶颈
ARouter路由原理:
ARouter维护了一个路由表Warehouse,其中保存着全部的模块跳转关系,ARouter路由跳转实际上还是调用了startActivity的跳转,使用了原生的Framework机制,只是通过apt注解的形式制造出跳转规则,并人为地拦截跳转和设置跳转条件。
热修复
Android中ClassLoader的种类&特点
-
BootClassLoader(Java的BootStrap ClassLoader): 用于加载Android Framework层class文件。
-
PathClassLoader(Java的App ClassLoader): 用于加载已经安装到系统中的apk中的class文件。
-
DexClassLoader(Java的Custom ClassLoader): 用于加载指定目录中的class文件。
-
BaseDexClassLoader: 是PathClassLoader和DexClassLoader的父类。
1、类加载方案:
65536限制:
65536的主要原因是DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用65535个方法。
LinearAlloc限制:
- DVM中的LinearAlloc是一个固定的缓存区,当方法数超过了缓存区的大小时会报错。
Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态地加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。
加载流程:
- 根据dex文件的查找流程,我们将有Bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在Bug的Key.class,排在数组后面的dex文件中存在Bug的Key.class根据ClassLoader的双亲委托模式就不会被加载。
类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?
- 这是因为类是无法被卸载的,要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。
各个热修复框架的实现细节差异:
- QQ空间的超级补丁和Nuwa是按照上面说的将补丁包放在Element数组的第一个元素得到优先加载。
- 微信的Tinker将新旧APK做了diff,得到path.dex,再将patch.dex与手机中APK的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Elements数组的第一个元素。
- 饿了么的Amigo则是将补丁包中每个dex对应的Elements取出来,之后组成新的Element数组,在运行时通过反射用新的Elements数组替换掉现有的Elements数组。
2、底层替换方案:
当我们要反射Key的show方法,会调用Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());,最终会在native层将传入的javaMethod在ART虚拟机中对应一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等。
替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。
AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容性问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。
Sophix采用的是替换整个ArtMethod结构体,这样不会存在兼容问题。
底层替换方案直接替换了方法,可以立即生效不需要重启。采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。
3、Instant Run方案:
ASM是一个java字节码操控框架,它能够动态生成类或者增强现有类的功能。ASM可以直接产生class文件,也可以在类被加载到虚拟机之前动态改变类的行为。
Instant Run在第一次构建APK时,使用ASM在每一个方法中注入了类似的代码逻辑:当change不为null时,则调用它的accesschange不为null时,则调用它的accesschange不为null时,则调用它的accessdispatch方法,参数为具体的方法名和方法参数。当MainActivity的onCreate方法做了修改,就会生成替换类MainActivityoverride,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的change设置为MainActivityoverride。最后这个override。最后这个override。最后这个change就不会为null,则会执行MainActivityoverride的accessoverride的accessoverride的accessdispatch方法,最终会执行onCreate方法,从而实现了onCreate方法的修改
借鉴Instant Run原理的热修复框架有Robust和Aceso。
4、动态链接库修复:
重新加载so。
加载so主要用到了System类的load和loadLibrary方法,最终都会调用到nativeLoad方法。其会调用JavaVMExt的LoadNativeLibrary函数来加载so。
so修复主要有两个方案:
- 1、将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回和加载。
- 2、调用System的load方法来接管so的加载入口。
插件化原理:
Activity插件化:
主要实现方式有三种:
- 反射:对性能有影响,主流的插件化框架没有采用此方式。
- 接口:dynamic-load-apk采用。
- Hook:主流。
Hook实现方式有两种:Hook IActivityManager和Hook Instrumentation。主要方案就是先用一个在AndroidManifest.xml中注册的Activity来进行占坑,用来通过AMS的校验,接着在合适的时机用插件Activity替换占坑的Activity。
Hook IActivityManager:
1、占坑、通过校验:
在Android 7.0和8.0的源码中IActivityManager借助了Singleton类实现单例,而且该单例是静态的,因此IActivityManager是一个比较好的Hook点。
接着,定义替换IActivityManager的代理类IActivityManagerProxy,由于Hook点IActivityManager是一个接口,建议这里采用动态代理。
- 拦截startActivity方法,获取参数args中保存的Intent对象,它是原本要启动插件TargetActivity的Intent。
- 新建一个subIntent用来启动StubActivity,并将前面得到的TargetActivity的Intent保存到subIntent中,便于以后还原TargetActivity。
- 最后,将subIntent赋值给参数args,这样启动的目标就变为了StubActivity,用来通过AMS的校验。
然后,用代理类IActivityManagerProxy来替换IActivityManager。
- 当版本大于等于26时,使用反射获取ActivityManager的IActivityManagerSingleton字段,小于时则获取ActivityManagerNative中的gDefault字段。
- 然后,通过反射获取对应的Singleton实例,从上面得到的2个字段中拿到对应的IActivityManager。
- 最后,使用Proxy.newProxyInstance()方法动态创建代理类IActivityManagerProxy,用IActivityManagerProxy来替换IActivityManager。
2、还原插件Activity:
- 前面用占坑Activity通过了AMS的校验,但是我们要启动的是插件TargetActivity,还需要用插件TargetActivity来替换占坑的SubActivity,替换时机为图中步骤2之后。
- 在ActivityThread的H类中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,最终会调用Activity的onCreate方法。在Handler的dispatchMessage处理消息的这个方法中,看到如果Handelr的Callback类型的mCallBack不为null,就会执行mCallback的handleMessage方法,因此mCallback可以作为Hook点。我们可以用自定义的Callback来替换mCallback。
自定义的Callback实现了Handler.Callback,并重写了handleMessage方法,当收到消息的类型为LAUNCH_ACTIVITY时,将启动SubActivity的Intent替换为启动TargetActivity的Intent。然后使用反射将Handler的mCallback替换为自定义的CallBack即可。使用时则在application的attachBaseContext方法中进行hook即可。
3、插件Activity的生命周期:
- AMS和ActivityThread之间的通信采用了token来对Activity进行标识,并且此后的Activity的生命周期处理也是根据token来对Activity进行标识的,因为我们在Activity启动时用插件TargetActivity替换占坑SubActivity,这一过程在performLaunchActivity之前,因此performLaunchActivity的r.token就是TargetActivity。所以TargetActivity具有生命周期。
Hook Instrumentation:
Hook Instrumentation实现同样也需要用到占坑Activity,与Hook IActivity实现不同的是,用占坑Activity替换插件Activity以及还原插件Activity的地方不同。
分析:在Activity通过AMS校验前,会调用Activity的startActivityForResult方法,其中调用了Instrumentation的execStartActivity方法来激活Activity的生命周期。并且在ActivityThread的performLaunchActivity中使用了mInstrumentation的newActivity方法,其内部会用类加载器来创建Activity的实例。
方案:在Instrumentation的execStartActivity方法中用占坑SubActivity来通过AMS的验证,在Instrumentation的newActivity方法中还原TargetActivity,这两部操作都和Instrumentation有关,因此我们可以用自定义的Instumentation来替换掉mInstrumentation。具体为:
- 首先检查TargetActivity是否已经注册,如果没有则将TargetActivity的ClassName保存起来用于后面还原。接着把要启动的TargetActivity替换为StubActivity,最后通过反射调用execStartActivity方法,这样就可以用StubActivity通过AMS的验证。
- 在newActivity方法中创建了此前保存的TargetActivity,完成了还原TargetActivity。最后使用反射用InstrumentationProxy替换mInstumentation。
资源插件化:
资源的插件化和热修复的资源修复都借助了AssetManager。
资源的插件化方案主要有两种:
- 1、合并资源方案,将插件的资源全部添加到宿主的Resources中,这种方案插件可以访问宿主的资源。
- 2、构建插件资源方案,每个插件都构造出独立的Resources,这种方案插件不可以访问宿主资源。
so的插件化:
so的插件化方案和so热修复的第一种方案类似,就是将so插件插入到NativelibraryElement数组中,并且将存储so插件的文件添加到nativeLibraryDirectories集合中就可以了。
插件的加载机制方案:
- 1、Hook ClassLoader。
- 2、委托给系统的ClassLoader帮忙加载。
音视频
为什么巨大的原始视频可以编码成很小的视频呢
- 1)空间冗余:图像相邻像素之间有较强的相关性
- 2)时间冗余:视频序列的相邻图像之间内容相似
- 3)编码冗余:不同像素值出现的概率不同
- 4)视觉冗余:人的视觉系统对某些细节不敏感
- 5)知识冗余:规律性的结构可由先验知识和背景知识得到
图像可以提取的特征有哪些?
颜色、纹理(粗糙度、方向度、对比度)、形状(曲率、离心率、主轴方向)、色彩等
衡量图像重建好坏的标准有哪些?怎样计算?
SNR(信噪比)
PSNR=10*log10((2n-1)2/MSE) (MSE是原图像与处理图像之间均方误差,所以计算PSNR需要2幅图像的数据!)
SSIM (结构相似性分别从亮度对比度、对比度、结构3方面度量图像的相似性)
AAC和PCM的区别?
AAC在数据开始时候加了一些参数:采样率、声道、采样大小
H264存储的两个形态?
a. Annex B :
StartCode :NALU单元,开头一般是0001或者001
防竞争字节:为了区分 0 0 0 1,它采用0 0 0 0x3 1作为区分
多用于网络流媒体中:rtmp、rtp格式
b. AVCC :
表示NALU长度的前缀,不定长用4、2、1来存储这个NALU的长度
防竞争字节
多用于文件存储中mp4的格式
怎么做到直播秒开优化?
- DNS 解析慢 为了有效降低 DNS 解析对首开的影响,我们可以提前完成播放域名->IP 地址的解析, 并缓存起来,播放的时候,直接传入带 IP 地址的播放地址,从而省去了 DNS 解析的耗时。 如果要支持用 IP 地址播放,是需要修改底层 ffmpeg 源码的。
- 播放策略 很多侧重点播的播放器,为了减少卡顿,会有一些缓冲策略,当缓冲足够多的数据之后 ,再送入解码播放。
而为了加快首开效果,需要对播放的缓冲策略做一些调整,如果第一帧还没有渲染出来的情况下, 不要做任何缓冲,直接送入解码器解码播放,这样就可以保证没有任何因为「主动」缓冲带来的首开延时。
- 播放参数设置 所有基于 ffmpeg 的播放器,都会遇到avformat_find_stream_info这个函数耗时比较久, 从而增大了首开时间,该函数主要作用是通过读取一定字节的码流数据, 来分析码流的基本信息,如编码信息、时长、码率、帧率等等,它由两个参数来控制其读取的数据量大小和时长, 一个是 probesize,一个是 analyzeduration。
减少 probesize 和 analyzeduration 可以有效地减少avformat_find_stream_info的函数耗时, 从而加快首开,但是需要注意的是,设置地太小可能会导致读取的数据量不足,从而无法解析出码流信息,导致播放失败, 或者出现只有音频没有视频,只有视频没有音频的问题。
-
服务端优化
-
服务器关键帧缓冲
-
CDN最近策略
常见的音视频格式有哪些?
-
MPEG(运动图像专家组)是Motion Picture Experts Group 的缩写。这类格式包括了MPEG-1,MPEG-2和MPEG-4在内的多种视频格式。
-
AVI,音频视频交错(Audio Video Interleaved)的英文缩写。AVI这个由微软公司发布的视频格式,在视频领域可以说是最悠久的格式之一。
-
MOV,使用过Mac机的朋友应该多少接触过QuickTime。QuickTime原本是Apple公司用于Mac计算机上的一种图像视频处理软件。
-
ASF(Advanced Streaming format高级流格式)。ASF 是MICROSOFT 为了和的Real player 竞争而发展出来的一种可以直接在网上观看视频节目的文件压缩格式。
-
WMV,一种独立于编码方式的在Internet上实时传播多媒体的技术标准,Microsoft公司希望用其取代QuickTime之类的技术标准以及WAV、AVI之类的文件扩展名。
-
NAVI,如果发现原来的播放软件突然打不开此类格式的AVI文件,那你就要考虑是不是碰到了n AVI。n AVI是New AVI 的缩写,是一个名为Shadow Realm 的地下组织发展起来的一种新视频格式。
-
REAL VIDEO(RA、RAM)格式由一开始就是定位在视频流应用方面的,也可以说是视频流技术的始创者。
-
MKV,一种后缀为MKV的视频文件频频出现在网络上,它可在一个文件中集成多条不同类型的音轨和字幕轨,而且其视频编码的自由度也非常大,可以是常见的DivX、XviD、3IVX,甚至可以是RealVideo、QuickTime、WMV 这类流式视频。
-
FLV是FLASH VIDEO的简称,FLV流媒体格式是一种新的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等缺点。
-
F4V,作为一种更小更清晰,更利于在网络传播的格式,F4V已经逐渐取代了传统FLV,也已经被大多数主流播放器兼容播放,而不需要通过转换等复杂的方式
ffmpeg的数据结构?
ffmpeg的数据结构可以分为以下几类:
-
(1)解协议(http,rtsp,rtmp,mms) AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。URLProtocol存储输入音视频使用的封装格式。每种协议都对应一个URLProtocol结构。(注意:FFMPEG中文件也被当做一种协议“file”)
-
(2)解封装(flv,avi,rmvb,mp4) AVFormatContext主要存储视音频封装格式中包含的信息 ffmpeg支持各种各样的音视频输入和输出文件格式(例如FLV, MKV, MP4, AVI),而 AVInputFormat和AVOutputFormat 结构体则保存了这些格式的信息和一些常规设置。
-
(3)解码(h264,mpeg2,aac,mp3) AVStream是存储每一个视频/音频流信息的结构体。AVCodecContext: 编解码器上下文结构体,存储该视频/音频流使用解码方式的相关数据。AVCodec: 每种视频(音频)编解码器(例如H.264解码器)对应一 个该结构体。三者的关系如下图:
-
(4)存数据 对于视频,每个结构一般是存一帧;音频可能有好几帧
-
- 解码前数据:AVPacket
- 解码后数据:AVFrame
在MPEG标准中图像类型有哪些?
I帧图像, P帧图像, B帧图像
列举一些音频编解码常用的实现方案?
第一种就是采用专用的音频芯片对 语音信号进行采集和处理,音频编解码算法集成在硬件内部,如 MP3 编解码芯片、语音合成 分析芯片等。使用这种方案的优点就是处理速度块,设计周期短;缺点是局限性比较大,不灵活,难以进行系统升级。
第二种方案就是利用 A/D 采集卡加上计算机组成硬件平台,音频编解码算法由计算机上的软件来实现。使用这种方案的优点是价格便 宜,开发灵活并且利于系统的升级;缺点是处理速度较慢,开发难度较大。
第三种方案是使用高精度、高速度 的 A/D 采集芯片来完成语音信号的采集,使用可编程的数据处理能力强的芯片来实现语音信号处理的算法,然后 用 ARM 进行控制。采用这种方案的优点是系统升级能力强,可以兼容多种音频压缩格式甚至未来的音频压缩格 式,系统成本较低;缺点是开发难度较大,设计者需要移植音频的解码算法到相应的 ARM 芯 片中去。
sps和pps的区别?
SPS是序列参数集 0x67
PPS是图像参数集 0x68
在SPS序列参数集中可以解析出图像的宽,高和帧率等信息。而在h264文件中,最开始的两帧数据就是SPS和PPS,这个h264文件只存在一个SPS帧和一个PPS帧。
AMR基本码流结构?
AMR文件由文件头和数据帧组成,文件头标识占6个字节,后面紧跟着就是音频帧;
格式如下所示:
文件头(占 6 字节)| :— | 语音帧1 | 语音帧2 | … |
文件头: 单声道和多声道情况下文件的头部是不一致的,单声道情况下的文件头只包括一个Magic number,而多声道情况下文件头既包含Magic number,在其之后还包含一个32位的Chanel description field。多声道情况下的32位通道描述字符,前28位都是保留字符,必须设置成0,最后4位说明使用的声道个数。
语音数据: 文件头之后就是时间上连续的语音帧块了,每个帧块包含若干个8位组对齐的语音帧,相对于若干个声道,从第一个声道开始依次排列。每一个语音帧都是从一个8位的帧头开始:其中P为填充位必须设为0,每个帧都是8位组对齐的。
视频拼接处理步骤?(细节处理,比如分辨率大小不一,时间处理等等)
解封装、解码、决定分辨率大小、编码、时间处理、封装。
NV21如何转换成I420?
首先需要明白为什么需要将NV21转换成I420,这是因为x264只支持编码I420的数据。 实际上就是YUV420p与YUV420sp之间的转换
影响视频清晰度的指标有哪些?
帧率 码率 分辨率 量化参数(压缩比)
如何秒开视频?什么是秒开视频?
什么是秒开视频? 秒开是指用户点击播放到看到画面的时间非常短,在 1 秒之内。
为什么需要秒开? 目前主流的直播协议是 RTMP,HTTP-FLV 和 HLS,都是基于 TCP 的长连接。在播放的过程中,若播放端所处的网络环境在一个较佳的状态,此时播放会很流畅。若网络环境不是很稳定,经常会发生抖动,如果播放端没有特殊处理,可能会经常发生卡顿,严重的甚至会出现黑屏。而移动直播由于其便捷性,用户可以随时随地发起和观看直播,我们无法保证用户的网络一直处于非常好的状态,所以,在网络不稳定的情况下保证播放的流畅度是非常重要的。
解决思路
3.1 获取关键帧后显示 改写播放器逻辑让播放器拿到第一个关键帧后就给予显示。 GOP 的第一个帧通常都是关键帧,由于加载的数据较少,可以达到 “首帧秒开”。如果直播服务器支持 GOP 缓存,意味着播放器在和服务器建立连接后可立即拿到数据,从而省却跨地域和跨运营商的回源传输时间。 GOP 体现了关键帧的周期,也就是两个关键帧之间的距离,即一个帧组的最大帧数。假设一个视频的恒定帧率是 24fps(即 1 秒 24 帧图像),关键帧周期为 2s,那么一个 GOP 就是 48 张图像。一般而言,每一秒视频至少需要使用一个关键帧。 增加关键帧个数可改善画质(GOP通常为 FPS 的倍数),但是同时增加了带宽和网络负载。这意味着,客户端播放器下载一个 GOP,毕竟该 GOP 存在一定的数据体积,如果播放端网络不佳,有可能不是能够快速在秒级以内下载完该 GOP,进而影响观感体验。 如果不能更改播放器行为逻辑为首帧秒开,直播服务器也可以做一些取巧处理,比如从缓存 GOP 改成缓存双关键帧(减少图像数量),这样可以极大程度地减少播放器加载 GOP 要传输的内容体积。
3.2 app 业务逻辑层面优化 比如提前做好 DNS 解析(省却几十毫秒),和提前做好测速选线(择取最优线路)。经过这样的预处理之后,在点击播放按钮时,将极大提高下载性能。
一方面,可以围绕传输层面做性能优化;另一方面,可以围绕客户播放行为做业务逻辑优化。两者可以有效的互为补充,作为秒开的优化空间。
秒开视频方案
4.1 优化服务器策略 播放器接入服务器请求数据的时间点的视频不一定是关键帧,那么需要等到下一个关键帧的到来,如果关键帧的周期是 2s 的话,那么等待的时间可能会在 0~2s 的范围内,这段等待的时间会影响首屏的加载时间。如果服务器有缓存,则播放端在接入的时候,服务器可以向前找最近的关键帧发给播放端,这样就可以省去等待的时间,可以大大的减少首屏的加载时间。
4.2 优化播放端策略 播放端请求到的第一帧数据肯定是关键帧,关键帧能够通过帧内参考进行解码。这样播放端就可以在接收到第一个关键帧的时候就立即开始解码显示,而不需要等到缓存一定数量的视频帧才开始解码,这样也能减少首屏画面显示的时间。
5 播放端首屏时长的优化 播放器的首屏过程中的几个步骤:
首屏时间,指的是从进入直播间开始到第一次看到直播画面的时间。首屏时间过长极易导致用户失去对直播的耐心,降低用户的留存。但游戏直播对画面质量和连贯性的要求高,对应推流端编码后的数据量和其他类型直播相比大的多,如何降低首屏时间是一个不小的难题。
在播放端的首屏过程中,主要有以下三个操作需要进行:加载直播间 UI(包括播放器本身)、下载直播数据流(未解码)和解码数据播放。其中数据解码播放又细分为以下几个步骤:
- 检测传输协议类型(RTMP、RTSP、HTTP 等)并与服务器建立连接接收数据;
- 视频流解复用得到音视频编码数据(H.264/H.265、AAC 等);
- 音视频数据解码,音频数据同步至外设,视频数据渲染都屏幕,至此,视频开始播放,首屏时间结束。
- 总结: 首先,加载 UI 可以以单例的方式进行,能够一定程度地提升首屏展示速度;其次,可以预设解码类型,减少数据类型检测时间;再次,设定合理的下载缓冲区大小,尽可能减少下载的数据量,当检测到 I 帧数据,立即开始解码单帧画面进行播放,提高首屏展示时间。
如何降低延迟?如何保证流畅性?如何解决卡顿?解决网络抖动?
产生原因 保证直播的流畅性是指在直播过程中保证播放不发生卡顿,卡顿是指在播放过程中声音和画面出现停滞,非常影响用户体验。造成卡顿的原因有几种情况:
推流端网络抖动导致数据无法发送到服务器,造成播放端卡顿; 播放端网络抖动导致数据累计在服务器上拉不下来,造成播放卡顿。 由于从服务器到播放器的网络情况复杂,尤其是在 3G 和带宽较差的 WIFI 环境下,抖动和延迟经常发生,导致播放不流畅,播放不流畅带来的负面影响就是延时增大。如何在网络抖动的情况下保证播放的流畅性和实时性是保障直播性能的难点。
流畅度优化 目前主流的直播协议是 RTMP、HTTP-FLV 和 HLS,都是基于 TCP 的长连接。在播放的过程中,若播放端所处的网络环境在一个较佳的状态,此时播放会很流畅。若网络环境不是很稳定,经常会发生抖动,如果播放端没有做特殊处理,可能会经常发生卡顿,严重的甚至会出现黑屏。而移动直播由于其便捷性,用户可以随时随地发起和观看直播,我们无法保证用户的网络一直处于一个非常好的状态,所以,在网络不稳定的情况下保证播放的流畅度是非常重要的。
为了解决这个问题,首先播放器需要将拉流线程和解码线程分开,并建立一个缓冲队列用于缓冲音视频数据。拉流线程将从服务器上获取到的音视频流放入队列,解码线程从队列中获取音视频数据进行解码播放,队列的长度可以调整。当网络发生抖动时,播放器无法从服务器上获取到数据或获取数据的速度较慢,此时队列中缓存的数据可以起到一个过渡的作用,让用户感觉不到网络发生了抖动。
当然这是对于网络发生抖动的情况所采取的策略,如果播放端的网络迟迟不能恢复或服务器的边缘结点出现宕机,则需要应用层进行重连或调度。
预测编码的基本原理是什么?
预测编码是数据压缩理论的一个重要分支。根据离散信号之间存在一定相关性特点,利用前面的一个或多个信号对下一个信号进行预测,然后对实际值和预值的差(预测误差)进行编码。如果预测比较准确,那么误差信号就会很小,就可以用较少的码位进行编码,以达到数据压缩的目的。
原理:利用以往的样本值对新样本值进行预测,将新样本值的实际值与其预测值相减,得到误差值,对该误差值进行编码,传送此编码即可。理论上数据源可以准确地用一个数学模型表示,使其输出数据总是与模型的输出一致,因此可以准确地预测数据,但是实际上预测器不可能找到如此完美的数学模型;预测本身不会造成失真。误差值的編码可以采用无失真压縮法或失真压縮法。
为什么要有YUV这种数据出来?(YUV相比RGB来说的优点)
RGB是指光学三原色红、绿和蓝,通过这3种的数值(0-255)改变可以组成其他颜色,全0时为黑色,全255时为白色。RGB是一种依赖于设备的颜色空间:不同设备对特定RGB值的检测和重现都不一样,因为颜色物质(荧光剂或者染料)和它们对红、绿和蓝的单独响应水平随着制造商的不同而不同,甚至是同样的设备不同的时间也不同。
YUV,是一种颜色编码方法。常使用在各个视频处理组件中。三个字母分别表示亮度信号Y和两个色差信号R-Y(即U)、B-Y(即V),作用是描述影像色彩及饱和度,用于指定像素的颜色。Y’UV的发明是由于彩色电视与黑白电视的过渡时期。黑白视频只有Y视频,也就是灰阶值。与我们熟知的RGB类似,YUV也是一种颜色编码方法,主要用于电视系统以及模拟视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。
YUV和RGB是可以相互转换的,基本上所有图像算法都是基于YUV的,所有显示面板都是接收RGB数据
H264/H265有什么区别?
同样的画质和同样的码率,H.265比H2.64 占用的存储空间要少理论50%。如果存储空间一样大,那么意味着,在一样的码率下H.265会比H.264 画质要高一些理论值是30%~40%。
比起H.264,H.265提供了更多不同的工具来降低码率,以编码单位来说,最小的8x8到最大的64x64。信息量不多的区域(颜色变化不明显)划分的宏块较大,编码后的码字较少,而细节多的地方划分的宏块就相应的小和多一些,编码后的码字较多,这样就相当于对图像进行了有重点的编码,从而降低了整体的码率,编码效率就相应提高了。
H.265标准主要是围绕着现有的视频编码标准H.264,在保留了原有的某些技术外,增加了能够改善码流、编码质量、延时及算法复杂度之间的关系等相关的技术。H.265研究的主要内容包括,提高压缩效率、提高鲁棒性和错误恢复能力、减少实时的时延、减少信道获取时间和随机接入时延、降低复杂度。
视频或者音频传输,你会选择TCP协议还是UDP协议?为什么?
选择UDP协议,UDP实时性好。TCP要保证丢失的package会被再次重发,确保对方能够收到。 而在视频播放中,如果有一秒钟的信号缺失,导致画面出现了一点瑕疵,那么最合适的办法是把这点瑕疵用随便哪些信号补充上,这样虽然画面有一点点瑕疵但是不影响观看。如果用的TCP的话,这点缺失的信号会被一遍又一遍的发送过来直到接收端确认收到。这不是音视频播放所期待的。而UDP就很适合这种情况。UDP不会一遍遍发送丢失的package。
何为直播?何为点播?
直播:是一个三方交互(主播、服务器、观众),这个交互式实时的!尽管会根据选择的协议不同而有一些延迟,但我们仍认为它直播是实时的!—>主播在本地发送音视频给服务器(推流),观众从服务器实时解码(拉流)收看收听主播发送给服务器的音视频(直播内容)。直播是不能快进的点播:首先一定要明确的一点,
点播不存在推流这一过程,你本身你的流已经早就推给服务器了,或者这么说也不对,应该是你的音视频早就上传到了服务器,观众只需要在线收看即可,由于你的音视频上传到了服务器,观众则可以通过快进,快退,调整进度条等方式进行收看
简述推流、拉流的工作流程?
推流:在直播中,一方向服务器发送请求,向服务器推送自己正在实时直播的数据,而这些内容在推送到服务器的这一过程中是以 “流” 的形式传递的,这就是“推流”,把音视频数据以流的方式推送(或上传)到服务器的过程就是“推流”! 推流方的音视频往往会很大,在推流的过程中首先按照 aac音频-编码 和 h264视频-编码的标准把推过来的音视频压缩 ,然后合并成 MP4或者 FLV格式,然后根据直播的封装协议,最后传给服务器完成推流过程。
拉流:与推流正好相反,拉流是用户从服务器获取推流方给服务器的音视频的过程,这就是“拉流”!拉流首先aac音频-解码 和 h.264视频-解码的内部把推过来的音视频解压缩,然后合成 MP4或者 FLV 格式,再解封装,最后到我们的客户端与观众进行交互。
直播推流中推I帧与推非I帧区别是什么?
I帧可以被独立地编码、解码,I帧的插入通常表示GOP(或视频片段)的结束
什么是GOP?
GOP ( Group of Pictures ) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位。 也就是说GOP组是指一个关键帧I帧所在的组的长度,每个 GOP 组只有 1 个 I 帧。 GOP 组的长度格式也决定了码流的大小。 GOP越大,中间的P帧和B帧的数量就越多,所以解码出来的视频质量就越高,但是会影响编码效率
为什么要用FLV?
是因为传输的协议要求,RTMP协议只支持FLV格式流
常见的直播协议有哪些?之间有什么区别?
RTMP协议
目前cdn厂商推流多用rtmp协议,实时性比HLS好,所以一般使用这种协议来上传视频流,即推动视频流到服务器。
HTTP-FLV协议
HTTP-FLV 和 RTMP 类似,都是针对于 FLV 视频格式做的直播分发流。但,两者有着很大的区别。
直接发起长链接,下载对应的FLV文件
头部信息简单
现在市面上,比较常用的就是HTTP-FLV 进行播放。但,由于手机端上不支持,所以,H5 的 HTTP-FLV 也是一个痛点。
HLS协议
HLS 协议本质还是一个个的 HTTP 请求 / 响应,所以适应性很好,在多数cdn厂商放在点播平台运行,不会受到防火墙影响。但它也有一个致命的弱点:延迟现象非常明显。如果每个ts 按照 5 秒来切分,一个 m3u8 放 6 个 ts 索引,那么至少就会带来 30 秒的延迟。如果减少每个 ts 的长度,减少 m3u8 中的索引数,延时确实会减少,但会带来更频繁的缓冲,对服务端的请求压力也会成倍增加。因此在延迟和实时性上需要作出平衡。