大纲
Java 基础
1. ==、equals 和 hashCode 的区别
== 用于基础数据类型的判断时,比较的是值,用于引用类型的判断时,比较的是对象在内存中的存放地址。这是因为对象是放在堆中的,栈中存放的是对象的地址,而 == 是对栈中的值进行比较的。
如果要比较堆中对象的内容是否相同,就要重写 equals。Object 中的 equals 方法同 ==,通过覆盖该方法来实现具体功能。
集合查找对象时,需要使用 equals 遍历比较,数据较多时效率很低,为了提高查找对象的效率,诞生了 hashCode。对象的 hashCode 有如下特性:
对象相同,hashCode 一定相同;
hashCode 相同,对象不一定相同。
依据此特性,在存对象时,可以直接根据 hashCode 值映射出对象应该存储的地址,如果该地址没对象,则直接存储,如果已经有 hashCode 值相同的对象存在,则调用 equals 函数比较,相同则丢弃,不同则生成链表存储起来。
这样存储,相当于把 hashCode 值相同的对象放在了一个桶里,每次查找对象时,直接从目标桶里挨个查找想要的对象,大大提高了查找效率。
因此,在重写对象的 equals 函数时,必须重写其 hashCode 函数,如果不重写,在 HashSet 等散列集合存储时,会因存放桶的不同,而存储了 equals 相同的多个对象,这并不是我们想要的结果。
在重写 hashCode 时,遵循俩个原则:
俩个 equals 相同的对象,hashCode 返回值一定相同(定位到正确的链表,保证正确去重);
俩个 equals 不同的对象,hashCode 可能相同,但是要尽可能保证桶的均匀,这样才能达到最大查找效率。
2. 基础数据类型各占多少字节
基础类型 | 字节数 |
---|---|
byte | 1 |
boolean | 1 |
char | 2 |
short | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
3. 对多态的理解
多态指允许不同类的对象对同一消息作出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。
实现多态的技术称为动态绑定,是指在执行期间判断所引用对象的实际类型,根据实际类型调用相应方法。
Java 的俩种多态是指:运行时多态和编译时多态。多态是面向对象的核心特征之一,类的多态性提供类中成员设计的灵活性和方法执行的多样性。
4. String、StringBuffer 与 StringBuilder 的区别
String 的值是不可变的,每次对 String 的操作都会生成新的 String 对象,效率低且浪费内存空间。当对字符串进行修改时,可以使用 StringBuffer 和 StringBuilder。StringBuffer 多线程安全,而 StringBuilder 适用于单线程且效率更高。
引申:String 为什么要设计成不可变的?
- Java 中的字符串常量池,保证了当创建一个 String 对象时,假如值已存在于常量池中,则不会创建新的对象,而是引用已经存在的。这是一种常见的优化方案。基于此,假如字符串对象允许改变,则改变一个对象会影响到另一个独立对象。
- 安全性上讲,String 不可变更有利于方法参数传递。
- 字符串不变性保证了 hash 码的唯一性,可以放心进行缓存而不必每次都去计算新的哈希码。
5. 父类的静态方法能否被子类重写
不能
重写指根据运行时对象的类型来决定调用哪个方法(动态绑定),而静态变量和静态方法是在编译时与类绑定的。
6. 线程和进程的区别
- 进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)。
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此 CPU 切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。 - 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。
- 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
引申:线程间和进程间同步的方式。
线程间同步:
- 同步代码块;
- 同步方法;
- 互斥锁 ReetrantLock;
进程间同步:
同步时会阻塞当前线程,直到唤醒为止。异步则可以继续干其它事情,其它线程完成任务时通知即可。要分清楚俩者的概念。
引申:线程间和进程间通信的方式。
线程间通信:
- Handler
- Activity.runOnUiThread
- View.post
- AsyncTask
进程间通信:
- 共享内存
- 管道
- UDS
- RPC
Android 进程间通信:
- Intent
- 文件共享
- Message
- AIDL
- ContentProvider
- Broadcast
- Socket
- Binder
完整RPC通信过程如下:
a. 客户端调用 Stub 接口;
b. Stub 根据操作系统的要求进行打包,并执行相应的系统调用;
c. 服务器端 Stub 解包并调用与数据包匹配的进程;
d. 进程执行操作;
e. 服务器以上述步骤的逆向过程将结果返回给客户端。
7. final、finally、finalize 的区别
final:
java 中的关键字、修饰符。被 final 修饰的:
- 类不能再派生子类,不能作为父类被继承。因此,一个类不能同时被声明为抽象类和 final 类。
- 变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。
- 方法只能使用,不能重载。
finally:
Java 的一种异常处理机制,是对 Java 异常处理模型的补充。finally 结构使代码总会执行,而不管无异常发生。使用 finally 可以维护对象的内部状态,并可以清理非内存资源。特别是在关闭数据库连接这方面,如果程序员把数据库连接的 close() 方法放到 finally 中,就会大大降低程序出错的几率。
finalize:
Java 使用 finalize 方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。
8. Serializable 和 Parcelable 的区别
使用序列化一般有以下三种用途:
- 本地文件保存对象;
- 网络传递对象;
- 进程间传递对象。
Serializable 是 Java 的序列化技术。
使用简单,对序列化的 class 增加继承,并添加一个序列化 id 即可。手动设置序列化 id 的好处是,当前 class 的成员变量发生改变时(比如增删),会影响自动生成的 id,从而导致反序列化失败,通常情况下手动或自动设置均可。
Serializable 的原理是通过本地读写文件实现,会产生大量的临时变量,占用内存高且可能引发 GC。
当一个父类实现序列化,子类自动实现序列化,不需要再显式实现 Serializable 接口。
Parcelable 是 Android 特有的序列化 API,虽然使用较为繁琐,但是效率高、占用内存少。
原理是在内存中建立一块共享数据块,序列化和反序列化均是操作这块的数据。
当内存使用场景为主时(如通过共享内存实现 IPC 通信),推荐使用 Parcelable,因为性能高。但是 Parcelable 不适合持久化,原因是不安全(增删变量会导致无法升级),且有版本兼容问题,至于效率问题则有争议。
9. 内部类的设计意图及种类
10. 锁机制相关知识:synchronized、volatile 和 Lock
Java 锁机制详解(一)synchronized
Java 锁机制详解(二)volatile
Java 锁机制详解(三)Lock
Lock 手写实现俩个线程交替、多个线程顺序执行
进入 synchronized 代码块前如果拿不到锁,则进入锁池等待下次竞争;
进入 synchronized 代码块后如果调用对象的 wait,则线程释放锁,并进入等待池。直到 notify 唤醒,从等待池进入锁池,重新获得竞争锁的权利。
11. 常见编码和字节占用数
ASCII编码字节数:
英文字符:1
Unicode编码字节数:
英文字符:4
中文简体:4
中文繁体:4
UTF-8编码字节数:
英文字符:1
中文简体:3
中文繁体:3
12. 深拷贝和浅拷贝的区别
- 浅拷贝:将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用
- 深拷贝:创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
13. 静态代理和动态代理的区别,什么场景使用
静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的 .class 文件就已经存在了。
动态代理类:在程序运行时,运用反射机制动态创建而成。
使用场景上,静态代理通常只代理一个类,并且事先知道要代理什么,比较适合专一化场景;而动态代理是代理一个接口下的多个实现类,并且事先并不知道要代理的东西,只有运行时才知道,更加泛用。
14. 如何将一个 Java 对象序列化到文件里
- 对象需要实现
Serializable
接口; - 通过
ObjectOutputStream.writeObject()
写入,
ObjectInputStream.readObject()
读取。
15. 谈谈对注解的理解
以下是总结:
注解本质是继承了 Annotation 的接口,是一种特殊的标注,本身没有意义,依赖解析器来实现它的价值。
注解利用几个元注解来分别决定它的作用范围、生命周期、是否允许继承等。其中生命周期可分为 Source(编译期注解可见,Class 不可见)、Class(Class 注解可见,类加载后丢弃)和 Runntime(永久保存,运行时可反射)。
注解在不同阶段有不同的价值,这里举一些例子。Source 阶段,通过 APT 扫描,可以获取注解信息。诸如通过 @Override 注解来检测父类是否有同名方法,EventBus3 也在该时期通过扫描 @Subscribe 来生成 java 类以保存订阅信息,另外还有 ARouter 生成路由、ButterKnife 生成绑定 id 等。Class 阶段实例不多,但是可以应用于字节码插桩。Runntime 阶段,可以利用反射获取注解信息,如 EventBus2 事件总线关系就是这么生成的,EventBus3也会在运行时未找到指定订阅时,尝试通过反射去获取。
除 Java 提供的注解外,Android 还提供了自己的注解库 support-annotations,包括如 @Null、@StringRes、@StringDef 等注解。
16. 谈谈对依赖注入的理解
依赖注入,即由容器动态的将某个依赖关系注入到组件之中。
依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
17. 谈谈对泛型的理解
泛型是 Java 1.5 的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
在 Java SE 1.5 之前,没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型的好处有三个:分别是安全性,泛型提供了编译期类型检测的机制;然后是扩展性,相比较 Object,泛型使数据的类别可以像参数一样由外传递进来;最后是可读性,不必等到运行时才去强制转换,定义或实例化阶段,就能看到要操作的数据类型。
Java 的泛型是伪泛型,在生成的 Java 字节码中是不包含泛型中的类型信息的,虚拟机层面是不存在所谓『泛型』的概念的。在编译期间,所有的泛型信息都会被擦除掉。这个过程就称为类型擦除。
在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限。
类型擦除虽然有局限性,但也可以利用类型擦除来绕过编译器不允许的操作限制。如利用反射往 List 中添加其它类型的参数。
18. ArrayList 和 LinkedList 的区别,以及应用场景
- ArrayList 基于数组,LinkedList 基于双链表。
- 因为 ArrayList 基于数组,所以搜索和读取数据很快,可以直接返回指定 index 位置的元素。但是插入、删除数据开销很大,需要移动数组插入位置后的所有元素。LinkList 则相反。
- LinkedList 会占用更多的内存,因为每个节点还存储了前后节点的位置信息。
应用场景:
- 随机访问较多,使用 ArrayList。插入删除频繁,使用 LinkedList。
- ArrayList 的插入、删除也不一定比 LinkedList 慢,如果在 List 末尾插入,LinkedList 需要一直查找到列表尾部,而 ArrayList 直接插入即可,这时 ArrayList 比 LinkedList 要快。所以要视情况而定。
Java 虚拟机
包括:
- APK 的生成;
- APK 的安装方式(JIT、AOT)、Dalvik 和 ART 的异同等;
- Java 内存结构;
- Java 内存模型;
- Java 引用类型;
- 垃圾识别策略;
- 垃圾回收算法;
- 类的加载过程(如何从 class 转变为被虚拟机识别的类);
虚引用必须与 ReferenceQueue 一起使用,当 GC 准备回收一个对象,如果发现它有虚引用,就会在回收之前,把这个 虚引用 加入到与之关联的 ReferenceQueue 中。
仅通过 ReferenceQueue 就能拿到内存泄漏的对象吗?
不能,因为 GC 回收之后,ReferenceQueue 中的虚引用都是正常回收对象的虚引用,无法通过 ReferenceQueue 拿到内存泄漏的对象。
一般做法是,将虚引用和包含对象的弱引用相关联(通过 map),每次 ReferenceQueue 获得新的虚引用时(queue.poll),就移除与之关联的弱引用,这样 GC 完毕之后,通过剩余的弱引用就能拿到内存泄漏的对象。
Java 多线程
1. 开启线程的几种方式
继承 Thread 类
实现 Runnable 接口
通过 Callable 和 Future 创建对象
2. 线程的几种状态
Java 线程状态在 Thread 中定义,源码中能看到有个枚举 State,总共定义了六种状态:
NEW:新建状态。线程对象已经创建,但尚未启动;
RUNNABLE:就绪状态,可运行状态。调用了线程的 start 方法,已经在 java 虚拟机中执行,等待获取操作系统资源如 CPU,操作系统调度运行;
BLOCKED:阻塞状态。线程等待锁的状态,等待获取锁进入同步块/方法或调用 wait 后重新进入需要竞争锁;
WAITING:等待状态。等待另一个线程以执行特定的操作。调用以下方法进入等待状态。 Object.wait、Thread.join、LockSupport.park;
TIMED_WAITING:线程等待一段时间。调用方法 Thread.sleep、Object.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil;
TERMINATED:进程结束状态。
处于 NEW、TERMINATED 这两个状态的线程不会出现在堆栈信息里面。
3. 如何控制某个方法允许并发访问线程的个数
利用 Semaphore 类。
4. wait、sleep 的区别
- wait 来自 Object 类,sleep 则是 Thread 的方法;
- 虽然都会让出 cpu 资源,但是 sleep 不会释放锁,而 wait 方法释放了锁,使得其他线程可以使用同步代码块或者同步方法;
- wait、notify 和 notifyAll 只能在同步方法或者同步代码块里面使用,而 sleep 可以在任何地方使用。
5. 什么可以导致线程阻塞/为什么会出现线程阻塞
一个线程需要依赖并等待另一个线程的执行结果,这时就会产生阻塞。
- 线程睡眠。Thread.sleep;
- 线程等待。Object.wait;
- 线程礼让。Thread.yield;放弃执行机会,重新进入锁池。
- 线程自闭。Thread.join;在当前线程中调用另一个线程的 join() 方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
- suspend 和 resume。因可能导致死锁而废弃。
6. 线程如何关闭
- 运行完毕后自动终止;
- Thread.stop,但是该方法是非常不安全的。
- Thread.interrupt,interrupt 依赖中断线程对中断状态的响应。
为什么弃用 stop?
- 调用 stop 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
- 调用 stop 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
7. Java 中实现同步的几种方法(如何实现线程同步)
- 使用 synchronized 关键字。可以用来修饰同步方法或同步代码块。
- 使用 wait + notify。
- 使用可重入锁 ReentrantLock。ReentrantLock 是可重入、互斥、实现了 Lock 接口的锁,它和 synchronized 具有相同的基本行为和语义,并扩展了其它能力。
- 使用 ThreadLocal。使用 ThreadLocal 管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
- 使用上层封装好的 concurrent 包。比如阻塞队列。
8. ThreadLocal 原理,实现及如何保证 Local 属性
每个线程都创建有一个 map,其中 key 为 ThreadLocal,value 为要保存的变量副本。这样:
ThreadLocal.set
实际为Thread.currentThread.map.put(this, value)
ThreadLocal.get
实际为Thread.currentThread.map.get(this)
。
所以当调用 get/set 方法时,实际是在操纵当前线程的变量副本。
9. 什么是线程安全,如何保证线程安全(如何实现线程同步)
定义:在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
如何保证:
- synchronized 关键字;
- Lock 接口;
- volatile + CAS;(volatile 保证了可见性以及防止了指令重排,但是不具备原子性,所以要配合 CAS)
- atomic 原子类。
深入点说,线程安全可以体现在三个方面,分别是原子性、可见性和有序性(Java 内存模型的三个特性)。针对这三点,Java 提供了底层封装的 API 来保证线程安全:
首先是 synchronized,它能保证原子性、可见性和有序性,相对重量级。
需要注意的是,synchronized 虽然能保证原子性,但其实是每一个 synchronized 块可以看成是一个原子操作,它保证每个时刻只有一个线程执行同步代码,非原子操作仍然可能发生指令重排,如 new 操作并不是原子操作,所以单例 double check 仍然需要使用 volatile 来防止指令重排。再比如自增不是原子操作,可能发生指令重排,但重排对结果没有影响,因为 synchronized 保证了块之间的有序执行。
volatile 能保证可见性和有序性,并能真正防止指令重排。
需要注意的是,volatile 的有序性指的是指令与指令之间有序,它本身不具有原子性,如自增操作(可分解为 1.读取原始值;2.加一;3.写入工作内存),多线程下,虽然 volatile 保证了指令不重排,但并不能保证自增过程中没有其它线程插入执行,导致脏数据。
synchronized 有序性保证了块与块之间的有序
volatile 有序性保证了指令与指令之间的有序
根本原因是
synchronized 靠系统内核互斥锁实现
volatile 靠内存屏障
其它保证线程安全的还有 Lock、 Atomic 类以及 concurrent 包。
synchronized 能防止指令重排序吗-链接
cas 原理-链接
10. 如何保证 List 线程安全(线程间操作 List)
- Vector。效率低,已过时。
- java.util.Collections.SynchronizedList。它能把所有 List 接口的实现类转换成线程安全的 List,由于它所有方法都是带同步对象锁的,即使是读操作,所以性能也一般。
- java.util.concurrent.CopyOnWriteArrayList。并发包里的并发集合类。即复制再写入,就是在添加元素的时候,先把原 List 列表复制一份,再添加新的元素。添加元素时,先加锁,再进行复制替换操作,最后再释放锁。获取元素则没有加锁,提升了读取性能。
11. Java 对象的生命周期
创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。
12. 谈谈 synchronized 中的类锁、方法锁和可重入锁
类锁包括:
- synchronized 修饰静态方法
- synchronized 锁代码块,锁为类
对象锁包括:
- synchronized 修饰成员方法(即方法锁,属于对象锁的一种)
- synchronized 锁代码块,锁为对象
可重入锁:
可重入锁就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
可重入原理:
每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM 会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放锁。
13. synchronized 的原理
synchronized 底层是通过一个 monitor 的对象来完成,其实 wait/notify 等方法也依赖于 monitor 对象,这就是为什么只有在同步的块或者方法中才能调用 wait/notify 等方法,否则会抛出 java.lang.IllegalMonitorStateException 异常的原因。
14. synchronized 与 Lock 的区别
- synchronized 是 Java 的关键字,来自 JVM 层面,而 Lock 是一个类。
- synchronized 是可重入、不可中断、非公平的锁,而 Lock 可重入、可中断、可公平。
- synchronized 无法判断锁状态,Lock 可以。
- synchronized 发生异常会自动释放锁,因此不会死锁;Lock 发生异常,不会主动释放锁,必须手动 unlock 来释放锁,可能引起死锁。(所以最好将同步代码块用 try-catch 包起来,finally 中写入 unlock,避免死锁的发生)
至于性能,1.6 版本优化后 synchronized 要比 Lock 快很多,参考 synchronized 实现原理和锁的优化。
15. 什么是死锁,如何避免死锁
多个线程因竞争多个资源而造成的一种相互等待的情况,若无外力作用,这些进程都将无法向前推进。
避免死锁的技术:
- 加锁顺序。线程按照一定顺序加锁;
- 加锁时限。给尝试获取锁加上一定的时限,超时则放弃获取锁,并释放自身占有的锁,然后等待一段随机的时间再重试;
- 死锁检测。主要针对不可能实现按序加锁并且锁超时也不可行的场景。例如,线程 A 请求某锁,但是该锁被线程 B 持有,这时就可以检查下线程 B 是否在请求线程 A 当前所持有的锁,如果有则死锁。检测后有俩种方式解决死锁问题:一种是释放所有锁,等待一段随机时间后重试。另一种是设置优先级,并让一部分线程回退重试。
16. ReentrantLock 的内部实现
17. 死锁的四个必要条件
- 互斥条件。即在一段时间内某资源仅为一个进程所占有。
- 不可剥夺条件。进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走。
- 请求与保持条件。进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件。存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
18. 线程池常见问题
1. 为什么使用线程池,线程池的优势或好处
- 节省频繁创建线程的开销;
- 避免线程栈溢出导致进程崩溃;
2. 线程池类的继承关系
线程池的真正实现:ThreadPoolExecutor implements ExecutorService
创建固定类型线程池的类:Executors
3. ThreadPoolExecutor 有哪些参数,作用分别是什么
核心线程数量:默认情况下,核心线程一直在线程池中存活,即使处于闲置状态。
最大线程数:达到该线程数,后续新任务会被阻塞。
最大存活时间:非核心线程闲置时的超时时间,超过该时间非核心线程会被回收。
时间单位:最大存活时间的时间单位。
BlockingQueue:任务队列;
线程工厂:创建线程的线程工厂;
4. 线程池的任务执行规则
- 线程数量未达核心线程数量,则直接启动一个核心线程执行任务;
- 线程池中的线程数量已经达到或超过核心,则进入任务队列等待;
- 如果无法插入到任务队列,则队列已满;此时如果线程数量未达到最大线程数,则启动一个非核心线程来执行任务;
- 如果队列已满且线程数量大于等于最大线程数,则拒绝任务,并抛出 RejectedExecution 异常。
5. 有哪些固定类型的线程池
FixedThreadPool:只有核心线程,用于更快响应外界请求。
CacheThreadPool:没有核心线程,线程空闲时会被复用,闲置超过 60s 则会被回收。因为任务会被立即执行,适合执行大量耗时少的任务。
SingleThreadPool:只有一个核心线程,且任务队列无上限,主要用来顺序执行耗时任务。
ScheduledThreadPool:核心线程数量固定,非核心线程可以无限大,回收时间为 10s,用来执行定时任务或周期性重复任务。
19. 谈谈对多线程(并发编程)的理解
1. 为什么使用多线程(使用多线程的好处,使用多线程解决了哪些问题)
- 充分利用计算机处理器性能,提高程序运行的效率;
- 任务处理异步化,不阻塞主任务。
2. 如何实现多线程
- 通过 new Thread;
- 通过线程池。
3. 多线程有哪些问题,如何解决多线程问题
- 线程安全问题。关于线程安全问题,参考【多线程-9-什么是线程安全,如何保证线程安全】
- 死锁问题。关于死锁问题,参考【多线程-15-什么是死锁,如何避免死锁】
- 资源消耗问题。无限制的创建线程不仅导致资源消耗,还可能因线程栈溢出导致进程崩溃。
20. 谈谈你对多线程同步机制的理解
说白了就是:什么是线程同步(安全)?如何做到线程同步(安全)?
参考【多线程-9-什么是线程安全,如何保证线程安全】
21. 多线程断点续传原理
大致原理,理解即可..
断点续传:
- 客户端从临时文件读取断点值并发送给服务端。
- 服务端与客户端将文件指针移至断点处(RandomAccessFile)
- 从断点处传输文件。
多线程下载:
将源文件按长度为分为 M 块文件,然后开辟 N 个线程,每个线程传输一部分,最后合并所有线程文件。
Http 相关
1. https 和 http 的区别
https 协议需要到 ca 申请证书,一般免费证书很少,需要交费。
http 是超文本传输协议,信息是明文传输,https 则是具有安全性的 ssl 加密传输协议。
http 和 https 使用的是完全不同的连接方式用的端口也不一样,前者是 80,后者是 443。
2. https 的请求过程
3. 描述下网络的几层结构
层级 | 作用 | 协议 | 关键字 |
---|---|---|---|
实体层 | 把电脑通过物理方式连接起来,负责传送 0 1 信号 | ||
链路层 | 对 0 1 信号分组,数据最长 1500 字节 | 以太网协议 | MAC 地址(每块网卡的唯一地址),电脑到电脑 |
网络层 | 引进新的地址,区分不同计算机是否属于同一子网络 | IP 协议 | IP 地址,电脑到电脑 |
传输层 | 表示数据包供哪个进程使用。 | UDP TCP 协议 | 端口到端口 |
应用层 | 区分不同数据格式 | HTTP FTP |
其中 http 数据放在 TCP 数据包的数据部分,也就是图中的【应用层数据包】。
4. http 访问网页的完整流程
- 访问 DNS 服务器,根据域名获取 IP 地址;
- 根据子网掩码判断是否属于同一子网络,不属于则通过网关转发;
- 先后封装为 http 数据包、TCP 数据包、IP 数据包、以太数据包,经过网关转发,到服务器。
- 服务器分别解析上述数据包,获取 http 请求,作出 http 响应,再按上述流程发回。
步骤 3 封装图:
5. 描述下 http 的三次握手
- 客户端向服务器发送 SYN (同步)包;
- 服务器向客户端发送 ACK (确认)包,同时也向客户端发送一条 SYN 包,俩者放到同一请求里;
- 客户端向服务器发送 ACK 包。
6. 网络连接中使用到哪些设备
设备名称 | 对应层、协议 | 作用 |
---|---|---|
集线器 | 实体层 | 连接设备 |
交换机 | 链路层 | 解决设备冲突,实现任意俩台设备的互联,即 MAC 地址 |
路由器 | 网络层 | 通过 IP 地址区分子网络,实现互联网连接。 |
7. http2 和 http1.1 的区别
8. 描述下网络通信协议
一般提问方式有:对网络通信协议熟悉吗?对 socket 和 http 了解吗?能解释下吗?
网络通信协议可以简单分为五层,分别是应用层(http)、传输层(TCP、UDP)、网络层(IP)、链路层(MAC)、物理层。
套接字(Socket)是支持 TCP/IP 协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的 IP 地址,本地进程的协议端口,远地主机的 IP 地址,远地进程的协议端口。
应用层通过传输层进行数据通信时,TCP 会遇到同时为多个应用程序进程提供并发服务的问题。多个 TCP 连接或多个应用程序进程可能需要通过同一个 TCP 协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与 TCP/IP 协议交互提供了套接字(Socket)接口。应用层可以和传输层通过 Socket 接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
建立 Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket ,另一个运行于服务器端,称为 ServerSocket。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
http 是超文本传输协议,是应用层协议,依赖于 TCP/IP 协议,是一种请求相应式、无连接、无状态的协议,请求结束后,会自动释放连接。
http1.0 中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。
http1.1 中,则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。
http2.0 中,新增了多路复用,支持压缩算法。
https 在 http 的基础上新增 ssl 层,使用加密传输(会进行加解密验证),端口号是 443。
9. 描述下 http 的消息结构
请求
请求行:包括请求方法(Get、Post...)、URL(/hello.txt)、协议版本(HTTP/1.1)
请求头:大量键值对,如 User-Agent:xxx、Host:xxx 等
空行
请求数据:post 请求数据在此
示例:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
响应
状态行:包括协议和状态码,如 HTTP/1.1 200 OK
消息报头:大量键值对,如 Content-Type:text/html、Content-Length:122 等等
空行
响应正文:响应结果数据
示例:
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
10. http 的请求类型有哪些
类型 | 功能 |
---|---|
OPTIONS | 返回服务器针对特定资源所支持的 http 请求方法。也可以利用向 Web 服务器发送'*'的请求来测试服务器的功能性。 |
HEAD | 向服务器索要与 GET 请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。 |
GET | 向特定的资源发出请求。 |
POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的创建或已有资源的修改。 |
PUT | 向指定资源位置上传其最新内容。 |
DELETE | 请求服务器删除 Request-URI 所标识的资源。 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断。 |
CONNECT | http1.1 协议中预留给能够将连接改为管道方式的代理服务器。 |
虽然 http 的请求方式有 8 种,但是我们在实际应用中常用的也就是 get 和 post,其他请求方式也都可以通过这两种方式间接的来实现。
Android 基础
1. 四大组件及其简单介绍
Activity、Service、BroadcastReceiver、ContentProvider
Activity
活动,可以简单的理解为页面。
生命周期:onCreate、onStart(可见不可交互)、onResume(可见可交互)、onPause(可见不可交互)、onStop(不可见)、onDestroy、onRestart。
四种状态:Running(激活)、Paused(可见不可交互)、Stopped(不可见不可交互)、Killed(被杀掉或启动以前)
Service
服务,可以后台运行,不需要呈现页面。Service 是运行在主线程的,如果要耗时操作,可以在 Service 中创建一个子线程来执行任务,或者使用 IntentService。
俩种启动方式:
- start。生命周期为:onCreate → onStartCommand → onDestroy
- bind。生命周期为:onCreate → onBind → onUnbind → onDestroy
onStart 方法已被废弃
使用 start 方式多次启动时,onCreate 只执行一次,onStartCommand 执行多次,但仅存在一个 Service 实例。该 Service 将会一直在后台运行,而不管 Activity 是否还存在,直到被调用 stopService,或自身的 stopSelf。当然如果系统资源不足,android 系统也可能结束服务。
使用 bind 方式多次启动时,onCreate、onBind 均只执行一次,onStartCommand 始终不会被调用。当连接建立之后,Service 将会一直运行,除非调用 Context.unbindService 断开连接或者之前调用 bindService 的 Context 不存在了,系统将会自动停止 Service,对应 onDestroy 将被调用。
既 start,又 bind 的服务,则该 Service 将会一直在后台运行。仅调用 unbindService 或 stopService 将不会停止 Service,而必须同时调用来停止服务(测试结果是这样,网上有各种争论..)。
IntentService 特性:
会创建独立的工作线程来处理所有的 Intent 请求;
会创建独立的工作线程来处理 onHandleIntent 方法实现的代码,无需处理多线程问题;
所有请求处理完成后,IntentService 会自动停止;
为 Service 的 onBind 提供默认实现,返回 null;
为 Service 的 onStartCommand 提供默认实现,将请求 Intent 添加到队列中;
BroadcastReceiver
广播接收器,用于接收广播。
俩种类型:
- 无序广播。能被所有接收者接收,传递效率高。
- 有序广播。传递有序,接收者可以将处理结果传给下个接收者,并且可以终止广播的传播。
俩种注册方式:
- 静态注册。Androidmanifest 中注册,常驻,即使应用程序关闭,程序也会被系统调用。
- 动态注册。跟随 context 的生命周期。如 activity 关闭时,其注册的广播也会被移除。
广播接收顺序:
- 有序广播时,优先级高的先接收。
- 无序广播或同优先级广播接收器,动态高于静态。
- 同优先级的同类广播接收器,先注册的优先于后注册的。
ContentProvider
内容提供者。支持在多个应用间存储和读取数据,相当于数据库。
它的作用就是将程序的内部的数据和外部进行共享,为数据提供外部访问接口,被访问的数据主要以数据库的形式存在,而且还可以选择共享哪一部分的数据。这样一来,对于程序当中的隐私数据可以不共享,从而更加安全。ContentProvider 是 Android 中一种跨程序共享数据的重要组件。
2. Activity 在各种场景下的生命周期变化
1.正常情况
启动:create → start → resume
返回键退出: pause → stop → destroy
2.Home 键
按 Home 键:pause → stop
再次启动:restart → start → resume
3.覆盖:
被半透明或非全屏 Activity 覆盖:A.pause → B.create → B.start → B.resume
从覆盖 Activity 回退:B.pause → A.resume → B.stop → B.destroy
4.跳转
跳转到新 Activity:A.pause → B.create → B.start → B.resume → A.stop
从新 Activity 回退:B.pause → A.restart → A.start → A.resume → B.stop → B.destroy
5.弹出 dialog
当 Activity 弹出 dialog 对话框的时候,Activity 不会调用 onPause();只有当 Activity 启动了 dialog 风格的 Activity 时才会调用。
6.异常终止
onStop 前调用 onSaveInstance
3. Activity 的四种启动模式
standard
默认启动模式,顺序压入栈
singleTop
如果新 activity 位于任务栈栈顶的时候,activity 不会被重新创建,同时回调 onNewIntent 方法。
实际生命周期函数为 a.pause → a.onNewIntent → a.resume
应用场景:推送通知页,避免启动多个推送页。
singleTask
如果 activity 实例在栈中存在,那么会将该 activity 弹至栈顶,其上 activity 全部弹出。生命周期同 singleTop。
应用场景:退出到主界面。
singleInstance
如果实例不存在,则创建实例并放置到独立的栈中,里面有且仅有它自己一个实例,以后如果继续启动该 activity,则直接将栈移动到前台。
singleInstance 由于其特殊性,下面根据具体场景说明 activity 的切换过程。
a 为 standard,b 为 singleInstance。
Q1:a 启动 b,b 再启动 a,此时栈信息是怎样的?
A1:初始 a 所在栈中有俩个 a 实例,另一个栈中有一个 b 实例。
Q2:在新启动的 a 页面点击返回,返回到初始 a 还是 b 页面?
A2:初始 a。因为在跳转到新的 a 页面时,已经切换了栈。
Q3:在 Q2 的基础上再次点击返回,返回到哪个页面?
A3:返回到 b 页面,因为此时 a 所在栈已经空了,所以切换下一个栈。
4. Activity 之间的通信方式
- Intent。
- 类的静态变量。
- Application。
- EventBus 事件总线。
- 借助 SharedPreference、SQLite、File、Service 等。
5. App 的启动流程,以及 Activity 的启动过程
Android App 启动流程源码分析
源码分析 Activity 可见性真实时机
Activity 启动与 App 的启动原理机制上大同小异,熟悉 App 的启动过程即可。
6. 横竖屏切换的时候,Activity 的生命周期
1.未配置 configChanges:
onPause → onStop → onDestroy → onCreate → onStart → onResume
即重新调用各个生命周期,切横屏执行一次,切竖屏执行俩次。
2.配置 configChanges = "orientation"
重新调用各个生命周期,无论横竖屏都只执行一次。
3.配置 configChanges = "orientation|keyboardHidden|screenSize"
不会调用各个生命周期,只执行 onConfigurationChanged 方法。
自从 Android 3.2(API 13),在设置 Activity 的 android:configChanges="orientation|keyboardHidden" 后,还是会重新调用各个生命周期函数。因为 screen size 也开始跟着设备的横竖切换而改变。所以,在 AndroidManifest.xml 里设置的 MinSdkVersion 和 TargetSdkVersion 属性大于等于 13 的情况下,如果想阻止程序在运行时重新加载 Activity,除了设置 "orientation", 你还必须设置 "ScreenSize"。
7. Activity 与 Fragment 之间生命周期比较
Fragment 的生命周期:
onAttach → onCreate → onCreateView → onActivityCreated → onStart → onResume → onPause → onStop → onDestroyView → onDestroy → onDetach
onAttach:Fragment 和 Activity 建立关联时调用;
onCreateView:Fragment 创建视图时调用;
onActivityCreated:相关联的 Activity 的 onCreate 方法已返回时调用;
onDestroyView:Fragment 中的视图被移除时调用;
onDetach:Fragment 和 Activity 取消关联时调用。
下面是当 Activity 包含一个 Fragment 的时候,Activity 和 Fragment 生命周期的变化,其中静态加载和动态加载有所区别。
下面 f 指代 fragment,a 指代 activity,以下为亲测数据。
静态加载:
f.onAttach → f.onCreate → f.onCreateView → a.onCreate → f.onActivityCreated → f.onStart → a.onStart → a.onResume → f.onResume → f.onPause → a.onPause → f.onStop → a.stop → f.onDestroyView → f.destroy → f.detach → a.destroy
动态加载
a.onCreate → f.onAttach → f.onCreate → f.onCreateView → f.onActivityCreated → f.onStart → a.onStart → a.onResume → f.onResume → f.onPause → a.onPause → f.onStop → a.stop → f.onDestroyView → f.destroy → f.detach → a.destroy
有几个需要注意的点:
- 静态注册和动态注册的区别在于,f.onAttach、f.onCreate、f.onCreateView 和 a.onCreate 的先后顺序不同。
- 后续同类生命周期方法,均是 fragment 先于 activity 执行,onResume 方法除外。
- 这里的顺序是以方法执行完毕为准的,比如 a.super.onStart 中调用了 f.onStart,可以说 a.onStart 先执行(a.onStart 先开始执行,以方法开始执行为准),也可以说 a.onStart 后于 f.onStart 执行(a.onStart 后执行完毕,以方法执行完毕为准)。因此网上可能有不同的顺序,个人觉得没必要太较真,了解下图的对应关系即可,如果想知道详细的顺序,可以参考再后一张图。
简化示意图:
完整流程图:
8. Fragment 在各场景下的生命周期变化
1.Home 键或跳转至其它 Activity
按下 Home 键或跳转到其他 Activity:onPause → onStop
重新打开或回退:onStart → onResume
2.使用 replace 替换 fragment
b.onAttach → b.onCreate → a.onPause → a.onStop → a.onDestroyView → a.destroy → a.detach → b.onCreateView → b.onActivityCreated → b.onStart → b.onResume
3.使用 show/hide 切换 fragment
生命周期无调用
9. Fragment 之间以及和 Activity 的通信
1. Activity 将数据传给 Fragment
- 使用 bundle
- 使用广播
- EventBus
2. Fragment 将数据传给 Activity
- 实现接口回调形式(onAttach 中将 Context 强转为接口类型)
- 使用广播
- EventBus
3. Fragment 之间通信
- 借助 Activity 传 bunndle
- 定义接口
- EventBus
10. 本地广播和全局广播有什么差别?
1、本地广播:发送的广播事件不被其他应用程序获取,也不能响应其他应用程序发送的广播事件。本地广播只能被动态注册,不能静态注册。动态注册需要用到 LocalBroadcastManager。
2、全局广播:发送的广播事件可被其他应用程序获取,也能响应其他应用程序发送的广播事件。全局广播既可以动态注册,也可以静态注册。
本地广播实际还是通过 Handler 实现,只不过利用了 HashMap 和 List 来维护广播接收者、IntentFilter、Intent 三者之间的关系。
11. ContentProvider、ContentResolver、ContentObserver 之间的关系
- ContentProvider:内容提供者,对外共享数据,可以通过 ContentProvider 把应用中的数据共享给其他应用访问。
- ContentResolver:内容解析者,其作用是按照一定规则访问内容提供者的数据。
- ContentObserver:内容观察者,目的是观察特定 URI 引起的数据库的变化,继而做一些相应的处理,类似于数据库技术中的触发器,当 ContentObserver 所观察的 URI 发生变化时,便会触发。
12. 广播使用的方式和场景
方式:静态注册、动态注册。详见【Android 基础-1-四大组件及其简单介绍】
在 Android 系统中,为什么需要广播机制呢?广播机制,本质上它就是一种组件间的通信方式,如果是两个组件位于不同的进程当中,那么可以用 Binder 机制来实现,如果两个组件是在同一个进程中,那么它们之间可以用来通信的方式就更多了,这样看来,广播机制似乎是多余的。然而,广播机制却是不可替代的,它和 Binder 机制不一样的地方在于,广播的发送者和接收者事先是不需要知道对方的存在的,这样带来的好处便是,系统的各个组件可以松耦合地组织在一起,这样系统就具有高度的可扩展性,容易与其它系统进行集成。
在软件工程中,是非常强调模块之间的高内聚低耦合性的,不然的话,随着系统越来越庞大,就会面临着越来越难维护的风险,最后导致整个项目的失败。Android 应用程序的组织方式,可以说是把这种高内聚低耦合性的思想贯彻得非常透彻,在任何一个 Activity 中,都可以使用一个简单的 Intent,通过 startActivity 或者 startService,就可以把另外一个 Activity 或者 Service 启动起来为它服务,而且它根本上不依赖这个 Activity 或者 Service 的实现,只需要知道它的字符串形式的名字即可,而广播机制更绝,它连接收者的名字都不需要知道。
引自 Android 系统中的广播机制简要介绍
- 需要低耦合通信时使用;
- 由于性能消耗大,同类型的观察者模式或者 EventBus 也能解决。故一般当需要进程间通信或者监听系统广播事件时,选择 BroadcastReceiver。比如监听网络情况,有网重试。
- 全局监听场景。比如推送;
13. 广播里可以直接启动 Activity 吗
onReceive 中 context 参数,如果是静态注册的广播,context 为 ReceiverRestrictedContext,所以在这里启动一个 Activity,需要在 intent 中添加 Intent.FLAG_ACTIVITY_NEW_TASK;如果是动态注册的广播,context 为当前注册时所在的组件,比如 Activity 或者 Service(Service 里启动 Activity 的话,也需要添加 FLAG_ACTIVITY_NEW_TASK)。
14. AlertDialog、PopupWindow 的区别
AlertDialog 是 Dialog 的子类,需要通过 builder 来创建对话框。通常用于提示用户做一些额外的行为,要求用户做出行动才能继续进行。
适用场景:输入账号密码、请求权限、警告等,总之是需要用户明确知道一些信息,用户做进一步操作前,需要确定或者填入信息。
PopupWindow 是 android.widget 包里的类,因此是悬浮窗的一种,只不过这个悬浮窗是悬浮在当前 activity 之上的。PopupWindow 可以用来显示任意视图,是出现在当前 activity 上的一个浮动容器。
适用场景:输入的补全信息、下拉选择的菜单,可以是一些提示的信息。
俩者对 activity 的生命周期都没影响
15. getApplication 和 getApplicationContext 的区别
没区别,来源归根结底都是来自 LoadedApk.makeApplication()
创建/直接返回的 Application,因此是同一个 Application,只不过返回途径不一样而已。
源码分析:getApplication 和 getApplicationContext 的区别
16. Application 和 Activity 的 Context 对象的区别
- Application 的 Context 直接继承自 ContextWrapper,而 Activity 的 Context 直接继承自 ContextThemeWrapper。
- Application 的 Context 生命周期贯穿整个 App 存活期间,而使用 Activity 的 Context 则可能导致内存泄漏。
- Application 相较 Activity,不能 showDialog、startActivity(需要 Flag) 以及 LayoutInflate(直接使用默认主题)。
- 一个应用只能有一个 Application Context。
17. 描述下 Android 的几种动画
- 帧动画。GIF
- 补间动画。有透明、大小、位置、旋转等,补间动画的 View 不是真正的移动,只是视觉意义。
- 属性动画。真正改变属性的动画。
- 触摸反馈动画。如水波纹效果。
- 揭露动画。背景色扩散效果。
- 转场动画。
- Vector 矢量动画。
18. 属性动画的特征
- 不局限于 View。
- 真实改变 View 属性。
- 动画效果丰富,支持自定义组合属性。
19. 如何导入已有的外部数据库
android 系统的数据库应该存放在 /data/data/(package name)/ 目录下,所以我们需要用 FileInputStream 读取原数据库,再用 FileOutputStream 把读取到的数据写入到该目录。
20. Android 中三种播放视频的方式
- 使用自带播放器。指定 Action 为 ACTION_VIEW,Data 为 uri,Type 为其 MIME 类型。
- 使用 MediaController + VideoView。
- 使用 MediaPlayer + SurfaceView。
21. 介绍下 Android 中数据存储的方式
- SharedPreference;用于保存键值对数据。
- 文件存储;
- SQLite 数据库;
- ContentProvider。为存储和获取数据提供了统一的接口,可以在不同应用程序之间共享数据。
- 网络存储。
22. SharedPreference 中 commit 和 apply 的区别
- apply 没有返回值而 commit 返回 boolean 表明修改是否提交成功;
- apply 是将修改数据提交到内存, 而后异步真正提交到硬件磁盘, 而 commit 是同步的提交到硬件磁盘。
效率上,apply > commit
安全上,commit > apply
23. 什么是混淆,Android 中哪些类不能混淆
混淆,将类名、方法名、成员变量等重命名为无意义的简短名称,以增加逆向工程的难度,同时通过移除无用的类减少包的大小。
以下为不能混淆的类:
- 反射使用的类或变量;
- bean 对象;
- 四大组件;
- 注解;
- 枚举;
- JNI 调用的 Java 方法;
- Java 调用的 Native 方法;
- JS 调用的 Java 方法;
- JavaScript 方法;
- Parcelable 序列化相关类;
- Gson、EventBus 等。
24. 什么是依赖倒置
依赖倒置原则:程序要依赖于抽象接口,不依赖于具体实现。核心是面向接口编程。
高层不依赖底层,应该依赖抽象,抽象不应该依赖于细节,细节应该依赖于抽象。
25. Android 权限
1. 权限的分类
- 正常权限。AndroidManifest 配置即可获得;
- 危险权限。6.0 之后定义的权限,有权限组的概念。不仅需要需要在 AndroidManifest 中配置,还需要在使用前 check 是否真正拥有权限,以动态申请。
- 特殊权限。如通知栏权限、自启动、悬浮窗和无障碍辅助等。严格的来讲,这部分内容不属于 Android 权限部分。因为它们不需要在 app 中配置,而是需要用户到系统对应的设置页面打开开关。但是实际开发中,确实有这样的需求:检测能不能弹出通知,不能则提示用户,或直接跳转到对应页面,引导用户打开开关。故把这部分纳入权限的范畴。
2. Android 中读写 SD 卡需要权限吗
读写权限虽然是危险权限,但并不是只要读写就要配置这俩个权限:
内置存储:
Context.getFileDir():/data/data/应用包名/files/
Context.getCacheDir():/data/data/应用包名/cache/
内置存储读写不需要配置任何权限。
外置sd卡:
Context.getExternalFilesDir():SDCard/Android/data/应用包名/files/
Context.getExternalCacheDir():SDCard/Android/data/应用包名/cache/
API<19 需要配置权限,API>=19 不需要配置权限。
即对于配置了读写权限的 app,使用"SDCard/Android/data/应用包名/"读写操作时,4.4 系统以下因为配置了权限而能正常读写,4.4 及以上系统因为不需要权限亦能正常读写。但是为了不配置多余的权限,建议如下写:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
以上文件夹,当 app 卸载时会被系统自动删除。
其余 sd 卡位置,6.0 以上需要动态申请读写权限。
读写权限只是为了限制 app 污染用户 sd 卡,保护手机安全,对于 app 而言,读写操作是正常而必要的,故划分出以上几个文件,使 app 在无需权限的情况下能正常存储必要的文件,且能在被卸载时自动删除这些文件,以达到保护用户的目的。
26. Android Profiler
Android Profiler 提供了四种工具,分别对 CPU、内存、流量和电量作监测,它们分别是 CPU Profiler、Memory Profiler、Network Profiler 和 Energy Profiler。
Android Profiler(一)CPU Profiler
Android Profiler(二)Memory Profiler
CPU Profiler 主要用来检测这些卡顿:方法耗时过长、启动耗时过长、帧渲染过长等。
Memory Profiler 主要用来检测这些卡顿:内存占用过高、内存泄漏、内存抖动导致的频繁 GC 卡顿。
使用 Memory Profiler 检测以下几种内存泄漏:
- 累积性内存泄漏。泄漏内存不断累积,最危险、最容易导致 OOM 的内存泄漏,容易检测。
- 覆盖性内存泄露。泄漏对象会被新泄漏的对象覆盖,同样相对容易检测。
- 短期内存泄漏。只会在短时间内泄漏,如子线程耗时任务、网络调度等,不容易检测,如果是重型页面、又恰好用户手机不佳,还可能导致 OOM,这种情况推荐使用 LeakCanary 去检测。
27. Android 项目图片文件夹对应的分辨率分别是
尺寸 | 分辨率 | dpi |
---|---|---|
ldpi | 240 x 320 | 120 |
mdpi | 320 x 480 | 160 |
hdpi | 480 x 800 | 240 |
xhdpi | 720 x 1280 | 320 |
xxhdpi | 1080 x 1920 | 480 |
Android 源码机制
1. LruCache 和 DiskLruCache 原理
源码分析 LruCache
源码分析 DiskLruCache
LruCache
- LruCache 会创建一个固定大小的缓存池,并维持一个 LinkHashMap 来有序的缓存数据。
- 在往缓存池 put 或 get 数据的时候,LinkHashMap 会将最近使用的数据移动到队尾。
- 在往缓存池 put 数据的时候,LruCache 会计算当前已缓存数据的大小。如果当前缓存数据超过了限定值,LruCache 会将 LinkHashMap 队首的数据删除,直到缓存数据大小满足限定值。
DiskLruCache 和 LruCache 区别不大,都是利用 LinkHashMap 实现缓存算法。只不过 DiskLruCache 是硬盘缓存,故需要持久化 LinkHashMap 中维持的 Lru 顺序关系。
Journal 文件详细的记录了缓存的操作记录,以便于 app 启动时,可以根据之前的操作记录,恢复 LinkHashMap 的数据。这份数据包括:有哪些缓存,以及这些缓存的 Lru 顺序。
2. Android 插件化和热修复
1. 为什么使用插件化
Android 开发中经常有这样的情况:模块化不清晰、方法数超限、想在不重新安装 App 的情况下增加新的模块,基于这样的需求,催生出了 Android 插件化技术。
2. 什么是插件化/插件化的基本原理是什么
利用 DexClassLoader 可以加载外部 dex 文件以及包含 dex 的压缩文件(apk 和 jar)的特点,通过 Hook 技术来启动外部 dex 文件中的 Activity。
3. 详细的描述下插件化实现/如何利用插件化启动外部 apk
Android 启动 Activity,大致可以分为以下几步:
- 当前 Activity 通知 AMS 检测 AndroidManifest 中是否有目标 Activity;
- AMS 检测到有目标 Activity,则通知当前 Activity 休眠;
- AMS 检测目标 Activity 进程是否存在,不存在通知 Zygote fork 进程;
- 启动目标 Activity。创建 Context,并通过 ClassLoader 创建 Activity。
因为要实现插件化,启动外部 apk,所以可以依赖第 1、4 步骤实现。
针对第一步,在 AndroidManifest 中创建占位的 Activity,然后通过 Hook,将本进程里,把要送检的 Activity 替换为占位 Activity 以绕过检测。同时,将真正要启动的 Activity 暂存在 Intent 中。
针对第四步,会进入 ActivityThread 的 performLaunchActivity 方法,也就是要启动 Activity 之前,再次利用 Hook 技术将真正要启动的 Activity 还原出来。在启动目标 Activity 时,利用 DexClassLoader 将外部 dex 文件加载进来,创建目标 Activity 的 Class 文件,同时将外部资源等也一并加载传入,然后启动目标 Activity 即可。
实际过程是复杂的,插件化的难点在于需要对系统源码非常熟悉,以选择合适的 Hook 点,另外就是系统版本的兼容性。
4. 热修复了解吗,原理是什么
热修复和插件化的原理大同小异,都是利用 DexClassLoader 来加载外部 dex 文件。
Android 加载 Class 文件使用的是 ClassLoader 及其双亲委托原理,即由下至上委托,由上至下查找,查找本应用的类则是在 PathClassLoader 中,系统创建了一个 Dex 文件数组,会依次遍历数组,直到找到目标 Class 为止。
利用这个原理,可以使用 DexClassLoader 将包含修复 Class 的 Dex 文件加载进来,然后利用 Hook 技术,将目标 Dex 插入到数组前列,这样加载类时由于先找到,实现了替换。
当然这只是热修复方案的一种。其他方法还有:
- 底层替换方案,原理是替换 C 中对 Java 方法的调用,如替换入口、替换 ArtMethod 结构体(该结构体包含了 Java 方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址)。优点是立即生效不用重启。
- Instant Run 方案。利用 ASM 字节码操作库,在方法中注入替换代码。该代码会判断函数是否有修改,以决定是否执行修改后的代码。
5. 插件化、热修复系列博客
1. ClassLoader 详解 (Class 的加载)
2. HookJava 基础 (如何 Hook)
3. Android App 启动流程源码分析 (Activity 的启动源码)
4. 从零开始的 Android 插件化:宿主 app 加载 sd 卡 apk (如何一步一步实现基础插件化)
5. 热修复原理 (热修复的种类、原理等)
6. 热修复原理与基础范例
3. Android 组件化
1. 为什么组件化
随着 app 版本迭代,业务越来越复杂,单一模块的功能越来越多,每个工程师需要熟悉大量的代码,而且很难多人协作,无论是维护还是编译都耗时耗力。而且单一功能耦合严重,根本无法做单元测试,所以必须要有更加灵活的架构。
安全性、可读性、可维护性、可扩展性都差
2. 如何实现组件化/描述下组件化方案
关于组件化,可以从以下几个方面描述:
- 组件化基础架构。包括架构图,以及参考组件化框架 ComponentLight 源码,说明如何利用辅助框架更好的实现架构应用。(解决切换成本高、开发运行不同依赖状态干扰的问题)
- 组件化通信。EventBus 解耦。
- 组件化跳转。隐式启动、ARouter 或 SimpleRouter。
- 组件化存储。GreenDao 对象关系型数据库。
- 组件化权限。一种方式是仅将危险权限放入到具体子 module,好处是移除模块危险权限也跟着移除;还有一种是将所有权限都放入到具体子 module,好处是最大程度解耦,但是增加了编译合并检测的消耗。另外权限申请框架有 AndPermission。
- 组件化混淆。这里推荐是仅在主 module 混淆、子 module 不混淆。因为这样可以避免重复混淆而导致资源找不到,但缺点是需要子 module 开发人员与主 module 同步,存在沟通成本。
- 组件化分发。参考 Activity 组件化分发结构。主要优势是:不是通过 Activity 直接控制 Manager 来执行相关模块的业务,而是让模块通过关注分发的生命周期,独立的完成自身业务,最大化解耦。
- 组件化流通。打包成 aar、上传 maven 等。参考 Android 仓库解析。
- Gradle 优化。也可以参考 Gradle 核心 个人笔记。
- 其他优化。如 findViewById,可以使用 ButterKnife(不推荐)、DataBinding(不推荐)、ViewBinding。再如编译流程优化,可以使用 gradle-task-tree、InstantRun、并行编译、重复任务过滤等。
组件化中需要注意的问题:
- AndroidManifest 是会合并的,因此要注意包名、Application 冲突等问题。
- 子 module 编译的静态变量不能是 final 的,因此 switch 场景不能使用,需要转换为 if-else 形式。
- 同名资源,会保留主 module 资源,所以布局起名最好以 module 名开头作区分。
Android 组件化架构 个人笔记
Android 组件化框架 ComponentLight
Activity 组件化分发结构
4. Handler 机制
1. 描述下 Handler 的原理
Handler 主要用于跨线程通信。涉及 MessageQueue、Message、Looper/Handler 这四个类。
首先是 Message,它是消息,是线程间通信的数据媒介;然后是 MessageQueue,它是消息队列,主要功能是向消息池投递信息(MessageQueue.enqueueMessage)和取走消息池的信息(MessageQueue.next)。它是个阻塞队列,因此当消息为空时,它会阻塞取消息的线程。最后是 Looper,它会不断循环的从 MessageQueue 中取出消息并执行。
以主线程为例说明,主线程在启动时(ActivityThread.main),会调用 Looper.prepare、Looper.loop,来创建 MessageQueue,并开启死循环从 MessageQueue 中取出消息,执行 Message.target.handleMessage 方法。当消息队列为空时,此循环会被阻塞。子线程通过调用 Handler.sendMessage 方法,将消息发送到消息队列中,此时队列不为空,循环被唤醒,主线程获取消息并执行。
整个过程类似生产-消费者模型。
2. 一个线程可以有几个 Looper、几个 MessageQueue 和几个 Handler
Looper 利用了 ThreadLocal,因此每个线程只有一个 Looper。
Looper 构造函数中创建了 MessageQueue 对象,因此一个线程只有一个 MessageQueue。
Handler 在创建时与 Looper 和 MessageQueue 产生关联,因此可以有多个。
3. 可以在子线程直接创建一个 Handler 吗,会出现什么问题,那该怎么做
不能,Handler 依赖于 Looper,而 Looper 是消费者线程(这里是子线程)消费 Message 的关键,因此必须在 Looper.prepare 之后。
class MyThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare(); // 为线程创建 Looper 对象
mHandler = new Handler() {
public void handleMessage(Message msg) {
}
};
Looper.loop(); // 启动消息循环
}
}
4. Looper 死循环为什么不会导致应用卡死,会消耗大量资源吗
可以简单的将 MessageQueue 理解为阻塞队列,当没有消息时,主线程会进入阻塞,释放 CPU 资源。binder 线程会通过 Handler 来唤醒主线程,而 binder 线程则会与系统 AMS 进行通信。实际上 Android 四大组件的生命周期也是这么运行的。
深入点讲,MessageQueue 实际上是管道。涉及到 Linux pipe/epoll 机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 Loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制,是一种 IO 多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步 I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量 CPU 资源。
真正会卡死主线程的操作是在回调方法 onCreate/onStart/onResume 等操作时间过长,会导致掉帧,甚至发生 ANR,looper.loop 本身不会导致应用卡死。
5. ANR 的原理
产生 ANR 的条件:
- 输入事件 5s 内没有处理完毕;
- BroadcastReceiver 的 onReceive 函数时 10 秒内没有处理完毕;
- Service 的各个生命周期函数 20 秒内没有处理完毕。
导致 ANR 的原因:
- 主线程执行了耗时操作;如网络请求、数据库操作、文件操作。
- 其他【进程】占用 CPU,导致本进程得不到 CPU 时间片,比如其他进程的频繁读写操作可能会导致这个问题。
- UI 线程等待子线程释放锁,从而无法处理用户请求;
- 耗时动画导致 CPU 负载过重。
ANR 的实现原理:
在应用收到输入事件、广播或运行服务时,AMS 的 Handler 会收到消息,并开启一个定时任务(收到什么消息、定多长时间,视场景而定),如果未超时,则会取消掉该任务,否则抛出 ANR 异常。
ANR 的分析:
- 本地结合 logcat 日志分析。
- 远程使用保存在手机本地的 /data/anr/traces.txt 日志文件分析。
6. MessageQueue 是队列吗,它是什么数据结构
MessageQueue 不是队列,它内部使用一个 Message 链表实现消息的存和取。 链表的排列依据是 Message.when,表示 Message 期望被分发的时间,该值是 SystemClock. uptimeMillis 与 delayMillis 之和。
7. Handler.postDelayed 函数延时执行计时是否准确
当上一个消息存在耗时任务的时候,会占用延时任务执行的时机,实际延迟时间可能会超过预设延时时间,这时候就不准确了。
8. Handler.sendMessageDelayed 是延迟的把消息放入到 MessageQueue 中的吗
不是。
Handler.sendMessageDelayed 实际调用了 sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
,最终是在 queue.next 方法中处理的。
queue.next 在取消息时,如果头部 Message 是有延迟且时间未到的,不返回 Message 而且会计算下时间,然后利用 nativePollOnce 进行阻塞。期间如果有新的消息进来,并且这个消息不是延迟、或是比当前延迟时间短,这个消息就插入头部并唤醒线程。
这么设计的原因是,如果通过延迟将消息放入到 MessageQueue 中,那么多个延迟消息就需要多个定时器。
参考源码
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
SystemClock.uptimeMillis 是系统从开机启动到现在的时间,期间不包括休眠的时间,获得到的时间是一个相对的时间。之所以使用这种方式来计算时间,而不是获得当前 CurrentTime 来计算,在于 handler 会阻塞、挂起、睡眠等,这些时候是不应该执行的。如果使用绝对时间的话,就会抢占资源来执行当前 handler 的内容,显然这是不应该出现的情况,所以要避免。
MessageQueen.enqueueMessage 插入消息,MessageQueue.next 取消息。由此也可知:消息在加入消息队列时就进行好排序,链表头的延迟时间小,尾部延迟时间最大。
9. 你了解 HandlerThread 吗
HandlerThread 可以作为消费者,在子线程里处理 Message,其原理实际就是实现了 Looper.prepare、Looper.loop。
Handler 小节参考资料 谈谈 Handler 机制和原理
Handler 小节参考资料 Handler postDelayed 的原理
5. Bundle 机制
1. 介绍下 Bundle
Bundle 是 Android 中传递数据的容器,通常用在 Intent、Message.setData、Fragment.setArguments 等相互通信的方法中。
Bundle 使用 final 修饰,意味着不可继承。它实现了 Parcelable 接口。内部则是通过 ArrayMap 去存储数据。
2. ArrayMap 原理
ArrayMap 主要是针对内存优化的数据结构。
内部有俩个数组存储数据,一个数组负责存储 key 的 hash 值,另一个数组负责存储 key 和 value。数组按照从小到大的顺序排序,添加、删除、查找数据的时候都是先使用二分查找法得到相应的 index,然后通过 index 来进行添加、查找、删除等操作。
原理图参考 此链接
6. HashMap 源码,SparseArray 原理
1. HashMap 的工作原理
HashMap 基于哈希原理,hashCode 最大的特点是:不同的对象,hashCode 可能相同;同一个对象,hashCode 一定相同。基于此,利用数组 + 链表的形式来实现 HashMap。
HashMap 通过 put 和 get 方法储存和获取对象。当将键值对传递给 put 方法时,它调用键对象的 hashCode 方法来计算哈希值,然后找到 bucket 位置来储存值对象。如果当前位置已有对象了,对象将会储存在链表的下一个节点中。 当获取对象时,同样是先用键对象的 hashCode 定位 bucket 位置,然后利用 equals 找到目标对象。
因此重写 hashCode,一是要保证同一对象的 hashCode 一定相同,二是要保证不同对象的 hashCode 可以相同、可以不同,但分布要均匀。
2. HashMap 和 HashTable 的区别
- HashMap 不是线程安全的,HashTable 则相反;(ConcurrentHashMap 同样是线程安全的,且扩展性更好)
- HashMap 可以接受为 null 的键值。
- 父类不同。HashMap 是 AbstractMap,而 HashTable 继承自 Dictionary。
让 HashMap 转为同步的方法:Map concurrentMap = Collections.synchronizeMap(hashMap);
3. HashMap 和 HashSet 的区别
- 实现接口不同。HashMap 实现了 Map 接口,HashSet 实现了 Set 接口;
- HashMap 存储键值对,HashMap 仅存储对象;
- HashMap 使用 put 添加对象,HashSet 使用 add。
没什么特别的区别,照着 map 和 set 的区别说即可。
4. HashMap 的大小超过了负载因子(load factor)定义的容量会怎么办
HashMap 默认的负载因子大小为 0.75,也就是说,当一个 map 填满了 75% 的 bucket 时候,和其它集合类(如 ArrayList)一样,将会创建原来 HashMap 大小的两倍的 bucket 数组,来重新调整 map 的大小,并将原来的对象放入新的 bucket 数组中。
5. 为什么 String、Interger 这样的类适合作为键
- 它们是 final、不可变的,因为键值改变可能导致 hashCode 改变,如果放入时和获取时返回不同的 hashCode,那就不能正确找到对象。
- 它们已经重写了 hashCode 和 equals 方法。
引申:什么样的对象可以作为键?
6. 介绍下 SparseArray
稀疏数组。
SparseArray 是 android 里为 <Interger, Object> 这样的 Hashmap 而专门写的类,目的是提高内存效率,其核心是折半查找函数(binarySearch)。
SparseArray 有两个优点:1.避免了自动装箱(auto-boxing)2.数据结构不依赖于外部对象映射。(SparseArray 内部使用两个一维数组来保存数据,一个用来存 key,一个用来存 value)
7. 介绍下 SurfaceView
如果绘制过程很复杂,并且界面更新还非常频繁,这时候就会造成界面的卡顿,影响用户体验,为此 Android 提供了 SurfaceView 来解决这一问题。
View 和 SurfaceView 的区别:
- View 适用于主动更新,SurfaceView 适用于被动更新,比如频繁刷新界面;
- View 在主线程中刷新,而 SurfaceView 则开启一个子线程来进行刷新。
- SurfaceVie 在底层机制中实现了双缓冲机制。
双缓冲主要是为了解决反复局部刷屏带来的闪烁,把要画的东西先画到一个内存区域里,然后整体的一次性画出来。
8. Android 里有哪些内存泄露
长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
- 静态变量持有 Context;
- 匿名内部类导致内存泄漏。如子线程耗时任务、Handler、TimerTask 等。解决办法:使用静态内部类代替内部类,使用弱引用持有 Context,或在 onDestroy 中移除 Message;
- 监听没有取消注册;
- 资源对象未关闭。如 Cursor、File、Stream、Bitmap 等。因为资源性对象往往都用了一些缓冲,缓冲不仅存在于 java 虚拟机内,还存在于 java 虚拟机外。如果仅仅是把它的引用置 null,而不关闭它们,也会造成内存泄漏。
- 容器类持有对象;
- WebView。解决方案是单开一个进程。
检测内存泄漏的工具:
- Memory Profiler;
- LeakCanary;
- MAT;输入 HRPOF 文件,输出分析结果。
9. Android app 里有哪些性能优化方案
优化种类、方案很多,视情况而定,以下是一些类型及其举例。
内存优化:
- 内存泄漏的优化。静态内部类 + 弱引用、onDestroy 及时移除 Message,并把相关资源置空,取消注册、及时关闭 Cursor、Bitmap 记得调用 recycle 等。
- 内存占用的优化。Bitmap 压缩和复用,枚举使用注解替代等。
- 内存抖动的优化。避免在 onDraw 等频繁调度的方法或循环中创建对象。
包大小优化:
- 删除重复的库。
- 删除无用的资源。
- 使用混淆。(混淆可以删除未用到的库)
- 图片使用 SVG、WebP 替代。
稳定性优化:
- 日志埋点监控。如 Firebase、无痕埋点等。crash 监控 + 报警。
流畅性优化:
- 使用线程池解决耗时问题;
- 避免过度绘制,避免绘制层级过高;
- 使用 merge、ViewStub、include 等元素。
- 列表优化。如将过重的 item 拆分为多个 item,因为滑动至可见时才加载,所以变相优化了重量级的 item。
include:将布局中的公共部分提取出来供其他 layout 复用;
merge:用于消除视图层次结构中的冗余视图。例如根布局是 LinearLayout,如果又 include 一个 LinerLayout 布局,不仅无意义,还会减慢 UI 加载速度。
ViewStub:占位 View。优点是需要时才会加载,否则不会占用内存。通常用于如新手引导、消息提醒等。
省电性优化:
- 加锁等待,让出 CPU 资源,避免无意义的循环等待。
- 加缓存,如使用享元模式等。
- 对网络数据进行压缩。
安全性优化:
- 使用 so 库存储密钥。
- 使用 local.properties 存储密钥,密钥本地流通,不上传 Git。
- 对本地数据、网络数据进行加密等。
- 额外加固。如梆梆加固。
构建优化:
- Gradle 优化、使用 Gradle 插件、进行分包(Gradle3.2.x 分包填坑)等。
10. Android 的系统架构
- 应用程序层。顶层所有的应用程序,如通讯录、浏览器等等。
- 应用程序框架层。为应用程序提供的高级服务。如 ActivityManager、WindowManager、PackageManager、ResourceManager、LocationManager 等等。
- 系统运行库层。一系列 C/C++ 程序库的集合,包括用于存储的 SQLite、用于网络安全的 SSL、用于播放音视频的库、浏览器引擎等,Android 的一些核心库(如 android.app)以及虚拟机也在此层。
- Linux 内核层。Linux 提供了系统最基本的功能:进程管理、内存管理、设备管理以及网络和大量设备驱动。
11. Android 启动如何优化
App 启动我们可以优化的过程包括【主 Application 启动】和【主 Activity 启动】,因此针对这俩个点做优化。
Application:
- 延迟加载。一些耗时的 sdk 可以延迟、或需要时、或使用 IdleHandler 在系统空闲时再加载。
- 异步加载。把不需要同步等待的业务全部放到异步加载。
- 闪屏优化。因为冷启动会先启动空白窗口,再执行 Application 初始化,所以可以先加背景图,给用户视觉已启动的感觉。
- 缓存数据。对于一些更新频率比较低的配置信息,或者资源等,可以采用缓存的方式避免每次启动都去下载,从而节省启动时间和 CPU 资源。
- dex 优化。通过分包、精简主包的方式去优化启动读取 class 的效率。
MainActivity:
- 布局优化。简化层级、减少过度绘制、暂时不需要显示的 View 可以使用 ViewStub 占位。
- 延迟加载。为了让用户尽快看到主界面,在首页绘制完毕后再做部分必须要在 UI 线程中执行的逻辑,实现方式是通过监听 onFirstDrawFinish。
- 异步。
12. 一个应用程序安装到手机上时发生了什么
此问题等同于应用程序的安装过程。
- 将 apk 复制到 data/app 目录下;
- 解压 apk,将 dex 拷贝至 data/dalvik-cache,命名为 apk 路径 + classes.dex;
- 在 data/data 目录下创建对应的包名目录,并在此目录下创建存储应用数据的相关目录。
13. App 是如何沙箱化,为什么要这么做;
如何沙箱化:即每一个 Android 应用程序都在自己的进程中运行,都拥有一个独立的虚拟机实例。应用之间相互隔离、独自运行并禁止相互访问。
至于权限隔离,基础是建立在 Linux 已有的 uid、gid 上的 。Android 每安装一个应用程序,就会为它分配一个 uid。(其中普通 Android 应用程序的 uid 是从 10000 开始分配,10000 以下是系统进程的 uid)而对于普通应用程序来说,gid 等于 uid。
为什么要沙箱:
主要是出于安全性考虑。
- 数据安全。应用之间隔离数据不可胡乱访问。
- 系统安全。一个进程、虚拟机的崩溃不会导致其他应用出现问题。
- 权限管理。可以为不同进程划分不同权限,方便系统管理。
14. 图片优化,如何压缩、如何缓存
优化方案
- 压缩图片;有质量压缩(像素不变,改变位深和透明度)、尺寸压缩(降低像素)、采样率压缩(好处是不会将大图读入内存,缺点是不能很好的保证图片质量)
- 内存缓存;使用 LruCache 最近最少使用缓存。
- 使用软引用;内存吃紧则回收对象。
15. 如何降低程序崩溃率
- 首先要保证代码质量。开发代码要有统一的约定;开发时 Lint 全局扫描,解决报警问题;提交代码有完善的 review 检测机制。
- 发版前测试。包括 QA 全量回归测试、云平台各种机型稳定性测试、崩溃分析等等。
- 上线后的系统监控。如 Firebase 线上监控、Google 统计、无痕埋点等。
- 上线后的解决方案:热修复(小 bug)或重发稳定版本(提前准备稳定版本,重大 bug 发版覆盖)。
16. OOM 原因及如何定位
OOM 的原因:
- 内存泄漏导致。
- 加载的文件或图片过大;
定位方法:
- 使用 Memory Profiler,动态跟踪内存使用信息;
- 使用 LeakCanary 定位内存泄漏;
- 使用 MAT,分析 hprof 文件,检测内存泄漏。
17. 描述下 Glide
Android 的图片加载和缓存库。
特色优化:
1.滑动监听,滑动时暂停加载,停止时开始加载,对应方法:resumeRequests,pauseRequests。
2.默认基于 HttpUrlConnection,可以替换为 OkHttp。
细节:
1.基于三级缓存,内存、硬盘、网络。
2.弱引用 + LruCache 共同完成内存缓存功能。
3.内存缓存最大为:每个进程最大可用内存 * 0.4,低配手机会替换为 0.33。
4.磁盘缓存为 250m。
18. AsyncTask 机制及缺点(了解)
AsyncTask 内部其实就是使用子线程(更准确的是 FutureTask)+ Handler 实现的。
缺点如下:
- AsyncTask 不会随 Activity 销毁而销毁,而一直要等 doInBackground 执行完毕,可能导致内存泄漏;
- 内部线程池上限为 128 个线程,当队列已满,将会抛出 RejectedExecutionException,而且还存在大量消耗系统资源导致系统崩溃的风险。
在 Android 1.6 之前的版本,AsyncTask 是串行的,在 1.6 至 2.3 的版本,改成了并行的。在 2.3 之后的版本又做了修改,可以支持并行和串行,当想要串行执行时,直接执行 execute 方法,如果需要并行执行,则要执行 executeOnExecutor(Executor)。
19. 列举几种常见的 RuntimeException,并说明 RuntimeException 和 Exception 区别
常见的RuntimeException:
- NullPointerException 空指针异常
- ClassCastException 类型转换异常
- IllegalArgumentException 传递非法参数
- IndexOutOfBoundsException 数组下标越界
- NumberFormatException 数组格式异常
- SecurityException 安全异常
Error 和 Exception 区别:
Error 是编译时错误和系统错误,而 Exception 是可以被抛出的基本类型。
RuntimeException 和 Exception 区别:
Exception 必须被开发者解决后才能编译通过,解决方式可以 throw 到上层,也可以 try-catch 处理。如 IOException,而 RuntimeException 是运行时异常,不受异常检查。
20. OOM 是否可以 try-catch
只在特殊情况下可行:即 try 中声明了很大的对象,导致 OOM。
但是通常情况下,OOM 可能是由内存泄漏等其他原因引起的,很难捕获。针对大对象,Java 也提供了 SoftReference、LruCache 解决内存问题。
21. try-catch 实现原理是什么,对性能有影响吗
实现原理:
当把 Java 源码编译成相应的字节码,如果方法内有 try-catch 异常处理,就会产生与该方法相关联的异常表,异常表记录的是 try 的起点和终点、catch 方法体所在的位置,以及声明捕获的异常种类。通过这些信息,当程序出现异常时,Java 虚拟机就会查找方法对应的异常表,如果发现有声明的异常与抛出的异常类型匹配就会跳转到 catch 处执行相应的逻辑,如果没有匹配成功,就会回到上层调用方法中继续查找,如此反复,一直到异常被处理为止,或者停止进程。所以,try 在反映到字节码上的就是产生一张异常表,只有发生异常时才会被使用。
对性能的影响:
try-catch 与未使用 try-catch 代码区别在于,前者阻止 Java 对 try 块内代码的一些优化,例如重排序。try-catch 里面的代码是不会被编译器优化重排的。所以在实际编程中,提倡 try 代码块的范围尽量小,这样才可以充分发挥 Java 对代码的优化。
Android View
1. View 基础
2. View 的生命周期如何变化,以及生命周期和 Activity 的对应关系
View 生命周期调用顺序
View 与 Activity 生命周期关联关系
3. 描述下 Android View 的加载、绘制流程
加载流程:
- Activity.setContentView 间接调用了 PhoneWindow.setContentView;
- 在此方法中通过 LayoutInflate 去加载对象,原理是利用 xml 的 pull 解析去解析 xml 布局文件,最终通过反射创建 view。
绘制流程:
Activity 启动时,会将 DecorView 与 ViewRoot 建立关联,之后 ViewRoot 的 requestLayout 会被调用,最终将调用 ViewRootImpl.performTraversals,它是整个绘制的起点。
performTraversals 会依次调用 measure、layout、draw 三大绘制方法。其中 measure 用于计算视图大小,layout 用于摆放视图位置,draw 用于绘制视图。
draw 的绘制过程:
- 绘制背景;
- 通过 onDraw 绘制自身内容;
- 通过 dispatchDraw 绘制子 View;
- 绘制滚动条;
4. onTouchListener、onTouchEvent、onClickListener 的执行顺序
优先级上,onTouchListener > onTouchEvent > onClickListener,onTouchListener 返回 true 则拦截事件。
5. 描述下 View 事件传递分发机制
Activity → Window(PhoneWindow)→ DecorView → ViewGroup → View
ViewGroup.dispatchTouchEvent → ViewGroup.onInterceptTouchEvent → View.dispatchTouchEvent → View.onTouchEvent → ViewGroup.onTouchEvent
各种场景下的各生命周期方法最好亲测熟记。
Android 第三方库源码解析
1. Volley
2. OkHttp
3. RxJava
4. Retrofit
5. EventBus
6. Jetpack
持续更新中...
目录大纲
Java 基础
- ==、equals 和 hashCode 的区别
- 基础数据类型各占多少字节
- 对多态的理解
- String、StringBuffer 与 StringBuilder 的区别
- 父类的静态方法能否被子类重写
- 进程和线程的区别
- final、finally 和 finalize 的区别
- Serializable 和 Parcelable 的区别
- 内部类的设计意图及种类
- 锁机制 synchronized、volatile、Lock
- 常见编码和字节占用数
- 深拷贝和浅拷贝的区别
- 静态代理和动态代理的区别,什么场景使用
- 如何将一个 Java 对象序列化到文件里
- 谈谈对注解的理解
- 谈谈对依赖注入的理解
- 谈谈对泛型的理解
Java 虚拟机
- APK 的生成
- APK 的安装方式(JIT、AOT)、Dalvik 和 ART 的异同等
- Java 内存结构
- Java 内存模型
- Java 引用类型
- 垃圾识别策略
- 垃圾回收算法
- 类的加载过程(如何从 class 转变为被虚拟机识别的类)
- 虚引用的功能及原理
Java 多线程
- 开启线程的几种方式
- 线程的几种状态
- 如何控制某个方法允许并发访问线程的个数
- wait、sleep 的区别
- 什么可以导致线程阻塞/为什么会出现线程阻塞
- 线程如何关闭
- Java 中实现同步的几种方法(如何实现线程同步)
- ThreadLocal 原理,实现及如何保证 Local 属性?
- 什么是线程安全?如何保证线程安全?(如何实现线程同步)
- 如何保证 List 线程安全?(线程间操作 List)
- Java 对象的生命周期
- 谈谈 synchronized 中的类锁、方法锁和可重入锁
- synchronized 的原理
- synchronized 与 Lock 的区别
- 什么是死锁,如何避免死锁
- ReentrantLock 的内部实现
- 死锁的四个必要条件
- 线程池常见问题
- 为什么使用线程池,线程池的优势或好处
- 线程池类的继承关系
- ThreadPoolExecutor 有哪些参数,作用分别是什么
- 线程池的任务执行规则
- 有哪些固定类型的线程池
- 谈谈对多线程(并发编程)的理解
- 为什么使用多线程(使用多线程的好处,使用多线程解决了哪些问题)
- 如何实现多线程
- 多线程有哪些问题,如何解决多线程问题
- 谈谈你对多线程同步机制的理解
- 多线程断点续传原理
Android 基础
- 四大组件及其简单介绍
- Activity 在各种场景下的生命周期变化
- Activity 的四种启动模式
- Activity 之间的通信方式
- App 的启动流程,以及 Activity 的启动过程
- 横竖屏切换的时候,Activity 的生命周期
- Activity 与 Fragment 之间生命周期比较
- Fragment 在各场景下的生命周期变化
- Fragment 之间以及和 Activity 的通信
- 本地广播和全局广播有什么差别?
- ContentProvider、ContentResolver、ContentObserver 之间的关系
- 广播使用的方式和场景
- 广播里可以直接启动 Activity 吗
- AlertDialog、PopupWindow 的区别
- getApplication 和 getApplicationContext 的区别
- Application 和 Activity 的 Context 对象的区别
- 描述下 Android 的几种动画
- 属性动画的特征
- 如何导入已有的外部数据库
- Android 中三种播放视频的方式
- 介绍下 Android 中数据存储的方式
- SharedPreference 中 commit 和 apply 的区别
- 什么是混淆,Android 中哪些类不能混淆
- 什么是依赖倒置
- Android 权限
- 权限的分类
- Android 中读写 SD 卡需要权限吗
- Android Profiler
- Android 项目图片文件夹对应的分辨率
Http 相关
- https 和 http 的区别
- https 的请求过程
- 描述下网络的几层结构
- http 访问网页的完整流程
- 描述下 http 的三次握手
- 网络连接中使用到哪些设备
- http2 和 http1.1 的区别
- 描述下网络通信协议
- 描述下 http 的消息结构
- http 的请求类型有哪些
Android View
- View 基础
- View 的生命周期如何变化,以及生命周期和 Activity 的对应关系
- 描述下 Android View 的加载、绘制流程
- onTouchListener、onTouchEvent、onClickListener 的执行顺序
- 描述下 View 事件传递分发机制
Android 源码机制
- LruCache 和 DiskLruCache 原理
- Android 插件化和热修复
- 为什么使用插件化
- 什么是插件化/插件化的基本原理是什么
- 详细的描述下插件化实现/如何利用插件化启动外部 apk
- 热修复了解吗,原理是什么
- 插件化、热修复系列博客
- Android 组件化
- 为什么组件化
- 如何实现组件化/描述下组件化方案
- Handler 机制
- 描述下 Handler 的原理
- 一个线程可以有几个 Looper、几个 MessageQueue 和几个 Handler
- 可以在子线程直接创建一个 Handler 吗,会出现什么问题,那该怎么做
- Looper 死循环为什么不会导致应用卡死,会消耗大量资源吗
- ANR 的原理
- MessageQueue 是队列吗,它是什么数据结构
- Handler.postDelayed 函数延时执行计时是否准确
- Handler.sendMessageDelayed 是延迟的把消息放入到 MessageQueue 中的吗
- 你了解 HandlerThread 吗
- Bundle 机制
- 介绍下 Bundle
- ArrayMap 原理
- HashMap 源码,SparseArray 原理
- HashMap 的工作原理
- HashMap 和 HashTable 的区别
- HashMap 和 HashSet 的区别
- HashMap 的大小超过了负载因子(load factor)定义的容量会怎么办
- 为什么 String、Interger 适合作为键
- 介绍下 SparseArray
- 介绍下 SurfaceView
- Android 里有哪些内存泄露
- Android app 里有哪些性能优化方案
- Android 的系统架构
- Android 启动如何优化
- 一个应用程序安装到手机上时发生了什么
- App 是如何沙箱化,为什么要这么做;
- 图片优化,如何压缩、如何缓存
- 如何降低程序崩溃率
- OOM 原因及如何定位
- 描述下 Glide
- AsyncTask 机制及缺点(了解)
- 列举几种常见的 RuntimeException,并说明 RuntimeException 和 Exception 区别
- OOM 是否可以 try-catch
- try-catch 实现原理是什么,对性能有影响吗
Android 第三方库源码解析
- Volley
- OkHttp
- RxJava
- Retrofit
- EventBus
- Jetpack
[TOC]