失效链接处理 |
2020年今日头条面试题 PDF 下载 下载地址:
提取码:czjf
相关截图: 主要内容:
今⽇头条⾯试
⾃我介绍:⾯试官您好,我是刘世麟,⾮常荣幸能参加贵公司的⾯试,下⾯我简单介绍⼀下我的个⼈
情况:我从实习到现在⼀直在致学教育⼯作,从事 Android 开发,凭借良好的⼯作能⼒和沟通能⼒,
连续两年蝉联「优秀员⼯」称号,在今年初被公司内聘为技术总监助理,协助技术总监开展部⻔管理
和项⽬推动⼯作。在⼯作之外,我喜欢编写技术博客和在 GitHub 上贡献开源代码,⽬前在 GitHub
上总共拥有 7k 左右的 Star,数篇技术博客也有数⼗万阅读。我⾮常地热爱移动开发,早已久仰贵团
队对技术的看᯿,所以希望今天⾃⼰⾯试有好的表现,未来能有幸与您共事。
你有什么要问我的吗?
对新⼊公司的员⼯有没有什么培训项⽬?
⼊职后参与的项⽬是怎样的?
Java 篇
HashMap 的内部结构?内部原理?和 HashTable 的区别,假如发⽣了 hash 碰
撞,如何设计能让遍历效率⾼?
HashMap 基于 Map 实现,允许 null 的键值,不保证插⼊顺序,也不保证序列不随时间⽽变化。其
内部是使⽤⼀个默认容量为 16 的数组来存储数据的,⽽数组中每⼀个元素却⼜是⼀个链表的头结
点。所以,更加确切的说,HashMap 内部存储结构是使⽤哈希表的拉链结构(数组 + 链表),这种
存储数据的⽅法叫做拉链法。⽽链表中的每个结点都是 Entry 类型,⽽ Entry 存储的内容包含
key、value、hash 和 next。
⼯作原理:主要是通过 hash 的⽅法,通过 put 和 get 来存取对象。
存取对象时,我们将 key-value 传给 put() 时,它通过 hashCode() 计算 hash 从⽽得到桶
(bucket)的位置,进⼀步存储。HashMap 会根据当前桶(bucket)的占⽤情况来⾃动调整容
量(超过负载因⼦ Load Factor 则 resize() 为原来的 2 倍);
获取对象时,我们将 key 传递给 get() ,它调⽤ hashCode() 计算 hash 从⽽得到桶
(bucket)的位置,并进⼀步通过 equals() 确认键值对。如果发⽣碰撞的时候,HashMap
将会通过链表把产⽣碰撞冲突的元素组织起来。在 Java 8 中,如果⼀个桶(bucket)中碰撞冲
突的元素超过某个限制(默认是 8),则使⽤红⿊树来替换链表,从⽽提⾼速度。
什么是红⿊树?
红⿊树本质上就是⼀种⼆叉查找树(⼆叉查找树的插⼊、删除、查找最好情况为 O(logn),但极
端的斜树为 O(n)),但它在⼆叉查找树的基础上额外添加了⼀个颜⾊做标记,同时具有⼀定的规
则,这些规则让红⿊树保证了⼀种平衡,插⼊、删除、查找的最坏时间复杂度都是 O(logn)。
性质:
任何⼀个结点都有颜⾊,⿊⾊或者红⾊;
根结点是⿊⾊的;
⽗⼦结点之间不能出现两个连续的红结点;
任何⼀个结点向下遍历到其⼦孙的叶⼦结点,所经历的⿊结点个数必须相等;
空节点被认为是⿊⾊的;
HashMap 和 HashTable 虽然都实现了 Map 接⼝,但 HashTable 的实现是基于 Dictionary 抽
象类。⽽且 HashMap 中可以把 null 作为键值,所以 HashMap 判断是否含有某个键是⽤
containsKey() ⽽不是 get() 。 get() ⽅法返回 null 的时候,并不能判断是没有键,也可能是
这个键对应的值为 null。但 HashTable 是不允许的。还有⼀个区别就是 HashMap 是⾮同步的,在
多线程中需要⼿动同步,⽽ HashTable 是同步的,可以直接⽤在多线程中。
但实际上,我们在多线程的时候,更加⻘睐于使⽤ ConcurrentHashMap ⽽不是 HashTable 。因
为 HashTable 使⽤ synchronized 来做线程安全,全局只有⼀把锁,直接锁住整个 Hash 表,⽽
ConcurrentHashMap 是⼀次锁⼀个桶。在线程竞争激烈的情况下 HashTable 效率是⾮常低下
的。但即便如此,我们也不能说 ConcurrentHashMap 就可以完全替代 HashTable 。根本在于
HashTable 的迭代器是强⼀致性的,⽽ ConcurrentHashMap 是弱⼀致性的。
ConcurrentHashMap 不允许 key 或者 value 为 null。
对于「强⼀致性」和「弱⼀致性」的理解:⽐如我们往 ConcurrentHashMap 底层数据结构加
⼊⼀个元素后,get 可能在某段时间内还看不到这个元素。
讲讲 ConcurrentHashMap。
由于 HashMap 是⼀个线程不安全的容器,主要体现在容量⼤于 总量*负载因⼦ 发⽣扩容时会出现环
形链表从⽽导致死循环。
因此需要⽀持线程安全的并发容器 ConcurrentHashMap 。 在 JDK 1.7 中,ConcurrentHashMap 仍然是数组加链表,和 HashMap 不⼀样的是,
ConcurrentHashMap 最外层并不是⼀个⼤的数组,⽽是⼀个 Segment 的数组,每⼀个 Segment 包
含⼀个和 HashMap 数据结构差不多的链表数组。
ConcurrentHashMap 采⽤了分段锁的技术,Segment 继承于 ReentrantLock,所以我们可以很⽅便
的对每⼀个 Segment 上锁。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,⼀
个线程占⽤锁访问⼀个 Segment 时,根本不会影响到其他的 Segment。 在 ConcurrentHashMap 的 get ⽅法中,⾮常⾼效,因为全程不需要加锁。只需要将 key 通过 hash
之后定位到具体的 Segment,再通过⼀次 hash 定位到具体的元素上。由于 HashEntry 中的 value
属性是⽤ volatile 关键字修饰的,保证了内存可⻅性,所以每次获取到的值都是最新值。
class Node<T>{
public T value;
public Node<T> parent;
public boolean isRed;
public Node<T> left;
public Node<T> right; }
虽然对 HashEntry 的 value 采⽤了volatile 关键字修饰,但并不能保证并发的原⼦性,所以 put 操作
时仍然需要加锁处理。⾸先是通过 key 的 hash 定位到具体的 Segment,在 put 之前会进⾏⼀次扩容
校验。这⾥⽐ HashMap 要好的⼀点是:HashMap 是插⼊元素之后在看是否需要扩容,有可能扩容
之后后续就没有插⼊就浪费了本次扩容,⽽ HashMap 的扩容是⾮常消耗性能的。⽽
ConcurrentHashMap 不⼀样,它是先将数据插⼊之后再检查是否需要扩容,之后再做插⼊。
⽽在 JDK 1.8 中,抛弃了原有的 Segment 分段锁,⽽采⽤了 CAS + synchronized 来保证并发安全
性。并把 1.7 中存放数据的 HashEntry 改为了 Node,但作⽤还是相同的。其中 value 和 next 均⽤
volatile 保证可⻅性。
JVM 虚拟机内存结构,以及它们的作⽤
JVM 内存结构主要由三⼤块:堆内存、⽅法区和栈。
每个线程包含⼀个栈区,栈中只包含基本数据类型和对象的引⽤,⽽且每个栈中的数据都是私有的,
其他栈不允许访问。此外,还会存放⽅法的形式参数和引⽤对象的地址,在使⽤完后,栈空间会⽴即
回收,堆空间等待 GC。
堆主要⽤于存放对象,同时也是垃圾收集器管理的主要区域。每个对象会包含⼀个与之对应的 class
信息,JVM 只有⼀个堆区(heap)被所有线程共享,堆区不存放基本数据类型和对象引⽤,只存放对
象本身。
⽽⽅法区主要⽤于存放线程所执⾏的字节码指令和常量池,会被所有线程共享,⽅法区包含所有的
class 和 static 变量。
讲讲 JVM 的类加载过程 && 双亲委派模型
JVM 的类加载过程分为加载、验证、准备、解析、初始化 5 个阶段。
加载:加载阶段由类加载器进⾏负责,类加载器根据⼀个类的全限定名读取该类的⼆进制字节流
到 JVM 内部,然后转换为⼀个对应的 java.lang.Class 对象实例;⼀个类由类加载器和类本身⼀
起确定,所以不同类加载器加载同⼀个类得到的 java.lang.Class 也是不同的。
验证:验证阶段负责验证类数据信息是否符合 JVM 规范,是否是⼀个有效的字节码⽂件;
准备:准备阶段负责为类中的 static 变量分配空间,并初始化(与程序⽆关、系统初始化);
解析:解析阶段负责将常量池中所有的符号引⽤转换为直接引⽤;
初始化:初始化阶段负责将所有的 static 域按照程序指定操作对应执⾏(赋值 static 变量,执⾏
static 块)。
上述阶段通常都是交叉混合允许,没有严格的先后执⾏顺序。
双亲委派过程:当⼀个类加载器收到类加载任务的时候,⽴即将任务委派给它的⽗类加载器去执⾏,
直到委派给最顶层的启动类加载器为⽌。如果⽗类加载器⽆法加载委派给它的类时,将类加载任务回
退到它的下⼀级加载器去执⾏。除了启动类加载器以外,每个类加载器拥有⼀个⽗类加载器,⽤户的
⾃定义类加载器的⽗类加载器是 AppClassLoader。双亲委派模型可以保证全限名指定的类,只被加
载⼀次。双亲委派模型不具有强制性约束,是 Java 设计者推荐的类加载器实现⽅式。
采⽤双亲委派模型的原因:⽐如⿊客定义⼀个 java.lang.String 类,该 String 类和系统 String 类有⼀
样的功能,只是在某个⽅法⽐如 equels() 中加⼊了病毒代码,并且通过⾃定义类加载器加⼊ JVM
中,如果没有双亲委派模型,那么 JVM 就可能误以为⿊客编写的 String 类是系统 String 类,导致
「病毒代码」最终被执⾏。⽽有了双亲委派模型,⿊客定义的 java.lang.String 类就⽤于不会被加载
进内存,因为最顶端的类加载器会加载系统的 String 类,最终⾃定义的类加载器⽆法加载
java.lang.String 类。
可以通过᯿写 loadClass() ⽅法,打破双亲委派模型。
谈谈 Java 的 垃圾回收算法
⾸先我们肯定得需要确定哪些是活着的对象,哪些是可以回收的。
引⽤计数算法:它是判断对象是否存活的基本算法:给每个对象添加⼀个引⽤计数器,每当⼀个
地⽅引⽤它的时候,计数器就加 1;当引⽤失效后,计数器值就减 1。但是这种⽅法有⼀个致命
的缺陷:当两个对象相互引⽤时会导致这两个对象都⽆法被回收。
根搜索算法:但⽬前主流商⽤语⾔都采⽤根搜索算法来判断对象是否存活。对于程序来说,根对
象总是可以被访问的,从这些根对象开始,任何可以被触及的对象都被认为是「活着的」对象,
⽆法触及的对象被认为是垃圾,需要被回收。
对于垃圾回收算法,主要包含标记-清除算法、复制回收算法、标记-整理算法和分代回收算法。
标记-清除算法:⾸先使⽤根搜索算法标记出所有需要回收的对象,标记完成后统⼀回收所有被
标记的对象。但有两个缺点:
效率问题:标记和清除的效率都不⾼;
空间问题:标记清除后会产⽣⼤量不连续的内存碎⽚;
复制回收算法:将可⽤内存分为⼤⼩相等的两份,在同⼀时刻只能使⽤其中的⼀份。当其中⼀份
内存使⽤完了,就把还存活的对象复制到另⼀份内存上,然后将这⼀份上的内存情况。复制回收
算法能有效地避免内存碎⽚,但是算法需要把内存⼀分为⼆,导致内存使⽤率⼤⼤降低。
标记-整理算法:复制算法在对象存活率较⾼的情况下会复制很多的对象,效率会很低。标记-整
理算法就解决了这样的问题,同样采⽤的是根搜索算法进⾏存活对象标记,但后续是将所有存活
的对象都移动到内存的⼀端,然后清理掉端外界的对象。
分代回收算法:在 JVM 中不同的对象拥有不同的⽣命周期,因此对于不同⽣命周期的对象也可
以采⽤不同的垃圾回收⽅法,以提⾼效率,这就是分代回收算法的核⼼思想。
在不进⾏对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进⾏回收,花费的时间相
对会⻓。同时,因为每次回收都需要遍历所有存活对象,但实际上,对于⽣命周期⻓的对象⽽
⾔,这种遍历是没有效果的,因为可能进⾏了很多次遍历,但是他们依旧存在。因此,分代垃圾
回收采⽤分治的思想,进⾏代的划分,把不同⽣命周期的对象放在不同代上,不同代上采⽤最适
合它的垃圾回收⽅式进⾏回收。
JVM 中共分为三个代:新⽣代、⽼年代和持久代。其中持久代主要存放的是 Java 类的类信息,
与垃圾收集要收集的 Java 对象关系不⼤。
新⽣代:所有新⽣成的对象⾸先都是放在新⽣代的,新⽣代采⽤复制回收算法。新⽣代的
⽬标就是尽可能快速地收集掉那些⽣命周期短的对象。新⽣代按照 8:1 的⽐例分为⼀个
Eden 区和两个 Survivor 区。⼤部分对象在 Eden 区⽣成,当 Eden 区满时,还存活的对
象将被复制到其中的⼀个 Survivor 区,当这个 Survivor 区满时,此区的存活对象将被复
制到另外⼀个 Survivor 区,当另⼀个 Survivor 区也满了的时候,从第⼀个 Survivor 区
复制过来的并且此时还存活的对象,将被复制到了「年⽼区」。需要注意,Survivor 的两
个区是对称的,没有任何的先后关系,所以同⼀个区中可能同时存在 Eden 复制过来的对
象,和从前⼀个 Survivor 区复制过来的对象,⽽复制到年⽼区的只有从第⼀个 Survivor 区
过来的对象,⽽且,Survivor 区总有⼀个是空的。
⽼年代:在新⽣代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到⽼年代中,⽼年
代采⽤标记整理回收算法。因此,可以认为⽼年代中存放的都是⼀些⽣命周期较⻓的对
象。
持久代:⽤于存放静态⽂件,如 final 常量、static 常量、常量池等。持久代对垃圾回收没
有显著影响,但有些应⽤可能动态⽣成或者调⽤⼀些 class。在这种时候需要设置⼀个⽐较
⼤的持久代空间来存放这些运⾏过程中新增的类。
谈谈 Java 垃圾回收的触发条件
Java 垃圾回收包含两种类型:Scavenge GC 和 Full GC。
Scavenge Gc:⼀般情况下,当新对象⽣成,并且在 Eden 申请空间失败的时候,就会触发
Scavenge GC,对 Eden 区进⾏ GC,清除⾮存活的对象,并且把尚且存活的对象移动到
Survivor 区,然后整理 Survivor 的两个区。这种⽅式的 GC 是对新⽣代的 Eden 区进⾏,不会
影响到⽼年代。因为⼤部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很⼤,所以
Eden 区的 GC 会频繁进⾏。
Full GC:Full GC 将会对整个堆进⾏整理,包括新⽣代、⽼年代和持久代。Full GC 因为需要对
整个堆进⾏回收,所以⽐ Scavenge GC 要慢,因此应该尽量减少 Full GC 的次数。在对 JVM 调
优的过程中,很⼤⼀部分⼯作就是对 Full GC 的调节,有如下原因可能导致 Full GC:
⽼年代被写满;
持久代被写满;
System.gc() 被显示调⽤;
synchronized 和 Lock 的区别
1. 使⽤⽅法的区别:
synchronized:在需要同步的对象中加⼊此控制, synchronized 可与加在⽅法上,也可
以加在特定代码块中,括号中表示需要加锁的对象。
Lock:需要显示指定起始位置和终⽌位置。⼀般使⽤ ReentrantLock 类作为锁,多个线
程中必须要使⽤⼀个 ReentrantLock 类作为对象才能保证锁的⽣效。且在加锁和解锁处
需要通过 lock() 和 unlock() 显式指出。所以⼀般会在 finally 块中写
unlock() 以防死锁。
2. 性能的区别:
synchronized 是托管给 JVM 执⾏的,⽽ lock 是 Java 写的控制锁的代码。在 Java 1.5
中, synchronized 是性能低下的。因为这是⼀个᯿量级操作,需要调⽤操作接⼝,导致有可
能加锁消耗的系统时间⽐锁以外的操作还多。相⽐下使⽤ Java 提供的 Lock 对象,性能更低⼀
些。但是到了 Java 1.6,发⽣了变化。 synchronized 在语义上很清晰,可以进⾏很多优化,
有适应⾃旋、锁消除、锁粗化、轻量级锁、偏向锁等,导致在 Java 1.6 上 synchronized 的性
能并不⽐ Lock 差。
synchronized:采⽤的是 CPU 悲观锁机制,即线程获得的是独占锁。独占锁就意味着 其
他线程只能依靠阻塞来等待线程释放所。⽽在 CPU 转换线程阻塞时会引起线程上下⽂切
换,当有很多线程竞争锁的时候,会引起 CPU 频繁的上下⽂切换导致效率很低。
Lock:采⽤的是乐观锁的⽅式。所谓乐观锁就是:每次不加锁⽽是假设没有冲突⽽去完成
某项操作,如果因为冲突失败就重试,直到成功为⽌。乐观锁实现的机制就是 CAS 操作。
我们可以进⼀步研究 ReentrantLock 的源代码,会发现其中⽐较᯿要的获得锁的⼀个⽅
法是 compareAndSetState 。这⾥其实就是调⽤的 CPU 提供的特殊指令。
3. ReentrantLock :具有更好的可伸缩性:⽐如时间锁等候、可中断锁等候、⽆块结构锁、多个
条件变量或者锁投票。
volatile 的作⽤,为什么会出现变量读取不⼀致的情况,与 synchronized 的区
别;
volatile 修饰的变量具有可⻅性
volatile 是变量修饰符,它修饰的变量具有可⻅性,Java 中为了加快程序的运⾏效率,对⼀些变
量的操作通常是在该线程的寄存器或是 CPU 缓存上进⾏的,之后才会同步到主存中,⽽加了
volatile 修饰符的变量则是直接读取主存,保证了读取到的数据⼀定是最新的。
volatile 禁⽌指令᯿排
指令᯿排是指处理器为了提⾼程序效率,可能对输⼊代码进⾏优化,它不保证各个语句的执⾏顺
序同代码中的顺序⼀致,但是它会保证程序最终执⾏结果和代码顺序执⾏的结果是⼀致的。指令
᯿排序不会影响单个线程的执⾏,但是会影响到线程并发执⾏的正确性。
⽽ synchronized 可⽤作于⼀段代码或⽅法,既可以保证可⻅性,⼜能够保证原⼦性。
在性能⽅⾯,synchronized 关键字是防⽌多个线程同时执⾏⼀段代码,会影响程序执⾏效率,⽽
volatile 关键字在某些情况下性能要优于 synchronized。
可⻅性是指多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看
到修改的值。
指令᯿排序:⼀般来说,处理器为了提供程序运⾏效率,可以会对输⼊代码进⾏优化,它不保
证程序中各个语句的执⾏先后顺序同代码中的顺序⼀致,但是它会保证程序最终执⾏结果和代
码顺序执⾏的结果是⼀致的。它不会影响到单线程的执⾏,却会影响到多线程的并发执⾏。
++i 在多线程环境下是否存在问题,怎么解决?
虽然递增操作 ++i 是⼀种紧凑的语法,使其看上去只是⼀个操作,但这个操作并⾮源⾃的。因为它并
不能作为⼀个不可分割的操作来执⾏。实际上,它包含了 3 个独⽴的操作:读取 i 的值,将值加 1,
然后将计算结果返回给 i。这是⼀个「读取-修改-写⼊」的操作序列,并且其结果状态依赖于之前的状
态,所以在多线程环境下存在问题。
要解决⾃增操作在多线程下线程不安全的问题,可以选择使⽤ Java 提供的原⼦类,如 AtomicInteger
或者使⽤ synchronized 同步⽅法。
原⼦性:在 java 中,对基本数据类型的变量的读取和赋值操作是原⼦性操作,即这些操作是不
可被中断的,要么执⾏,要么不执⾏。也就是说,只有简单的读取、赋值(⽽且必须是将数字
赋值给某个变量)才是原⼦操作。(变量之间的相互赋值不是原⼦操作,⽐如 y = x,实际上是
先读取 x 的值,再把读取到的值赋值给 y 写⼊⼯作内存)
Thread.sleep() 和 Thread.yield() 区别
sleep() 和 yield() 都会释放 CPU。
sleep() 使当前线程进⼊停滞状态,所以执⾏ sleep() 的线程在指定的时间内肯定不会执
⾏; yield() 只是使当前线程᯿新回到可执⾏状态,所以执⾏ yield() 的线程有可能在进⼊到可
执⾏状态后⻢上⼜被执⾏。
sleep() 可使优先级低的线程得到执⾏的机会,当然也可以让同优先级和⾼优先级的线程有执⾏的
机会; yield() 只能使同优先级的线程有执⾏的机会。
讲讲常⽤的容器类
List 接⼝实现:允许数据᯿复
1. ArrayList:实现 List 接⼝,内部由数组实现,可以插⼊ null,元素可᯿复,线程不安全,
访问元素快;空间不⾜的时候增⻓率为当前⻓度的 50%。
2. Vector:ArrayList 的线程安全写法,由于⽀持多线程,所以性能⽐ ArrayList 较低;空间
不⾜的时候增⻓率为当前⻓度的 100%。它的同步是通过 Iterator ⽅法加 synchronized 实
现的。
3. Stack:线程同步,继承⾃ Vector,添加了⼏个⽅法来完成栈的功能。
4. LinkList:实现 List 接⼝,内部实现是双向链表,擅⻓插⼊删除,元素可᯿复;线程不同
步。
Set 接⼝实现:不允许数据᯿复,最多允许⼀个 null 元素。
1. HashSet:实现 Set 接⼝,线程不同步,不允许᯿复元素,基于 HashMap 存储,遍历时不
保证顺序,并且不保证下次遍历顺序和之前⼀样,允许 null 元素;
2. LinkedHashSet:继承⾃ HashSet,不允许᯿复元素,可以保持顺序的 Set 集合,基于
LinkedHashMap 实现;
3. TreeSet:线程不同步,不允许᯿复元素,保持元素⼤⼩次序的集合,⾥⾯的元素必须实现
Comparable 接⼝,源码算法基于 TreeMap;
4. EnumSet:线程不同步,内部使⽤ Enum 数组实现,速度⽐ HashSet 快。只能存储在构造
函数传⼊的枚举类的枚举值。
Map
1. HashMap:线程不冲突。根据 key 的 hashcode 进⾏存储,内部使⽤静态内部类 Node 的
数组进⾏存储,默认初始⼤⼩为 16,每次扩⼤⼀倍。当发⽣ hash 冲突时,采⽤拉链法存
储。可以接受 null 的 key 和 value。在 JDK 1.8 中,当单个桶中元素⼤于等于 8 时,链表
实现改为红⿊树实现;当元素个数⼩于 6 时,变回链表实现,由此来防⽌ hashCode 攻
击。
2. LinkedHashMap:继承⾃ HashMap,相对 HashMap 来说,遍历的时候具有顺序,可以
保证插⼊的顺序,存储⽅式和 HashMap ⼀样,采⽤哈表表⽅法存储,不过
LinkedHashMap 多维护了⼀份上下指针;⼤多数情况下遍历速度⽐ HashMap 慢,不过有
种情况例外,当 HashMap 容量很⼤,实际数据较少的时候,遍历起来可能会⽐
LinkedHashMap 慢,因为 LinkedHashMap 的遍历速度只和实际数据有关,和容量⽆关,
⽽ HashMap 的遍历速度和它的容量有关。
3. TreeMap:线程不同步,基于红⿊树的 NavigableMap 实现,能够把它保存的记录根据键
排序,默认是按键值的升序排序,也可以指定排序的⽐较器,当⽤ Iterator 遍历
TreeMap 时,得到的记录是排过序的。
4. HashTable:线程安全,不能存储 null 的 key 和 value。
如何去除 ArrayList 的重复元素?
直接采⽤ HashSet 即可。作为它的参数,然后再 addAll。但这种⽅式不能保证原来的顺序,如果要
求顺序,可以使⽤ LinkedHashSet 即可。
讲讲 Java 的泛型擦除,泛型主要是为了解决什么问题?如何⽤泛型做 Json 的解
析的?
泛型信息只存在于代码编译阶段,在进⼊ JVM 之前,与泛型相关的信息会被擦除掉,这样的现象就叫
泛型擦除。
泛型的最⼤作⽤是提升代码的安全性和可读性。如果没有泛型,我们只能采⽤ Object 来实现参数的
任意化,这样在使⽤的时候需要进⾏类型强转,⽽且这样即使错误了,也不会在编译时就提示错误。
⽽有了泛型就可以很好的解决这个问题,在编译时就会出现编译不通过的现象。⽽泛型提升代码效率
的优点也显⽽易⻅,如果没有泛型,解决前⾯的问题只能通过编写多个 set 和 get ⽅法来处理,但有
了泛型便只需要⼀个 get 和⼀个 set ⽅法就可以处理。
既然说到了「泛型擦除」的概念,就不得不提到⼀个经典的现象,我们假设两个 ArrayList,⼀个⾥⾯
存放 Integer 类型,⼀个⾥⾯存放 String 类型,但调⽤他们的 getClass() ⽅法却是相等的。因为
在 JVM 中它们的 Class 都是 List.class。⽽我们在做 Json 解析的时候,⼀定会遇到有的数据是
JsonObject,有个数据是 JsonArray 的情况。我们正常使⽤ gson 做解析的时候,需要对象.class 做参
数传⼊,所以我们必须要知道 List ⾥⾯存放的数据类型。这时候我们就可以通过「反射」的机制来获
取⾥⾯的泛型信息了。主要⽅式是采⽤
((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments(
) 拿到装载的泛型类的真实类型数组,拿到真实类型后我们便可以采⽤ gson 进⾏ Json 解析了。
谈谈 Java 的 Error 和 Exception 的区别联系。
Error 和 Exception 均集成⾃ Throwable,但 Error ⼀般指的是和虚拟机相关的问题,⽐如系统崩
溃,虚拟机错误,OOM 等,遇到这样的错误,程序应该被终⽌。⽽ Exception 表示程序可以处理的
异常,可以捕获并且可能恢复。
软引⽤和弱引⽤的区别?
只具有弱引⽤的对象拥有更短暂的⽣命周期,可能随时被回收(主要发⽣在每次 GC)。⽽只具有软引⽤
的对象只有在内存吃紧的时候才会被回收(主要在 OOM 之前),在内存⾜够的时候,通常不被回
收。
⽐如我们经常去读取⼀些图⽚,由于读取⽂件需要硬件操作,速度较慢,从⽽导致性能较低。所以我
们可以考虑把这些⽂件缓存起来,需要的时候直接从内存读取。但由于图⽚占⽤内存空间较⼤,⽐较
容易发⽣ OOM,所以我们可以考虑软引⽤来避免这个问题发⽣。
成员变量和静态⽅法可以被重写么?重写的规则是怎样的?
⼦类᯿写⽗类的⽅法,只有实例⽅法可以被重写,᯿写后的⽅法必须仍为实例⽅法。成员变量和静态
⽅法都不能被重写,只能被隐藏。(形式上可以写,但本质上并不是᯿写,⽽是属于隐藏)
⽅法的᯿写(override)遵循两同两⼩⼀⼤原则:
⽅法名必须相同,参数类型必须相同。
⼦类返回的类型必须⼩于或者等于⽗类⽅法返回的类型。
⼦类抛出的异常必须⼩于或者等于⽗类⽅法抛出的异常。
⼦类访问的权限必须⼤于或者等于⽗类⽅法的访问权限。
᯿写⽅法可以改变其他的⽅法修饰符,⽐如 final,synchronized,native 等。
内部类访问局部变量的时候,为什么变量必须加上 final 修饰符?
因为⽣命周期不同。局部变量在⽅法结束后就会被销毁,但内部类对象并不⼀定,这样就会导致内部
类引⽤了⼀个不存在的变量。所以编译器会在内部类中⽣成⼀个局部变量的拷⻉,这个拷⻉变量的⽣
命周期和内部类对象相同,就不会出现上述问题。但这样就导致了其中⼀个变量被修改,两个变量值
可能不同的问题。为了解决这个问题,编译器就要求局部变量需要被 final 修饰,以保证两个变量值
相同。
Android 篇
Android 为什么推荐使⽤ ArrayMap 和 SparseArray? 在 Java 中,我们通常会采⽤ HashMap 来存储 K-V 数据类型,然后在 Android Studio 中这样使⽤却
经常会得到⼀个警告,提示使⽤ ArrayMap 或者 SparseArray 做替代。
HashMap 基本上就是⼀个 HashMap.Entry 的数组,更准确地说,Entry 类中包含以下字段:
⼀个⾮基本数据类型的 key
⼀个⾮基本数据类型的 value
保存对象的哈希值
指向下⼀个 Entry 的指针
当有键值对插⼊时,HashMap 会发⽣什么 ?
⾸先,键的哈希值被计算出来,然后这个值会赋给 Entry 类中对应的 hashCode 变量。
然后,使⽤这个哈希值找到它将要被存⼊的数组中“桶”的索引。
如果该位置的 “桶” 中已经有⼀个元素,那么新的元素会被插⼊到 “桶” 的头部,next 指向上⼀个
元素——本质上使“桶”形成链表。
现在,当你⽤ key 去查询值时,时间复杂度是 O(1)。
虽然时间上 HashMap 更快,但同时它也花费了更多的内存空间。
所以它会有严᯿的缺点:
⾃动装箱的存在意味着每⼀次插⼊都会有额外的对象创建,这跟垃圾回收机制⼀样也会影响到内
存的利⽤;
HashMap.Entry 对象本身是⼀层额外需要被创建以及被垃圾回收的对象;
「桶」在 HashMap 每次被压缩或者扩容的时候都会被᯿新安排,这个操作会随着对象数量的增
⻓⽽变得开销极⼤。
⽽ ArrayMap 和 SparseArray 更加考虑内存优化,它们内部均采⽤两个数组进⾏数据存储。
在 ArrayMap 中,内部的数组⼀个⽤于记录 key 的 hash 值,另外⼀个数组⽤于记录 value 值,这样
既能避免为每个存⼊ map 中的键创建额外的对象,还能更积极地控制这些数组⻓度的增加。因为增
加⻓度只需拷⻉数组中的键,⽽不是᯿新构建⼀个哈希表。
当插⼊⼀个键值对时:
键/值被⾃动装箱。
键对象被插⼊到 mArray[] 数组中的下⼀个空闲位置。
值对象也会被插⼊到 mArray[] 数组中与键对象相邻的位置。
键的哈希值会被计算出来并被插⼊到 mHashes[] 数组中的下⼀个空闲位置。
对于查找⼀个 key :
键的哈希值先被计算出来
在 mHashes[] 数组中⼆分查找此哈希值。这表明查找的时间复杂度增加到了 O(logN)。
⼀旦得到了哈希值所对应的索引 index,键值对中的键就存储在 mArray[2index] ,值存储在
mArray[2index+1]。
这⾥的时间复杂度从 O(1) 上升到 O(logN),但是内存效率提升了。当我们在 100 左右的数据量
范围内尝试时,没有耗时的问题,察觉不到时间上的差异,但我们应⽤的内存效率获得了提⾼。
需要注意的是,ArrayMap 并不适⽤于可能含有⼤量条⽬的数据类型。它通常⽐ HashMap 要慢,因
为在查找时需要进⾏⼆分查找,增加或删除时,需要在数组中插⼊或删除键。对于⼀个最多含有⼏百
条⽬的容器来说,它们的性能差异并不巨⼤,相差不到 50%。
Bitmap 加载发⽣ OOM 怎么办?怎么处理。
在 Android 开发中,Bitmap 的加载通常采⽤ BitmapFactory.decodeXXX() ,但这些⽅法在构造
Bitmap 的时候就开始分配内存,所以很容易造成 OOM,解决⽅案也很简单,只需要对
BitmapFactory.Options 定义属性后进⾏压缩即可。需要设置 inJustDecodeBounds 属性为
true,来阻⽌解析时分配内存,此时解析返回 null,但是却可以拿到图⽚的宽⾼等信息。这个时候可
以根据⾃⼰的需求通过计算 inSampleSize 的值,之后把 inJustDecodeBounds 属性设置为
false 后再解析⼀次即可。
有了解过 IntentService 么?
IntentService 作为 Service 的⼦类,默认给我们开启了⼀个⼯作线程执⾏耗时任务,并且执⾏
完任务后,会⾃动停⽌服务。拓展 IntentService 也⽐较简单,提供⼀个构造⽅法和实现
onHandleIntent() ⽅法就是了,不需要᯿写⽗类的其他⽅法。但如果要绑定服务的话,还是要᯿
写 onBind() 返回⼀个 IBinder 的。使⽤ Service 可以同时执⾏多个请求,⽽使⽤
IntentService 只能同时执⾏⼀个请求。
详情可点击:⾯试:Android Service,你真的了解了么?
Activity 的⼏种启动模式有了解么?各⾃的使⽤场景?
详情可点击:⾯试:说说 Activity 的启动模式
standard
Android 默认的启动模式,每次 start 都会新建实例,适⽤于⼤多数场景。
singleTop
如果 start 的 Activity 刚好在栈顶,则不会新建实例,直接调⽤ onNewIntent() 。使⽤场景:
资讯类内容⻚⾯。
singleTask
调⽤ start 启动的 Activity 只要在当前 Activity 栈⾥⾯存在,则直接调⽤ onNewIntent() ,并
把上⾯的所有实例全部移除。使⽤场景:APP 的主⻚⾯。
singleInstance
Activity 栈⾥⾯只会存在⼀个实例,每次启动都会直接调⽤ onNewIntent() 。适⽤于⽐如接电
话,闹钟。
Looper.prepare() 和 Looper.loop() ⽅法分别做了什么?
Looper.prepare():⾸先从 ThreadLocal 中获取⼀个 Looper ,如果没有则向
ThreadLocal 中添加⼀个新的 Looper ,同时新建⼀个 MessageQueue 。主线程的
Looper 在 ActivityThread 中创建。
ThreadLoacl 是 Java 提供的⽤于保存同⼀进程中不同线程数据的⼀种机制。每个线程中
都保有⼀个 ThreadLocalMap 的成员变量, ThreadLoaclMap 内部采⽤
WeakRefrence 数组保存,数组的 key 即为 ThreadLocal 内部的 hash 值。通常情况
下,我们创建的变量是可以被任何⼀个线程访问并修改的,但 ThreadLocal 创建的变量
只能被当前线程访问,其他线程⽆法访问和修改。Handler 正是利⽤它的这⼀特性,来做
到每个线程都有⾃⼰独有的 Looper。
ActivityThread 是 Android 应⽤的主线程,在 Application 进程中管理执⾏主线程,
调度和执⾏活动和⼴播,和活动管理请求的其他操作。
Looper.loop():循环使⽤ MessageQueue.next() 来获取消息,该函数在 MessageQueue 中
没有消息的时候会阻塞,这⾥采⽤了 epoll 的 I/O 多路复⽤机制,当获取到⼀个消息的时候会
返回。
讲讲 LruCache,除此之外,你还知道哪些缓存算法?
LruCache 中维护了⼀个集合 LinkedHashMap ,该 LinkedHashMap 是以访问顺序排序的。当调⽤
put() ⽅法时,就会在结合中添加元素,并调⽤ trimToSize() 判断缓存是否已满,如果满了就
⽤ LinkedHashMap 的迭代器删除队尾元素,即近期最少访问的元素。当调⽤ get() ⽅法访问缓
存对象时,就会调⽤ LinkedHashMap 的 get() ⽅法获得对应集合元素,同时会更新该元素到队
头。
除了我们常⽤的 LRU 缓存,实际上我们在近来停更的 ImageLoader 库⾥⾯可以看到其他的缓存算
法,⽐如根据缓存对象被使⽤的频率来处理的 LFU 算法;⽐如根据给对象设置失效期的 Simle timebased 算法;⽐如超过指定缓存,就移除栈内最⼤内存的缓存对象的 LargestLimitedMemoryCache
算法。
你是如何进⾏ APK 瘦身的?
⾸先我们得知道 APK ⽂件⾥⾯都是由哪些⽂件构成的,通过 APK Analyzer 我们可以得知,APK 占⽤
最⼤的两块是 lib 和 res。
在满⾜要求的情况下,我们可以指定更少的 so 库进⾏优化,通常情况下直接指定 armeabi 或者
armeabi-v7a 就可以了。
对于图⽚,我们可以通过 ImageOptim 进⾏图⽚压缩,虽然官⽅提供了 shrinkResources 设置
项,但由于该项设置有⻛险就没有处理。
对于 dex ⽂件,删除了不少⽆⽤库(这些库都是早期为了兼容低版本⼿机)
简述 Android 事件传递机制,什么时候会触发 ACTION_CANCLE。
详情可点击:⾯试:从源码的⻆度谈谈 Android 的事件传递机制
我们先说 ACTION_DOWN 事件。当⽤户按下屏幕的时候,事件产⽣并传递给了 Activity 并调⽤
Activity 的 dispatchTouchEvent() ⽅法,如果 Activity 没有调⽤ onInterceptTouchEvent()
进⾏事件拦截,则会传递给 DecorView 。由于 DecorView 继承⾃ FrameLayout ,⽽
FrameLayout 是 ViewGroup 的⼦类,所以直接会调⽤ ViewGroup 的
dispatchTouchEvent() ⽅法。在 ViewGroup 中同样要调⽤ onInterceptTouchEvent() 查看
是否要拦截,默认返回 false,不过我们可以直接改写覆盖它。如果 ViewGroup 没有做事件拦截,
则会通过⼀个 for 循环倒序遍历该 ViewGroup 下的所有⼦ View 的 dispatchTouchEvent() ,在
该⽅法中,会查看该 View 是否 enable,再查看是否᯿写了 OnTouchListener 的 onTouch() ⽅
法,这也是 onTouch() 优先于 onTouchEvent() 的原因。在 View 的 onTouchEvent() ⽅法
中,只要 View 的 CLICKABLE 或者 LONG_CLICKABLE 有⼀个为 true,那么 onTouchEvent() 就
会消耗这个事件,接着就会在 ACTION_UP 事件中调⽤ performClick() ⽅法。如果⼦ View 没有
消耗这个事件,则会传递回给 ViewGroup 。如果 ViewGroup 也没有消耗这个事件,则向上传递给
Activity。
⽽对 ACTION_UP 和 ACTION_MOVE 事件就不⼀样了。不管 ACTION_DOWN 事件在哪个控件消费了
(return true),那么 ACTION_UP 和 ACTION_MOVE 就会从上往下(通过
dispatchTouchEvent() ⽅法)做事件分发向下传,就只会传递到这个控件,⽽不会选择继续往下
传递。如果 ACTION_DOWN 事件是在 dispatchTouchEvent() 消费,那么事件到此停⽌传递;如
果 ACTION_DOWN 事件是在 onTouchEvent() 消费,那么就会把 ACTION_UP 和 ACTION_MOVE
传递给该控件的 onTouchEvent() 处理并结束传递。
处理滑动冲突:主要分为外部拦截法和内部拦截法。
外部拦截法:通过᯿写⽗ View 的 onInterceptTouchEvent() ,根据业务逻辑需要在
ACTION_MOVE ⾥⾯进⾏处理。
内部拦截法:通过᯿写⼦ View 的 dispatchTouchEvent() ,根据业务逻辑决定⾃⼰消
费还是⽗ View 处理,主要通过 View 的 requestDisallowInterceptTouchEvent()
来处理。
对于 ACTION_CANCLE 的调⽤时机,我之前看过系统⽂档的解释是这样的:在设计设置⻚⾯的滑动开
关的时候,如果不监听 ACTION_CANCEL ,在滑动到中间时,如果你⼿指上下移动,就是移动到开关
控件之外,则此时会触发 ACTION_CANCLE ⽽不是 ACTION_UP ,造成开关的按钮停顿在中间位
置。实际上我也去写了⼀个⼩ demo 做尝试,我们知道 ViewGroup 是⼀个能放置其他 View 的布局
类,在每次事件分发的过程中,它都会调⽤⾃⼰的 onInterceptTouchEvent() 来判断是否拦截事
件。假如前⾯⼀直返回 false 在让⼦ View 处理事件,这时候突然 onInterceptTouchEvent() 返回
true 进⾏事件拦截,这时候便会在⼦ View 中响应 ACTION_CANCEL 了。
Android 的多点触摸机制
简述 Android 的绘制流程。
View 的绘制流程是从 ViewRootImpl 的 performTraversals() ⽅法开始,其内部会调⽤
performMeasure()、performLayout()、performDraw()。
performMeasure():
performMeasure() 会调⽤最外层的 ViewGroup 的 measure(),measure() 会回调
onMeasure()。ViewGroup 的 onMeasure() 是抽象⽅法,但其提供了 measureChildren()。这
会遍历⼦ View 然后循环调⽤ measureChild(),measureChild() 中会⽤
getChildMeasureSpec()、⽗ View 的 MeasureSpec 和⼦ View 的 LayoutParam ⼀起获取本
View 的 MeasureSpec。然后再调⽤ View 的 measure(),View 的 measure() 再调⽤ Viwe 的
onMeasure()。该⽅法默认返回的是 measureSpec 的测量值,所以我们要实现⾃定义的
wrap_content 需要᯿写该⽅法。
MeasureSpec (View 的内部类)测量规格为 int型,值由⾼ 2 位规格模式 specMode 和 低 30 位具体尺⼨ specSize 组成。其中 specMode 只有三种值:
MeasureSpec.EXACTLY //确定模式,⽗ View 希望⼦ View 的⼤⼩是确定的,由
specSize 决定;
MeasureSpec.AT_MOST //最多模式,⽗ View 希望⼦ View 的⼤⼩最多是 specSize
指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,⽗View完全依据⼦View的设计值来决
定;
最顶层 DecorView 测量时的 MeasureSpec 是由 ViewRootImpl 中
getRootMeasureSpec() ⽅法确定的,LayoutParams 宽⾼参数均为MATCH_PARENT,
specMode 是 EXACTLY,specSize 为物理屏幕⼤⼩。
只要是 ViewGroup 的⼦类就必须要求 LayoutParams 继承⼦ MarginLayoutParams,否
则⽆法使⽤ layout_margin 参数。
View 的布局⼤⼩由⽗ View 和⼦ View 共同决定。
使⽤ View 的 getMeasuredWidth() 和 getMeasuredHeight() ⽅法来获取 View 测量
的宽⾼,必须保证这两个⽅法在 onMeasure() 流程之后被调⽤才能返回有效值。
View 的测量宽⾼和实际的宽⾼有区别么?
基本上 99% 都是没区别的,但存在特殊情况,有时候可能会因为某种原因对 View 进
⾏多次测量,这样每次测量的⼤⼩可能是不相等的,但这样的情况下,最后⼀次测量
基本是⼀致的。
View 的 MeasureSpec 由谁决定?
除了 DecorView 以外,其它 View 的 MeasureSpec 都是由⾃⼰的 LayoutParams 和
⽗容器⼀起决定的。⽽ DecorView 的测量存在于 ViewRootImpl 的源码中。
⾃定义 View 中如何没有处理 wrap_content 情况,会发⽣什么?为什么?如何解
决?
会发⽣设置成 match_parent ⼀样的效果,View 设置为 wrap_content,实际上就是
MeasureSpec 的 AT_MOST 模式,如果没有处理,测量出来的宽⾼则会是测量的⽗
布局的剩余容量⼤⼩,实际上这样是和设置为 match_parent 是⼀样的。解决⽅案就
是给⾃定义 View 设置⼀个默认的⼤⼩,这样的话在 wrap_content 的时候就显示默
认的⼤⼩了。
performLayout():
performLayout() 会调⽤最外层 ViewGroup 的 layout() ⽅法,layout() ⽅法通过调⽤抽象⽅法
onLayout() 来确定⼦ View 的位置。
使⽤ View 的 getWidth() 和 getHeight() ⽅法来获取 View 测量的宽⾼,必须保证这两个⽅
法在 onLayout() 流程之后被调⽤才能返回有效值。
perfomrDraw():
performDraw() ⽅法会调⽤最外层的 ViewGroup 的 draw(),其中会先后绘制背景、绘制⾃⼰、
绘制⼦ View、绘制装饰。
ViewRootImpl 中的代码会创建⼀个 Canvas 对象,然后调⽤ View 的 draw() ⽅法来执⾏具体的
绘制⼯作。主要点为:
如果该 View 是⼀个 ViewGroup,则需要递归绘制其所包含的所有⼦ View。
View 默认不会绘制任何内容,真正的绘制都需要⾃⼰在⼦类中实现。
View 的绘制是借助 onDraw() ⽅法传⼊的 Canvas 类来进⾏的。
在获取画布剪切区(每个 View 的 draw 中传⼊的 Canvas )时会⾃动处理掉
padding ,⼦ View 获取 Canvas 不⽤关注这些逻辑,只⽤关⼼如何绘制即可。
默认情况下⼦ View 的 ViewGroup.drawChild 绘制顺序和⼦ View 被添加的顺序⼀
致,但是你也可以᯿载 ViewGroup.getChildDrawingOrder() ⽅法提供不同顺序。
Android 的进程间通信,Linux 操作系统的进程间通信。
Linux 操作系统的所有进程间通信 IPC 包括:
1. 管道:在创建时分配⼀个 page ⼤⼩的内存,缓存区⼤⼩⽐较有限;
2. 消息队列:信息复制两次,额外的 CPU ⼩号,不适合频繁或者信息量⼤的通信;
3. 共享内存:⽆需复制,共享缓冲区直接附加到进程虚拟地址空间,速度快。但进程间的同步问题
操作系统⽆法实现,必须各进程利⽤同步⼯具解决;
4. 套接字:作为更通⽤的接⼝,传输效率低,主要⽤于不同机器或跨⽹络的通信;
5. 信号量:常作为⼀种锁机制,防⽌某进程正在访问共享资源时,其它进程也访问该资源。因此它
主要作为进程间以及同⼀进程内不同线程之间的同步⼿段;
6. 信号:不适⽤于信息交换,更适⽤于进程中断控制,⽐如⾮法内存访问,杀死某个进程等。
Android 内核也是基于虚拟机内核,但它的 IPC 通信却采⽤ Binder,是有原因的:
性能优越:Binder 数据拷⻉仅需⼀次,⽽管道、消息队列、Socket 均需要两次,所以 Binder
性能仅次于共享内存(共享内存不需要拷⻉数据)。
因为 Linux 内核没有直接从⼀个⽤户空间到另⼀个⽤户空间直接拷⻉的函数,需要先⽤
copy_from_user() 拷⻉到内核空间,再⽤ copy_to_user() 拷⻉到另外⼀个⽤户空
间。⽽在 Android 中,为了实现⽤户空间到⽤户空间的拷⻉, mmap() 分配的内存除了
映射进了接收⽅进程⾥,还映射到了内核空间。所以调⽤ copy_from_user() 将数据拷
⻉进内核空间也相当于拷⻉进了接收⽅的⽤户空间。
稳定性⾼:Binder 基于 C/S 架构,稳定性明显优于不分客户端和服务器端的共享内存⽅式;
安全性⾼:为每个 APP 分配 UID,进程的 UID 是鉴别进程身份的᯿要标志。
Android 的 IPC 通信⽅式。
1. 使⽤ Bundle 的⽅式
因为 Bundle 实现了 Parceable 接⼝,所以可以很⽅便的在不同进程之间传输,传输的数据必须
是能够被反序列化的数据或基本数据类型。Bundle 的⽅式简单轻便,但只能单⽅⾯传输数据,
使⽤有局限。所以仅仅适合四⼤组件的进程间通信。
2. 使⽤⽂件共享的⽅式
共享⽂件也是⼀种不错的进程间通信⽅式,两个进程通过读写同⼀个⽂件来实现交换数据。但这
样的⽅式在并发编程的时候容易出问题。所以只适合没有并发的场景交换⼀些简单的数据。
3. 使⽤ Messenger 的⽅式
Messenger 底层实现是 AIDL,功能⼀般,⽀持⼀对多的串⾏通信,⽀持实时通信。但不能很好
的处理并发现象,不⽀持 RPC,只能传输 Bundle ⽀持的数据类型。适⽤于低并发的⼀对多即时
通信,⽆ RPC 需求。
4. 使⽤ AIDL 的⽅式
AIDL 是⼀种 IDL 语⾔,功能强⼤,⽀持⼀对多的并发通信,⽀持实时通信。但使⽤稍复杂,需
要处理好线程同步,适合于⼀对多通信⽽且有 RPC 需求。
在我印象中,AIDL 分为两类,⼀类是定义 Parcelable 对象,以供其他 AIDL ⽂件使⽤。另⼀类
是定义⽅法接⼝,以供系统使⽤来完成跨进程通信。
它⽀持基本数据类型和实现 Parcelable 的类型。传参除了 Java 基本类型和 String、
CharSequence 之外其他的都必须在前⾯加上 tag 参数。分别是:
in:表示数据只能由客户端流向服务端;
out:表示数据只能由服务端流向客户端;
inout:表示数据可以在服务端和客户端双向流通。
5. 使⽤ ContentProvider 的⽅式
ContentProvider 数据访问功能⽅⾯特别强⼤,⽀持⼀对多并发数据共享,但却是⼀种受约束的
AIDL,主要提供数据源的增删查改操作,适⽤于⼀对多的进程间数据共享。
6. Socket
Socket ⽅式功能强⼤,可通过⽹络传输字节流,⽀持⼀对多的并发实时通信,但其使⽤繁琐,
不⽀持直接的 RPC,适⽤于⽹络数据交换。
⼀个 APP 的程序⼊⼝是什么?整个 APP 的主线程的消息循环是在哪⾥创建的?
APP 的程序⼊⼝是 ActivityThread.main()。 在 ActivityThread 初始化的时候,就已经创建消息循环了,所以在主线程⾥⾯创建 Handler 不需要指
定 Looper,⽽如果在其他线程使⽤ Handler,则需要单独使⽤ Looper.prepare() 和 Looper.loop()
创建消息循环。
简述 APP 的启动流程
1. ⾸先是调⽤ startActivity(intent),通过 Binder IPC 机制,最终调⽤到
ActivityManagerService,这个 Service ⾸先会通过 PackageManager 的 resolveIntent() ⽅法
来收集这个 intent 对象的指向信息;然后通过 grantUriPermissionLocked() ⽅法来验证⽤户是
否有⾜够的权限去调⽤该 intent 对象指向的 Activity。如果有权限,则开始创建进程。
2. 创建进程。
ActivityManagerService 调⽤ startProcessLocked() ⽅法来创建新的进程,这个⽅法会通过
socket 通道传递参数给 Zygote 进程,Zygote 孵化⾃身,并调⽤ ZygoteInit.main() ⽅法来实例
化 ActivityThread 对象并最终返回新进程的 PID。
ActivityThread 随后会在 main ⽅法中依次调⽤ Looper.prepareLoop() 和 Lopper.loop() 来开
启消息循环。这也是在主线程中使⽤ Handler 并不需要使⽤这两个⽅法来开启消息循环的原
因。
3. 绑定 Application
接下来要做的就是将进程和指定的 Application 绑定起来,这个是通过 ActivityThread 对象中调
⽤ bindApplication() ⽅法完成的,这个⽅法发送⼀个 BIND_APPLICATION 的消息到消息队列
中,最终通过 handleBindApplication() ⽅法来处理这个消息,然后调⽤ makeApplication() ⽅
法来加载 APP 的 classes 到内存中。
4. 启动 Activity
经过前⾯的步骤,系统已经拥有了该 Application 的进程,后⾯的话就是普通的从⼀个已经存在
的进程中启动⼀个新进程的 activity 了。实际调⽤⽅法详⻅ Activity 的启动过程。
简述 Activity 的启动过程。
⾸先还是得当前系统中有没有拥有这个 Application 的进程。如果没有,则需要处理 APP 的启动过
程。在经过创建进程、绑定 Application 步骤后,才真正开始启动 Activity 的⽅法。 startActivity() ⽅
法最终还是调⽤的 startActivityForResult()。 在 startActivityForResult() 中,真正去打开 Activity 的实现是在 Instrumentation 的
execStartActivivity() ⽅法中。
在 execStartActivity() 中采⽤ checkStartActivityResult() 检查在 manifest 中是否已经注册,如果没
有注册则抛出异常。否则把打开 Activity 的任务交给 ActivityThread 的内部类 ApplicationThread,
该类实现了 IApplicationThread 接⼝。这个类完全搞定了 onCreate()、onStart() 等 Activity 的⽣命
周期回调⽅法。
在 ApplicationThread 类中,有⼀个⽅法叫 scheduleLaunchActivity(),它可以构造⼀个 Activity 记
录,然后发送⼀个消息给事先定义好的 Handler。
这个 Handler 负责根据 LAUNCH_ACTIVITY 的类型来做不同的 Activity 启动⽅式。其中有⼀个᯿要的
⽅法 handleLaunchActivity() 。 在 handleLaunchActivity() 中,会把启动 Activity 交给 performLaunchActivity() ⽅法。
在 performLaunchActivity() ⽅法中,⾸先从 Intent 中解析出⽬标 Activity 的启动参数,然后⽤
ClassLoader 将⽬标 Activity 的类通过类名加载出来并⽤ newInstance() 来实例化⼀个对象。
创建完毕后, 开始调⽤ Activity 的 onCreate() ⽅法,⾄此,Activity 被成功启动。
Bundle 的数据结构,如何存储?既然有了 Intent.putExtra(),为何还需要
Bundle?
Bundle 内部是采⽤ ArrayMap 进⾏存储的,并不是 HashMap 。⽽ ArrayMap 内部是使⽤两个
数组进⾏数据存储,⼀个数组记录 key 的 hash 值,另⼀个数组记录 value 值,内部使⽤⼆分法对
key 进⾏排序,并使⽤⼆分法进⾏添加、删除、查找数据,因此它只适合于⼩数据量操作。
Intent 可以附加的数据类型,⼤多数都是基础类型,⽽ Bundle 添加数据的功能更加强⼤。⽐如
有⼀些可以使⽤ Bundle 的场景:
Activity A 传递给 Activity B 再传递给 Activity C,如果⽤ Intent 的话,需要不断地取和存,
⽐较麻烦,但⽤ Bundle 就很好解决了这个问题;
在设备旋转的时候保存数据,我们就会⽤ Bundle ; 在 Fragment 中,传递数据采⽤ Bundle 。
之所以不⽤ HashMap 是因为 HashMap 内部使⽤是数组 + 链表的结构(JDK 1.8 增加了红⿊
树),在数据量较少的情况下,HashMap 的 Entry Array 会⽐ ArrayMap 占⽤更多的内存。 ⽽
Bundle 的使⽤场景是⼩数据量,实际上 SDK 也对数据量做了限制,在数据量太⼤的时候使⽤
Bundle 传递会抛异常。所以相⽐之下,使⽤ Bundle 来传递数据,可以保证更快的速度和更少
的内存占⽤。
Android 图⽚加载框架对⽐区别。
ImageLoader:⽀持下载进度监听,可以在 View 滚动的时候暂停图⽚的加载,默认实现多种内
存缓存的⽅法,⽐如最⼤最先删除、使⽤最少最先删除、最近最少使⽤最先删除、先进先删除
等,⽽且也可以⾃⼰配置缓存算法,可惜现在已经不再维护,该库使⽤前还需要进⾏配置。
Picasso:包⽐较⼩,可以取消不在视ᰀ范围内图⽚资源的加载,并可使⽤最少的内存完成复杂
的图⽚转换,可以⾃动添加⼆级缓存,⽀持任务调度优先级处理,并发线程数可以根据⽹络类型
进⾏调整,图⽚的本地缓存交给了 OkHttp 处理,可以控制图⽚的过期时间。但功能⽐较简单,
⾃身并不能实现「本地缓存」的功能。
Glide:⽀持多种图⽚格式缓存,适⽤于更多的内容表现形式,⽐如 Gif,WebP、缩略图、
Video 等,⽀持根据 Activity 或者 Fragment 的⽣命周期管理图⽚加载请求;对 Bitmap 的复⽤
和主动回收做的较好,缓存策略做的⽐较⾼效和灵活(Picasso 只会缓存原始尺⼨的图⽚,⽽
Glide 缓存采⽤的是多种规格)。加载速度快且内存开销⼩(默认 Bitmap 格式的不同,使得内
存开销是 Picasso 的⼀半)。但⽅法较多较复杂,因为相当于 Picasso 的改进,包较⼤但总的来
说影响不⼤。
Glide 还可以设置 Gif 图次数以及可以设置 Gif 的第⼀帧显示,⽐如常⻅的地址展示 Gif 动
画。
Fresco:最⼤的优势莫过于在 5.0 以下设备的 Bitmap 加载。在 5.0 以下的系统, Fresco 会将
图⽚放到⼀个特别的内存区域,⼤⼤减少 OOM(会在更底层的 Native 层对 OOM 进⾏处理,
图⽚将不再占⽤ APP 的内存),所以它相当适⽤于⾼性能加载⼤量图⽚的场景。但它的包太⼤
也⼀直为⼈诟病,⽽且⽤法⽐较复杂,底层还会涉及 C++ 领域。
讲讲 Glide 的三级缓存
Glide 的三级缓存为内存缓存、磁盘缓存和⽹络缓存,默认采⽤的是内存缓存。
有缓存,必定就有缓存 key,之前简单看了⼀下 key 的⽣成⽅法,真的繁琐,传了整整 10 个参数。
不过逻辑也不是很复杂,主要就是᯿写 equals() 和 hashCode() ⽅法来保证 Key 的唯⼀性。
内存缓存
Glide 默认会采⽤内存缓存,当然可以通过 skipMemoryCache(true) ⽅法禁⽤。Glide 的内存缓
存其实也是使⽤了 LruCache 算法,它的主要原理就是把最近使⽤过的对象放在
LinkedHashMap 中,并且把最近最少使⽤的对象在缓存值达到预设值之前从内存中删除。不过
在 Glide 中,除了 LruCache 算法以外,Glide 还结合了⼀种弱引⽤机制,共同完成缓存功能。
所以在获取的时候,会先⽤ LruCache 的 loadFromCache() ⽅法来获取,如果获取到则会将它
从缓存区移除,然后再把这个图⽚存储到 activeResource 中。这个 activeResource 就是⼀个缓
存的 HashMap,⽤来缓存正在使⽤中的图⽚,这样可以保护这些图⽚不会被 LruCache 算法回
收掉。如果没有获取到再通过弱引⽤机制的 loadFromActiveResources() ⽅法来获取缓存图⽚。
若是都没有获取到,才会向下执⾏。
在 Glide 中,有⼀个变量,当这个变量⼤于 0,则代表图⽚正在使⽤中,就放到 activeResource
弱引⽤缓存中。如果这个变量变成 0 以后,我们就从弱引⽤中移除,并 put 到
LruResourceCache 中。
硬盘缓存
硬盘缓存,有 4 种模式,默认只会缓存转换过后的图⽚,另外也可以选择只缓存原始图⽚或者既
缓存原始图⽚也缓存转换过的图⽚,甚⾄是选择不缓存。
和内存缓存类似,硬盘缓存的实现也是采⽤的 LruCache 算法。Glide 会优先从磁盘缓存中读取
图⽚,只有从缓存中读取不到图⽚时,才会去读取原始图⽚。
在缓存原始图⽚的时候,其实传⼊的 Key 和之前的 Key 不⼀样,这是因为原始图⽚并不需要那
么多的参数,所以这个 Key 的⽣成不需要那么多的参数。
主线程中 Looper.loop() ⽆限循环为什么不会造成 ANR?
Looper.loop() 主要功能是不断地接收事件和处理事件,只能说是某⼀个消息或者说消息的处理阻
塞了 Looper.loop() ,⽽不是 Looper.loop() 阻塞了它。总的来说, Looper.loop() ⽅法可
能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能⼀直处理事件就不会造成 ANR。
RxJava 2 的背压是什么?如何形成的?⼜如何解决?
被观察者发送消息太快以⾄于它的操作符或者订阅者不能及时处理相关的消息,从⽽操作消息造成阻
塞的现象叫做背压。
在 RxJava 1.0 中,背压事件的缓存尺很⼩,只有 16,所以不能处理较⼤量的并发事件,⽽且被观察
者⽆法得知观察者对事件的处理能⼒和事件处理进度,只知道把时间⼀股脑抛给观察者,所以操作很
多事件不能被背压,抛出我们闻名的 MissingBackpressureException 异常。
常规解决⽅案:
既然背压是由于被观察者发送事件太快或者太⼤所导致,所以我们肯定可以通过⼀定的⽅法让被
观察者发送事件更慢;
使⽤ filter 过滤操作符或者 sample 操作符让观察者少处理⼀些事件;
很明显,我们在正常开发中这样的解决⽅案肯定不可取,因为我们不知道应该让被观察者保持⼀个怎
样的频率才是最佳的,所以在 RxJava 2.0 中官⽅推出了 Flowable 和 Subscriber 来⽀持背压,
同时去除了 Observable 对背压的⽀持。 Flowable 在设计的时候实际上采⽤了⼀种新的思路,也
就是「响应式拉取」的⽅式来处理处理事件和发出事件速率不均衡的问题。在 Flowable 的使⽤
中,我们会有⼏种背压策略,并在 create() 的时候作为参数传进去。值得注意的是我们可以通过
调⽤ request() ⽅法来定义观察者处理事件的能⼒,在被观察者上⾯可以通过该 requested 变
量来获取下游的处理能⼒(这个值最⼤能得到 128,因为它是内部调⽤的)。这样只要被观察者根据
观察者的处理能⼒来决定发送多少事件,就可以避免发出⼀堆事件从⽽导致 OOM。这也就完美的解
决了事件丢失的问题,⼜解决了速度的问题。如果我们没有调⽤ request() 进⾏设置,依然会出现
收不到消息(不在同⼀个线程,且没有超过缓存区的 128 个)或直接抛出异常(在同⼀个⼯作线
程)。
RxJava 2.0 背压的策略确实相对 1.0 版本提升了很多,但是真的没有我们想象的完美,因为丢失的事
件不⼀定是我们想要丢失的,所以还是应该根据实际需求来制定防阻塞策略。
RxJava 2.0 中增加了背压策略模式:
ERROR:处理跟不上发送速度时报异常;
MISSING:如果流的速度⽆法同步可能会报异常;
BUFFER:和 1.0 的 Observable ⼀样,超过缓存区直接报异常;
DROP:跟不上速度直接丢弃;
LASTED:⼀直保留最新值,直到被下游消费掉;
⾃定义 View 优化
频繁调⽤的⽅法尽量减少不必要的代码,⽐如 onDraw() ,尽量不要做内存分配的事情;
尽量地减少 onDraw() 的调⽤次数,如果可以尽量少使⽤⽆参数的 invalidate() ,⽽选择
4 个参数的 invalidate() 进⾏局部᯿绘;
避免使⽤ requestLayout() , requestLayout() 的执⾏会去᯿新 measure ,这样的计算
⼀旦遇到冲突,将需要⽐较⻓的时间;
布局优化的措施
1. 降低 Overdraw,减少不必要额背景绘制;
2. 减少嵌套和空间的个数:RelativeLayout 和 LinearLayout 都需要 measure 两次才能完成,所
以⼀定要尽量少嵌套;
使⽤ merge 标签减少 UI 的层级,提⾼加载速度,替代 FrameLayout 或者当⼀个布局
include 另外⼀个布局的时候,主要⽤于消除⽬录结构中的多余 ViewGroup;
使⽤ ViewStub,作⽤和 include 类似,也是加载另外⼀个布局,但和 include 不同的是,
ViewStub 引⼊的布局模式是不显示的,不会占⽤ CPU 和内存,所以经常⽤来引⼊那些默
认不显示的布局,⽐如进度条、错误提示等;
APP 如何保证后台服务不被杀死?
主要是两个⽅⾯,提升进程的优先级,降低被杀死的概率,以及在进程被杀死后,进⾏拉活。
提升进程优先级的⽅案:
1. 利⽤ Activity:监控⼿机锁屏解屏事件,在锁屏时启动 1 个像素的 Activity,解锁的时候把
Activity 销毁,该⽅案可以使进程的优先级在屏幕锁屏时间由 4 提升为最⾼优先级 1。
2. 在后台播放⼀段⽆声⾳乐。
3. 利⽤ Notification:Service 的优先级为 4,可以通过 setForeground 把后台 Service 设置为前
台 Service,但是不做处理会是⽤户感知的。解决⽅案是通过实现⼀个内部 Service,在
LiveService 和其内部 Service 中同时发送具有相同 ID 的 Notification,然后将内部 Service 结
束掉。随着内部 Service 的结束,Notification 将会消失,但系统优先级依然保持为2。
进程死后拉活的⽅案:
1. 利⽤系统⼴播拉活。缺点是系统⼴播不可控,只能保证发⽣事件时拉活,⽆法保证进程挂掉后⽴
即拉活;⽽且⼴播接收器被管理软件、系统软件通过「⾃启管理」等功能禁⽤的场景⽆法接收⼴
播,从⽽⽆法᯿启。所以该⽅案多⽤于备⽤⽅案。
2. 利⽤第三⽅应⽤⼴播拉活:⽐如个推 SDK、友盟等。
3. 利⽤系统 Service 机制拉活。把 Service 设置为 START_STICKY,利⽤系统机制在 Service 挂掉
后⾃动拉活,但在短时间只能有 5 次,⽽且取得 Root 权限的管理⼯具或者系统⼯具可以通过
forestop 停⽌掉,⽆法᯿启。
如何定位 ANR,有哪些检测⽅式
定位:如果在开发机器上出现问题,我们可以通过查看 /data/anr/traces.txt 即可,最新的 ANR 信息
会在最开始的部分。或者使⽤ Systrace 和 TraceView 找出影响响应的问题。
TraceView 是 Android SDK ⾃带的⼀个系统性能分析⼯具,可以定位应⽤代码中的耗时操作。
Systrace 是 Android 4.1 新增的应⽤性能数据采样和分析⼯具,需要借助 chrome 浏览器。
检测:可以使⽤ BlockCanary 分析 Android 的 ANR。
介绍下 MVP 模式,为什么从 MVC 重构到了 MVP?知道 MVVM 么?
MVP 实际上就是 MVC 的变种,MVP 把 Activity/Fragment 中的 UI 逻辑抽象成了 View 接⼝,把业务
逻辑抽象成 Presenter 接⼝,Model 类还是原来的 Model。主要有以下好处:
Activity 代码更加简洁,耦合性更低,可读性更⾼,各板块各司其职;
⽅便进⾏单元测试,因为Presenter 被抽象成接⼝,可以有多种具体的实现;
可以有效避免内存泄漏。在 MVC 模式中,异步任务对 UI 的操作会放在 Activity 中,所以异步任
务会持有 Activity 的引⽤,这在特定情况下肯定会造成内存泄漏的。
MVVM 虽然没有使⽤过,但却有所了解,最开始了解还是从 Google 2015 推出的 databinding 开始
的。MVVM 包含 View、Model、ViewModel 三个部分。核⼼思想和 MVP 类似,利⽤数据绑定、依
赖属性、命令、路由事件等新特性,打造了⼀个更加灵活⾼效的架构。
View 对应于 Activity 和 XML,负责 View 的绘制以及⽤户交互;
Model 同样是代表实体模型;
ViewModel:负责 View 和 Model 间的交互,负责业务逻辑。
Parcelable 和 Serializable 的区别。
Parcelable 和 Serializable 都⽀持序列化和反序列化操作,但 Serializable 使⽤ I/O 读写存储在硬盘
上,⽽ Parcelable 是直接在内存中读写,Parcelable 的性能⽐ Serializable 好,在内存开销⽅⾯较
⼩,所以在内存间做数据传输时推荐使⽤ Parcelable,如在 Activity 之间传输数据。
⽽ Serializable 对数据持久化更⽅便保存,所以在保存或⽹络传输数据时选择 Serializable,因为
Android 不同版本 Parcelable 可能不同,所以不推荐使⽤ Parcelable 进⾏数据持久化。
System.gc() 和 Runtime.gc() 的区别
System.gc() 和 Runtime.gc() 是等效的,在 System.gc() 内部也是调⽤的
Runtime.gc() 。调⽤两者都是通知虚拟机要进⾏ gc,但是否⽴即回收还是延迟回收,由 JVM 决
定。两者唯⼀的区别就是⼀个是类⽅法,⼀个是实例⽅法。
算法 && Other
如何在 100 亿个数中找到最⼤的 100 个数。
因为数字很多,所以可以先⽤ hash 法进⾏去᯿,如果᯿复率很⾼的话,这样可以减少很多的内存⽤
量,从⽽缩⼩运算空间。然后再采⽤最⼩堆的⽅式进⾏处理,即先读取前 100 个数,形成最⼩堆,保
证堆顶的数最⼩,然后依次读取剩余数字,只要出现⽐堆顶元素⼤的元素,则替换堆顶元素并᯿新调
整堆为最⼩堆,直到读取完成,最⼩堆中的元素就是最⼤的 100 个数。
⽣成最⼩堆⽅式:
private static void heapAdjustMin(int[] arr, int i, int n) {
int temp = arr[i];
// j 代表左结点
int j = 2 * i + 1;
while (j < n) {
// 我们先找出⼩的,如果满⾜说明右结点⽐较⼩
if (j + 1 < n && arr[j] > arr[j + 1])
++j;
// 此时 arr[j] 就是最⼩值了
算法题:⼀个⾏列都有序的数组,给定⼀个数,找出具体的位置
如果都是升序,则从最右边开始找,⼩了就往下找,⼤了就往左找。
TCP 和 UDP 区别是什么?简单说⼀下 TCP 三次握⼿和四次挥⼿协议。
TCP 和 UDP 同为数据传输协议,但 TCP 更加强调安全,它⾯向字节流,需要先通过三次握⼿协议建
⽴连接后才可以进⾏数据传输。⽽ UDP 更强调速度,它⾯向报⽂传输(数据报有⻓度),⽆需连接,所
以带来的优势是效率⾼,⽽安全性低也⼀直为⼈诟病。
TCP 并不能保证数据⼀定会被对⽅收到,只能够做到如果有可能,就把数据传递到接收⽅,否
则就通知⽤户(通过放弃᯿传并且中断连接这⼀⼿段)。因此准确说 TCP 也不是 100% 可靠的
协议,它所能提供的是数据的可靠递送或故障的可靠通知。
对于 TCP 三次握⼿:
第⼀次握⼿:客户端发送 SYN 报⽂,并置发送序号为 X,然后客户端进⼊ SYN_SEND 状态,等
待服务器确认;
第⼆次握⼿:服务器收到 SYN 报⽂段,然后发送 SYN + ACK 报⽂,并置发送序号为 Y,在确认
序号为 X + 1,发送后服务器进⼊ SYN_RECV 状态;
第三次握⼿:客户端收到服务器的 SYN + ACK 报⽂段,然后向服务器发送 ACK 报⽂,并置发送
序号为 Z,在确认序号为 Y + 1,服务器和客户端都进如 ESTABLISHED 状态,完成 TCP 三次握
⼿。
进⾏三次握⼿,主要是为了防⽌服务器端⼀直等待⽽浪费资源。
对于 TCP 四次挥⼿:
第⼀次挥⼿:客户端发送⼀个 FIN 标志位置为 1 的包,表示⾃⼰已经没有数据可以发送,但仍可
以接收数据,发送完毕后,客户端进⼊ FIN_WAIT_1 状态;
第⼆次挥⼿:服务器确认客户端的 FIN 包,发送⼀个确认包,表明⾃⼰接收到了客户端关闭连接
的请求,但还没准备好关闭连接。发送完毕后,服务器端进⼊ CLOSE_WAIT 状态,客户端接收
到这个确认包后,进⼊ FIN_WAIT_2 状态,等待服务器端关闭连接;
第三次挥⼿:服务器端准备关闭连接时,向客户端发送结束连接请求,FIN 置为 1。发送完毕
后,服务器端进⼊ LAST_ACK 状态,等待来⾃客户端的最后⼀个 ACK;
第四次挥⼿:客户端接收到来⾃服务器端的关闭请求,发送⼀个确认包,并进⼊到 TIME_WAIT
状态,等待可能出现的要求᯿传的 ACK 包;服务器端接收到这个确认包之后,关闭连接,进⼊
CLOSE 状态;客户端等待了某个固定时间之后,没有收到服务器端的 ACK,认为服务器端已经
正常关闭连接,于是⾃⼰也关闭连接,进⼊ CLOSE 状态。
// 如果 ⽗结点⽐左右结点最⼩的都⼩,说明已经符合条件
if (temp <= arr[j])
break;
// 否则的话就把最⼩的值和 ⽗结点对换;
arr[i] = arr[j];
i = j;
j = 2 * i + 1;
}
arr[i] = temp;
}
HTTP 状态码
1XX:指示信息,表示请求已接收,继续处理
2XX:成功:常⻅ 200
3XX:᯿定向:常⻅ 301:请求永久᯿定向;302:请求临时᯿定向;
4XX:客户端错误:常⻅ 400:客户端请求语法错误,服务器⽆法理解;401:请求未经授权;
403:服务器收到请求,但拒绝服务;
5XX:服务器错误:常⻅ 500:服务器发⽣不可预期的 URL;503:服务器当前不能处理客户端
请求,⼀段时间后恢复正常。
讲讲 Kotlin 或者 Python 的 Lambda 表达式和⾼阶函数。
Lambda 表达式其实就是⼀个未声明的函数表达式(匿名函数)。其优势有 3 个:
⽐较轻便,即⽤即扔,很适合需要完成⼀项功能,但是此功能只在此⼀处使⽤,连名字都很随意
的情况下。
作为匿名函数,⼀般⽤来给 filter、map 这样的函数式编程服务;
可以作为回调函数,传递给某些应⽤,⽐如消息处理。
⽽⾼阶函数就是把函数作为参数或者返回值的函数。
⼿写线程安全的单例模式代码
针对⽬前的简历
// 懒汉式
public class Single{
private volatile static Single instance;
private SIngles(){}
public static Single getInstance(){
if (instance == null){
synchronized(Single.class){
if (instance == null)
instance = new Single();
}
}
return instance;
}
}
// 内部类⽅案
public class Single{
private Single(){}
private static class Holder{
private static Single INSTANCE = new Single();
}
public static Single getInstance(){
return Hodler.INSTANCE;
}
}
针对⽬前的简历
Handler 机制引出的 ThreadLocal,⼏种常⽤的 ThreadLocal,讲讲原理。
ThreadLocal 是⼀个线程内部的数据存储类,每个线程都会保存⼀个 ThreadLocalMap 的实例
threadlocals。ThreadLocalMap 是 ThreadLocal 的内部类,所以每个 Thread 都有⼀个对应的
ThreadLocalMap。⾥⾯保存了⼀个 Entry 数组,这个数组专⻔⽤于保存 ThreadLocal 的值。通过这
样的⽅式,就能让我们在多个线程中互不⼲扰存储和修改数据。
ThreadLocal 是⼀个泛型类,其主要核⼼是 set() 和 get() ⽅法,在 set ⽅法中,我们先获取当前线
程,再根据当前线程获取 ThreadLocalMap,如果该 ThreadLocalMap 不为空,就把 ThreadLocal
和我们想存放的数据设置进去。如果 ThreadLocalMap 为空的话,则先创建 ThreadLocalMap 后再
设置数据。
在 get ⽅法中,同样是先取出当前的 ThreadLocalMap 对象,如果该对象为空,则调⽤
setInitialValue(),默认情况下 initialValue() ⽅法返回为 null,不过我们可以通过᯿写 initialValue()
⽅法来修改它。。如果取出的 ThreadLocalMap 对象不为空,那就取出它的 table 数组并找出对应的
数据。
因为 ThreadLocal 的 get() 和 set() ⽅法操作的对象都是当前线程的 ThreadLocalMap 对象的 table 数
组,它的所有读写操作都来⾃各⾃线程的内部,所以才能做到不同线程互不⼲扰存储和修改数据。
在 Android 中,你是如何做系统签名的?
1. ⾸先必须在应⽤的 Manifest.xml 中增加 android:sharedUserId="android.uid.system"
2. 然后打包出未签名的 apk,再编译出 signapk.jar、从系统源码⽬录获取 platform.x509.pem、
platorm.pk8 两个⽂件,利⽤ java -jar 命令打包出带系统签名的 apk。
命令为 java -jar signapk.jar platform.x509.pem platform.pk8 未签名.apk 已经签名.apk
讲讲 Android 中常⽤的设计模式
单例模式
主要作⽤:省去创建⼀种对象创建的时间,对系统内存的使⽤频率降低,降低 GC 压⼒,缩短
GC 停顿的时间。
最终优化版本:静态内部类。
建造者模式
适⽤于构造⼀个对象需要很多个参数,并且参数的个数或者类型不固定的时候。⽐如我开源的图
⽚压缩库、Android 中的 AlertDialog、Glide、OkHttp。
适配器模式
在 Android 中应⽤:Adapter
装饰模式
优点是:对于拓展⼀个对象的功能,装饰模式⽐继承更加灵活,不会导致类的个数急剧增加;可
以通过⼀种动态的⽅式来拓展⼀个对象的功能;可以对⼀个对象进⾏多次装饰,通过使⽤不同的
具体装饰类以及这些装饰类的排列组合。
Android 中的应⽤:Context。
HTTP GET 和 POST 的区别
1. GET 的᯿点是从服务器上获取资源;POST 的᯿点是向服务器发送数据。
2. GET 传输数据是通过 URL 请求,以 field = value 的⽅式,这个过程对⽤户是可⻅;⽽ POST 传
输数据通过 HTTP 的 POST 机制,将字段与对应值封存在请求体中发送给服务器,这个过程对⽤
户是不可⻅的。
3. GET 的数据传输量较⼩,因为 URL 的⻓度是有限制的,但效率较⾼;⽽ POST 可以传输⼤量数
据,所以上传⽂件不能⽤ GET ⽅式。
4. GET 是不安全的,因为 URL 是可⻅的,可能会泄漏私密信息;⽽ POST 较 GET 安全性更⾼。
5. GET ⽅式只能⽀持 ASCII 字符,向服务器传的中⽂字符可能会乱码;⽽ POST ⽀持标准字符集,
可以正确传输中⽂字符。
常⻅的 HTTP ⾸部字段
通⽤的⾸部字段(请求报⽂和响应报⽂都会使⽤的⾸部字段)
Date:报⽂创建时间
Connection:连接的管理
Cache-Control:缓存的控制
Transfer-Encoding:请求报⽂的传输编码格式
请求⾸部字段(请求报⽂会使⽤的⾸部字段)
Host:请求资源所在服务器
Accept:可处理的媒体类型
Accept-Charset:可接受的字符集
Accept-Encoding:可接受的内容编码
Accept-Language:可接受的⾃然语⾔
响应⾸部字段(响应报⽂会使⽤的⾸部字段)
Accept-Ranges:可接受的字节范围
Location:令客户端᯿新定向的 URI
Server:HTTP 服务器的安装信息
实体⾸部字段(请求报⽂和响应报⽂的实体部分都会使⽤的⾸部字段)
Allow:资源科⽀持的 HTTP ⽅法
Content-Type:实体主体的类型
Content-Encoding:实体主体适⽤的编码⽅式
Content-Language:实体主体的⾃然语⾔
Content-Length:实体主体的字节数
Content-Range:实体主体的位置范围,⼀般⽤于发出部分请求时使⽤
Cookie 和 Session 的区别
Cookie 和 Session 都是保存客户端状态的机制,它们都是为了解决 HTTP ⽆状态的问题的。它们有明
显的不同:
Cookie 将状态保存在客户端;⽽ Session 把状态保存在服务器端。
Cookie 不是很安全,别⼈可以通过分析存在本地的 Cookie 并进⾏ Cookie 欺骗,考虑到安全应
该⽤ Session。
Session 会在⼀定时间内存在服务器上,当访问增多,会⽐较占⽤服务器的性能,考虑到减轻服
务器性能⽅⾯,应当考虑 Cookie。
介绍下这个 iSchool 协议,并讲下它是如何⽀持原⽣和 H5 交互的?
iSchool 协议是我们的 iSchool 客户端专有的协议,主要⽤于⽹⻚和客户端、服务器和客户端交互。协
议以 iSchool:// 开头,后⾯更上动作命令和参数。
⽬前我们⽀持的参数包括 page 和 function。
page 作⽤于服务器指定客户端开启特定的⻚⾯,实际上我们 APP 的主⻚菜单⼊⼝就是通过接⼝返回
page 协议的⽅式来做跳转映射的。
function 协议主要⽤于⽹⻚ H5 和客户端双向通信专⽤。⽹⻚通过 ischool://function/ 协议调⽤客户
端指定命令,客户端执⾏结果以回调的形式调⽤ JS 提供的。
⽬前我们的 function 协议⽀持是否隐藏原⽣标题栏、打开⼀个新的 URL、安装 APP、播放视频、录
制视频、选择图⽚、⽀付以及分享到微信等功能;
我们会分别定义⼀个 IschoolFunction 和 IschoolPage 接⼝,暴露各种⽅法。然后定义⼀个 Ischool
协议解析器类,⽤于解析协议,解析后把参数存放到 Map 中,⽅便原⽣⻚⾯读取。
你在 iSchool 项⽬中都对分别对 6.0、7.0、8.0 做了哪些适配?
在 6.0 中,当时我加⼊公司时,项⽬还使⽤的 SdkVersion 还是 22,所以不存在权限问题,并且
还在使⽤ HttpClient 库,所以我⾸先是处理了 HttpClient 在 23 版本不能直接使⽤的问题,主要
是在 build.gradle 加上:useLibrary 'org.apache.http.legacy' 这句话。对于动态权限⽅⾯,主
要是⾸先检查应⽤是否有这个权限,如果没有权限则采⽤ requestPermission 来请求权限。
但在 Android 原⽣系统中,如果第⼆次弹出权限申请的对话框,会出现「以后不再弹出」的选
项,当⽤户勾选后,再申请权限 shouldShowRequestPermissionRationale() ⽅法就会返回
true,导致不会弹出权限申请的对话框。我们⼀开始是采⽤弹窗告诉⽤户,但后⾯发现有些⼿
机,⽐如⼩⽶4 就⼀直让 shouldShowRequestPermissionRationale() ⽅法返回 false,但也不
会弹出权限申请对话框。所以我们最终的处理⽅案是在⽤户拒绝权限的回调⾥⾯,直接弹窗提
示,并提供到系统设置⻚⾯进⾏设置的处理。
在 7.0 中,虽然提供了不少新特性,⽐如多窗⼝、通知增强,低电耗模式等,但我们在项⽬中主
要适配了根据路径获取图⽚需要采⽤ FileProvider 的问题,之前我们可能都是使⽤
Uri.fromFile() ⽅法来获取⽂件的 Uri,但 7.0 以后必须采⽤ FileProvider.getUriFromFile() ⽅
法。
在 Andrid 8.0 中,主要适配的是不能再直接安装 APK,⽽需要在 manifest.xml ⽂件中增加
REQUEST_INSTALL_PACKAGES 权限。
你是怎么处理各种机型的屏幕适配的?
在我们的项⽬中,对 Android 的各种机型做适配可谓是花了⼤⼯夫。
1. ⾸先 Android 提供了 dp ⽅案,这也是我们最开始的适配⽅案,但我们很快发现了问题。我们的
应⽤⾥⾯有个 banner 图轮播的功能,我们⾼度采⽤ dp 做处理,宽度采⽤ match_parent 占满
屏幕宽度,对⼤多数⼿机显示都不存在问题。但我们的屏幕宽度其实是不⼀样的,⽽⾼度固定后
肯定会出现图⽚显示不完全的情况。
2. 所以我们采⽤了⼀个笨办法,那就是宽⾼限定符适配,也就是穷举市场⾯主流⼿机的宽⾼像素
点,但很快发现这样维护起来相当麻烦,所以⼀直在寻求好办法进⾏处理。
3. 后⾯发现了鸿洋的 AutoLayout,简单看了下⾥⾯的源码,也是受宽⾼限定符的影响,我们可以
直接在布局中写 px 值,这样布局会等⽐例放缩,但由于它不能兼顾到⾃定义 View 以及存在性
能问题⽽且他本⼈也没有维护的情况下,所以我们后⾯并没有⽤这个⽅案。
4. 终于在最后今⽇头条开源了⼀个⽐较不错的适配⽅案。我们直接通过修改 density 的值,强⾏把
所有不同尺⼨分辨率的⼿机的宽度值 dp 改成⼀个统⼀的值,这就解决了所有的适配问题。
设计模式的原则
设计模式可以让我们的程序更健壮、更稳定、更加容易拓展,编写的时候我们需要遵循 6 ⼤原则:
1. 单⼀职责原则:
单⼀原则很简单,就是将⼀组相关性很⾼的函数、数据封装到⼀个类中,换句话说,⼀个类应该
有职责单⼀。
2. 开闭原则:
开闭原则就是说⼀个类应该对于拓展是开放的,但是对于修改是封闭的。因为开放的 APP 或者
是系统中,经常需要升级和维护等,⼀旦进⾏修改,就容易破坏原有的系统,甚⾄带阿⾥⼀些难
以发现的 Bug。所以开闭原则相当᯿要。
3. ⾥⽒替换原则:
⾥⽒替换原则的定义为:所有引⽤基类的地⽅必须能透明地使⽤其⼦类对象。简单地说,就是以
⽗类的形式声明的变量或形参,赋值为任何继承于这个⽗类的之类不影响程序的执⾏。
4. 依赖倒置原则:
依赖倒置主要是实现解耦,使得⾼层次的模块不依赖于低层次模块的具体实现细节。⼏个关键
点:
⾼层模块不应该依赖底层模块。⼆者都应该依赖其抽象类或接⼝;
抽象类或接⼝不应该依赖于实现类;
实现类应该依赖于抽象类或接⼝;
5. 接⼝隔离原则:
接⼝隔离原则定义:类之间的依赖关系应该建⽴在最⼩的接⼝上。其原则是将⾮常庞⼤的、臃肿
的接⼝拆分成更⼩的更具体的接⼝。
6. 迪⽶特原则:
描述的原则:⼀个对象应该对其他的对象有最少的了解。什么意思呢?就是说⼀个类应该对⾃⼰
调⽤的类知道的最少。还是不懂?其实简单来说:假设类 A 实现了某个功能,类B需要调⽤类 A
的去执⾏这个功能,那么类 A 应该只暴露⼀个函数给类 B,这个函数表示是实现这个功能的函
数,⽽不是让类 A 把实现这个功能的所有细分的函数暴露给 B。
Android 中常⽤的设计模式
1. 单例 => 调⽤系统服务时拿的 Binder 对象;
2. Builder
主要是为了把⼀个复杂对象的构造和它的表示分离,使得同样的构造过程可以创建不同的表示。
=> 常⻅的⽐如 Android 的 Dialog。
其中我的图⽚压缩库就运⽤了单例和 Builder 模式。
3. ⼯⼚⽅法模式
根据传⼊的参数决定创建哪个对象。
=> 典型的获取系统服务 getSystemService() 。
4. 策略模式
如何使⽤策略模式呢,我不打算写示例代码了,简单描述⼀下,就将前⾯说的算法选择进⾏描
述。我们可以定义⼀个算法抽象类 AbstractAlgorithm,这个类定义⼀个抽象⽅法 sort()。每个
具体的排序算法去继承 AbstractAlgorithm 类并᯿写 sort() 实现排序。在需要使⽤排序的类
Client 类中,添加⼀个 setAlgorithm(AbstractAlgorithm al);⽅法将算法设置进去,每次
Client 需要排序⽽是就调⽤ al.sort()。
=> ⽐如 Android 的属性动画的使⽤插值器。
5. 责任链模式
使多个对象都有机会处理请求,从⽽避免请求的发送者和接收者直接的耦合关系,将这些对象连
成⼀条链,并沿这条链传递该请求,直到有对象处理它位置。
=> ⽐如 Android 的事件分发机制。
6. 观察者模式
=> ⽐如 Adapter 的 notifyDataSetChanged()。
7. 迭代器模式
=> Java 的迭代器 Iterator 类
8. 代理模式
为其他类提供⼀种代理以控制这个对象的访问。
=> Android 中的典型应⽤就是 AIDL 的⽣成,它根据当前的线程判断是否要跨进程访问,如果不
需要跨进程访问就直接返回实例,如果需要跨进程则返回⼀个代理。在跨进程通信时,需要把参
数写⼊到 Parcelable 对象,然后再执⾏ transact() 函数,我们要写的代码挺多的。AIDL 通过⽣
成⼀个代理类,代理类中⾃动帮我们写好这些操作。
9. 适配器模式
=> 典型的 Android 的 ListView 和 RecyclerView。它们只关⼼⾃⼰的 ItemView,⽽不⽤关⼼这
个 ItemView ⾥⾯具体显示的是什么。⽽数据源存放的是显示的内容,它保存了 ItemView 要显
示的内容。ListView 和数据源本身没有关系,这时候,适配器提供了 getView() ⽅法给 ListView
使⽤,每次 ListView 只需要提供位置信息给 getView() 函数,然后 getView() 函数根据位置信息
向数据源获取对应的数据,根据数据返回不同的 View。
10. 装饰模式
public abstract class Component{
public abstract void operate();
}
public class ConcreteComponent extends Component{
public void operate(){
//具体的实现
}
}
public class Decorator{
private Component component;
public Decorator(Component component){
this.component=component;
=> 在 Android 中的 Context 。
11. 享元模式
使⽤享元对象有效地⽀持⼤量的细粒度对象。
=> ⽐如 Java 的常量池,线程池等,主要是为了᯿⽤对象。在 Android ⾥⾯的
Message.obtain() 就是⼀个很好的例⼦。
讲讲 LeakCanary 的实现原理。
LeakCanary 就是通过注册 ActivityLifecycleCallbacks ,监听⽣命周期⽅法的回调,作为整个
内存泄漏分析的⼊⼝。每次 onActivityDestroyed(Activity activity) ⽅法被回调之后,都会
创建⼀个 KeyedWeakReference 对相应的 Activity 的状态进⾏跟踪。
1. 在后台线程(HandlerThread)检查引⽤是否被清除,如果没有,⼿动调⽤ GC。
2. 如果引⽤还是未被清除,把 heap 内存 dump 到 APP 对应的⽂件系统中的⼀个 .hprof ⽂件
中。
3. 在另外⼀个进程中的 HeapAnalyzerService 有⼀个 HeapAnalyzer 使⽤ HAHA 开源库解析
这个⽂件。
4. 得益于唯⼀的 reference key, HeapAnalyzer 找到 KeyedWeakReference ,定位内存泄漏。
5. HeapAnalyzer 计算到 GC roots 的最短强引⽤路径,并确定是否是泄漏。如果是的话,建⽴
导致泄漏的引⽤链。
6. 引⽤链传递到 APP 进程中的 DisplayLeakService , 并以通知的形式展示出来。
多渠道打包?美团的多渠道打包?
之前有对美团的多渠道打包⽅案进⾏调研。
美团之前的多渠道打包⽅案主要是在 META-INF ⽬录下添加空⽂件,⽤空⽂件的名称来作为渠道的唯
⼀标示,之前在 META-INF ⽬录下添加⽂件是不需要᯿新签名应⽤的,所以好使。但 Android 7.0 出
了新的签名⽅式,把 META-INF 列⼊了保护区,向 META-INF ⽬录添加空⽂件将会对其他区产⽣影
响,所以美团之前的多渠道打包⽅式将会出现问题。但美团响应速度很快,很快就出现了 Walle,可
以完美在新签名⽅式下进⾏多渠道打包。
新的签名⽅案主要是在不受保护的 APK Signing Block 上做⽂章。好像是在 APK 中添加 ID-value。
}
public void operate(){
operateA();
component.operate();
operateB();
}
public void operateA(){
//具体操作
}
public void operateB(){
//具体操作
}
}
我们的 iSchool 项⽬采⽤多渠道打包并不是为了统计各个商店的下载量等,⽽是主要为了应对我们的
商业模式。所以采⽤的传统的⽅式进⾏多渠道打包,在 Gradle ⽂件⾥⾯通过 productFlavors 标签设
置不同的 applicationId 和其它信息。并且设置 task 做到指定打⼀个代理商的渠道包。我们对资源⽂
件也是不⼀样的。我们对同⼀个资源采⽤不⼀样的后缀名称,然后通过字符串截取去判断真正的资源
⽂件,达到不⼀样的项⽬引⽤不⼀样的资源的⽬的。
RxJava 源码。
简单看了⼀下 RxJava2 的 源码订阅过程。
创建 Observable & 定义需要发送的事件
创建 Observable 的本质其实就是创建了⼀个 ObservableCreate 类的对象。它的内部有 1 个
subscribeActual() ⽅法。
创建 Observer & 定义响应事件的⾏为
Observer 是⼀个接⼝,内部定义了 4 个接⼝⽅法,分别⽤于响应被观察者发送的不同事件。
通过订阅(subscribe)连接观察者和被观察者
订阅的本质实际上就是调⽤ ObservableCreate 类对象的 subscribeActual()。
执⾏逻辑为:
1. 创建⼀个 CreateEmitter 对象(封装成 1 个 Disposable 对象);
2. 调⽤被观察者 ObservableOnSubscribe 对象复写的 subscribe();
3. 调⽤观察者 Observer 复写的 onSubscribe();
4. 被观察者内部的 subscribe() 依次回调 Observer 复写的⽅法。
RxJava 内置了许多操作符,每个操作符的 subscribeActual() 中的默认实现都不相同。但基本原理都
是封装传⼊的 Observer 对象,再调⽤ Observable 复写的 subscribe()。
各种操作符内的 Observable 中的 SubscirbeActual() ⽅法都含有⼀句代码:
source.subscribe(Observer),这个 source 就是上游创建的被观察者对象 Observable。所以才导致
RxJava 的多个被观察者 Observable 发送事件的顺序是⾃上⽽下的,和代码顺序⼀样。
OkHttp 源码。
OkHttp ⽀持异步请求和同步请求。因为异步请求不会阻塞主线程,所以我们通常会直接把异步请求
写在 UI 主线程中。
我们⾸先会创建⼀个 OkHttpClient 对象,在源码中可以知道,OkHttpClient 对象是通过 Builder 模
式创建的,在 OkHttpClient.Builder() 类的构造⽅法中,对它的属性都有默认值和默认对象。
创建好 OkHttpClient 对象之后,我们会通过调⽤它的 newCall(Request request) ⽅法来创建⼀个
RealCall 对象。
对于异步请求,我们会直接调⽤ RealCall 的 enqueue() ⽅法,这个⽅法⾥⾯⼜会调⽤调度器对象
Dispatcher 的 enqueue(AsyncCall call) ⽅法。这个⽅法接收⼀个 AsyncCall 对象参数。
从 Dispatcher 的 enqueue(AsyncCall call) ⽅法中可以看到,得到异步任务之后,如果异步
任务运⾏队列中的个数⼩于 64 并且每个主机正在运⾏的异步任务⼩于 5,则将该异步任务加⼊到异步
运⾏队列中,并通过线程池执⾏该异步任务,若不满⾜以上两个条件,则将该异步任务加⼊到预备异
步任务队列中。
AsyncCall 是 RealCall 的内部类。它主要是响应⽹络请求的各种回调,并最后从 Dispatcher 对象的异
步请求队列中移除该任务,并将符合条件的预备异步任务队列中的任务加⼊到正在运⾏的异步任务队
列中,并将其放⼊线程池中执⾏。
在 AsyncCall 中最᯿要的就是 execute() ⽅法。这个⽅法⾥⾯会通过⼀个⽅法把所有的拦截器做聚合
之后⽣成⼀个拦截器调⽤链对象并返回。
对于同步请求,和异步不⼀样的是在直接执⾏ RealCall 对象的 execute() ⽅法。它会调⽤ Dispatcher
对象的 executed(RealCall call) ⽅法将这个同步任务加⼊到 Dispatcher 对象的同步任务队列中去。接
着会调⽤ getResponseWithInterceptorChain() ⽅法获取到请求的响应对象。最终,也是在 finally 中
将同步任务从 Dispatcher 对象的同步任务队列中移除。
拦截器是 OkHttp 设计的精髓所在,每个拦截器负责不同的功能,使⽤责任链模式,通过链式调⽤执
⾏所有的拦截器对象中的 intercept() ⽅法。拦截器在某种程度上也借鉴了⽹络协议中的分层思想,请
求时从最上层到最下层,响应时从最下层到最上层。
系统中已经默认添加了᯿试᯿定向、桥接拦、缓存、连接服务器、请求服务器等拦截器,但我们依然
可以⾃定义拦截器。
编写起来也⾮常简单,我们只需要实现 Interceptor 接⼝,并᯿写其中的 intercept() ⽅法即可。
ART 和 Dalvik 虚拟机以及 JVM 的区别
⾸先看看 JVM 和 Dalvik 的区别。
Dalvik 基于寄存器,⽽ JVM 是基于栈的。
Dalvik 运⾏ dex ⽂件,⽽ JVM 运⾏ Java 字节码。
再来看看 ART 和 Dalvik。
ART 和 Dalvik 的机制不同。在 Dalvik 下,应⽤每次运⾏的时候,字节码都需要通过即时编译器转换
为机器码,这会拖慢应⽤的运⾏效率,⽽在 ART 环境中,应⽤在第⼀次安装的时候,字节码就会预先
编译成机器码,使其成为真正的本地应⽤。这个过程叫做预编译。这样的话,应⽤的启动和执⾏都变
得更加快速。
ART 拥有着给带来系统性能的显著提升,让应⽤启动更快、运⾏更快、体验更流畅、触摸反馈更及
时、更⻓的电池续航能⼒、⽀持更低的硬件等优势。当然 ART 也有缺点,那就是机器码占⽤的存储空
间更⼤,⽽且应⽤的安装时间会变⻓。
sleep 和 wait 有什么区别
功能差不多,都⽤来做线程控制,但 sleep 不会释放同步锁,wait() 会释放同步锁。
⽤法不同,sleep 可以⽤时间指定来使它⾃动醒过来,如果时间不到,我们只能通过调⽤ interreput()
来强⾏打断,⽽ wait() 可以⽤ notify() 直接唤起。
讲讲⽬前 Android 主要的热修复⽅案
总的来说热修复主要是利⽤三个⽅法:类加载、底层替换和 Instant Run。
采⽤类加载⽅案的⽬前以腾讯系为主,⽐如 Tinker 等,但这个⽅案需要᯿启 APP 后让 ClassLoader
᯿新加载新的类。因为类是⽆法被卸载的,要想᯿新加载新的类就需要᯿启 APP。所以采⽤类加载⽅
案的热修复框架是不能即时⽣效的。
⽽底层替换⽅案不会加载新类,⽽是直接在 Native 层修改原有的类,由于是在原有类进⾏修改,所
以会有⽐较多的限制,不能增减原有的⽅法和字段。但这样的⽅法可以⽴即⽣效并且不需要᯿启,⽬
前采⽤底层替换⽅案的主要以阿⾥系为主。⽐如阿⾥百川、AndFix 等。底层替换⽅案存在⼀个问题,
那就是需要针对 Dalvik 虚拟机和 ART 虚拟机做适配没需要考虑指令集的兼容问题,需要 native 代
码⽀持,兼容性也会有⼀定的影响。
除了资源修复,当然还可以借鉴 Instant Run 的原理。⽐如美团开源的 Robust。简单看了下 Robust
插件的原理,它主要是对每个产品代码的每个函数都在编译打包的阶段⾃动地插⼊⼀段代码,插⼊过
程对业务开发是完全透明的。Robust 会为每个 class 增加⼀个类型为 ChangeQuickRedirect 的静态
成员,⽽在每个⽅法前都插⼊了使⽤ changeQuickRedirect 相关的逻辑,当 changeQuickRedirect
不为 null 的时候,可能会执⾏到 accessDispatch 从⽽替换掉之前⽼的逻辑,达到 fix 的⽬的。
讲讲 HTTPS 的加密过程。
客户端请求 HTTPS 连接 => 服务器返回证书(证书中包含公钥)=> 客户端产⽣密钥并⽤服务器的公
钥加密 => 客户端发送加密密钥 => 服务器返回加密的密⽂通信
AsyncTask 的原理
AsyncTask 内部封装了两个线程池和⼀个 Handler。
其中⼀个线程池负责任务调度,这个线程池实际上是⼀个静态内部类,即所有 AsyncTask 对象公有
的;其内部维护了⼀个双向队列,容量根据元素数量调节;通过同步锁修饰 execute(),保证
AsyncTask 的任务是串⾏执⾏;每次都是从队列头部取出任务。
另⼀个线程负责真正执⾏具体的线程任务。实际上就是⼀个已经配置好的可执⾏并⾏任务的线程池。
⽽ Handler 主要负责异步通信和消息传递。
OkHttp 的优势。
1. 对同⼀个主机发出的所有请求都可以共享相同的套接字连接;
2. 使⽤线程池来复⽤连接以提⾼效率;
3. 提供了对 gzip 的默认⽀持来降低传输内容的⼤⼩;
4. 对 HTTP 响应的缓存机制,可以避免不必要的⽹络请求;
5. 当⽹络出问题时,OkHttp 会⾃动᯿试⼀个主机的多个 IP 地址;
Android 9.0 新特性
全⾯屏⽀持,刘海屏⼿机可能会成为趋势;
通知栏多种通知;
多摄像头的更多动画;
GPS 定位外的 WIFI 定位;
⽹络还有神经⽹络;
Material Design 2.0;
Android Dashboard:⽤户可以⾃⼰看清楚⾃⼰在⼿机上都做了什么,在哪个应⽤停留了多少时
间,停留过⻓还会有提示;
增加夜间模式;
Adavance Battery:需要⽤户⼿动开启该模式,机器学习,降低后台占⽤,提升续航;
Shush:屏幕朝下完全进⼊勿扰状态。除了闹钟,其他⾃动调整为静⾳或者震动的模式。
Actions 和 Slices:检测⽤户⾏为,让系统做出对应的动作。
HR 猜测⾯
你对未来有什么规划?
你过往⼯作中取得过怎样的成功,⼜遭遇了哪些失败?
咕咚总监⾯⾃我介绍
⾯试官您好,我是刘世麟,⾮常荣幸能参加咕咚的复试,下⾯我简单介绍⼀下我的个⼈情况:我从实
习到现在⼀直在致学教育⼯作,从事 Android 开发,凭借良好的⼯作能⼒和沟通能⼒,连续两年蝉联
「优秀员⼯」称号,在今年初被公司内聘为技术总监助理,协助技术总监开展部⻔管理和项⽬推动⼯
作。在⼯作之外,我喜欢编写技术博客和在 GitHub 上贡献开源代码,⽬前在 GitHub 上总共拥有 7k
左右的 Star,数篇技术博客也有数⼗万阅读。我⾮常地热爱移动开发,我希望我的技术能像我的发际
线⼀样深⼊。不瞒您说,我⼀直认为咕咚是⼀家技术氛围良好的企业,所以⼀直渴望加⼊,通过前⾯
和徐哥以及超哥的交流,我更加地坚定了我内⼼的想法。同样,我认为我很适合咕咚的技术团队。⾸
先,我⾮常的热爱移动开发,⽽且学习能⼒较强,这和咕咚⽬前的团队配置⾮常相符,所以我认为我
可以很快融⼊团队,并迅速开启开发⼯作。此外,咕咚⽬前采⽤的 Java 和 Kotlin 混合开发 以及
MVVM 架构都是我可以迅速上⼿的。周三和徐哥交流的咕咚项⽬的问题点,我也专⻔去做了探索。听
闻咕咚最近在筹备团队技术博客的事情,我希望并且相信我能在这⾥发挥好⾃⼰的优势!
今⽇头条⾯试
1. ClassLoader 加载原理。
ClassLoader加载类的原理
1. 原理介绍
ClassLoader 使⽤的是双亲委托模型来搜索类的,每个 ClassLoader 实例都有⼀个⽗类加载
器的引⽤(不是继承的关系,是⼀个包含的关系),虚拟机内置的类加载器( Bootstrap
ClassLoader )本身没有⽗类加载器,但可以⽤作其它ClassLoader实例的的⽗类加载器。当⼀
个 ClassLoader 实例需要加载某个类时,它会试图亲⾃搜索某个类之前,先把这个任务委托给
它的⽗类加载器,这个过程是由上⾄下依次检查的,⾸先由最顶层的类加载器 Bootstrap
ClassLoader 试图加载,如果没加载到,则把任务转交给 Extension ClassLoader 试图加
载,如果也没加载到,则转交给 App ClassLoader 进⾏加载,如果它也没有加载得到的话,则
返回给委托的发起者,由它到指定的⽂件系统或⽹络等URL中加载该类。如果它们都没有加载到
这个类时,则抛出 ClassNotFoundException 异常。否则将这个找到的类⽣成⼀个类的定义,
并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
2、为什么要使⽤双亲委托这种模型呢?
因为这样可以避免᯿复加载,当⽗亲已经加载了该类的时候,就没有必要 ClassLoader 再加载
⼀次。考虑到安全因素,我们试想⼀下,如果不使⽤这种委托模式,那我们就可以随时使⽤⾃定
义的String来动态替代java核⼼api中定义的类型,这样会存在⾮常⼤的安全隐患,⽽双亲委托的
⽅式,就可以避免这种情况,因为String已经在启动时就被引导类加载器( Bootstrcp
ClassLoader )加载,所以⽤户⾃定义的 ClassLoader 永远也⽆法加载⼀个⾃⼰写的String,
除⾮你改变JDK中 ClassLoader 搜索类的默认算法。
3、 但是JVM在搜索类的时候,⼜是如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,⽽且要判断是否由同⼀个类
加载器实例加载的。只有两者同时满⾜的情况下,JVM才认为这两个class是相同的。就算两个
class是同⼀份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两
个不同class。⽐如⽹络上的⼀个Java
类 org.classloader.simple.NetClassLoaderSimple ,javac编译之后⽣成字节码⽂件
NetClassLoaderSimple.class , ClassLoaderA 和 ClassLoaderB 这两个类加载器并读取
了 NetClassLoaderSimple.class ⽂件,并分别定义出了 java.lang.Class 实例来表示这
个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同⼀份字节码⽂件,如果试图
将这个Class实例⽣成具体的对象进⾏转换时,就会抛运⾏时异
常 java.lang.ClassCaseException ,提示这是两个不同的类型。
2. LeakCanary ⼯作原理,GC 如何回收,可以作为GC 根结点 的对象有哪些?
JVM垃圾回收的根对象的范围有以下⼏种:
(1)虚拟机(JVM)栈中引⽤对象 (2)⽅法区中的类静态属性引⽤对象
(3)⽅法区中常量引⽤的对象(final 的常量值)
(4)本地⽅法栈JNI的引⽤对象
3. JVM 内存模型,如何理解 Java 虚函数表
4. 如何从 100 万个数中找到最⼩的⼀百个数,考虑算法的时间复杂度和空间复杂度。
5. JS 和 Java Native 如何通信?
6. APP 加固怎么做?
7. MVP 和 MVC 以及 MVVM 的主要却别,为什么 MVP 要⽐ MVC 好?
8. Android 的混淆原理?
9. 如何设计⼀个安卓的画图库,做到对扩展开放,对修改封闭,同时⼜保持独⽴性
10. 怎样做系统调度?
11. 数组实现队列。
12. 实现 LRUCache。
13. HashMap 原理,Hash 碰撞,如何设计让遍历效率更⾼?
14. ConcurrentHashMap 特点是什么?如何实现的。JDK 1.8 ⽤红⿊树实现,什么是红⿊树?
15. Syncronized 和 Lock 的区别,join 和 yield 的⽤法,volitate ⽤法,CAS 怎么实现的?
16. jvm内存结构 ⼿写代理模式 ⼿写线程安全的单例模式;GC算法有哪些?Android触摸机制;除
了读取trace⽂件,还能如何获取到ANR异常?
17. ⼀个⾏列都有序的数组,给定⼀个数,找出具体的位置。
18. Retrofit 原理。
主要是通过动态代理将接⼝直接转换成代理对象。动态代理和静态代理的区别,动态代理直接在
虚拟机层⾯构建字节码对象。
19. View⾃定义的流程,实现哪些⽅法。
20. 链表回⽂结构。
21. Android内存优化怎么优化。性能优化怎么优化。卡顿是什么原理。
22. 中序遍历,⾮递归实现
|